读开源作者的源代码,就犹如跟作者一起对话一样,学到其中源码的精髓、思想,犹如大师在黑板上讲解项目的实现思路,收益匪浅,就说这么多,如果你有兴趣也一起来读源码吧。
1、找到开源项目
- 脚手架vite github.com/vitejs/vite
- vue3.0 github.com/vuejs/core
- pc ui element-plus github.com/element-plu…
- h5 ui vant github.com/youzan/vant
2、查看文件发现他们的大致共性
- 管理依赖: pnpm(全部)
- 实现仓库的monorepo: pnpm的workspace(全部)
- typescript:使用ts进行编写代码(全部)
- github yml: 工作流(全部)
- Git Hook 工具:husky + lint-staged(element-plus和vant)
- 代码规范: EditorConfig+Prettier + ESLint(除了vue3.0没有使用EditorConfig,其他三个仓库都用了)
- 提交规范:Commitizen + Commitlint (element-plus)
- 打包工具: Rollup(vue3.0和element-plus) 、esbuild(vant和vite)
- 单元测试: vitest(element-plus和vite)、jest(vue3.0和vant)
3、接下来有时间我会学习一下以下知识
- pnpm和monorepo下的项目库、二次封装组件库、工具库
- 创建工程化项目
- 代码规范: EditorConfig+Prettier + ESLint
- 提交规范:Commitizen + Commitlint
- Git Hook 工具:husky + lint-staged
- github yml: 工作流
- typescript:现在的vue3项目中零零散散的也使用了一下ts,但还不够深入,有时间继续深入学习一下
- 打包工具: Rollup,通过Rollup开发一个组件库
- 单元测试: vitest和jest要学习一个,来练练手,工具库的单元测试还是比较好处理,现在是组件库的单元测试要学习一下
4、本文主要重点知识
- 上一篇博文通过google/zx写了自己公司15个项目的编译打包上传过程
- 接下来就来看看vue3.0源码中是如何通过脚本或者自动化的手段去处理打包发布的过程
5、严格校验使用pnpm 安装依赖
- 在项目根目录下使用yarn命令的话会有提示
yarn // 提示如下: yarn install v1.22.17 info No lockfile found. $ node ./scripts/preinstall.js This repository requires using pnpm as the package manager for scripts to work properly. error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
- 如果使用pnpm就不会有上述提示了,不过如果确实有要求要使用yarn命令
// 会忽略相应的前置钩子 prexxxx,和后置钩子 postxxxx。 yarn --ignore-scripts,
- 通过package.json中scripts脚本列表中的preinstall,做了判断处理
"preinstall": "node ./scripts/preinstall.js", // 查看scripts/preinstall.js文件 if (!/pnpm/.test(process.env.npm_execpath || '')) { console.warn( `\u001b[33mThis repository requires using pnpm as the package manager ` + ` for scripts to work properly.\u001b[39m\n` ) process.exit(1) }
查看vite脚手架 github.com/vitejs/vite…
// 一个命令行便可以限定只能使用pnpm "preinstall": "npx only-allow pnpm",
- 其实这两者干了同样一件事情,都是只允许使用pnpm进行执行scripts
6、调试script/release.js
- 先到package.json中找到scripts的 release
"release": "node scripts/release.js",
- 开启调试的方式
- 将鼠标悬浮于
release
上,可以看到[运行脚本]和[调试脚本] 点击调试脚本即可调试,当然要提前设置断点 - 或者可以看到
scripts
上方,会有一个调试按钮,点击选择 release即可进入调试状态
- 开启后终端有会如下显示
pnpm run release Debugger attached. > @3.2.36 release H:\github\sourceCode\core > node scripts/release.js Debugger attached. ? Select release type ... > patch (3.2.37) minor (3.3.0) major (4.0.0) custom
7、release.js中引用的依赖说明
- 依赖minimist:解析命令行中的参数
// 安装依赖 npm i minimist // 引入依赖 import minimist from 'minimist' console.log(process.argv, 'process') const argv = minimist(process.argv.slice(2)) console.log(argv, '打印参数列表') //通过node环境直接执行 node ./other/minimist.js -a aa -b bb -c cc // [ // 'C:\\Program Files\\nodejs\\node.exe', // 'H:\\github\\2022\\zx-ts\\other\\minimist.js', // '-a', // 'aa', // '-b', // 'bb', // '-c', // 'cc' // ] process // { _: [], a: 'aa', b: 'bb', c: 'cc' } 打印参数列表
可以发现其中process.argv的第一和第二个元素是Node可执行文件路径和被执行js文件的路径。
- chalk终端多色彩输出
npm i chalk import chalk from 'chalk' console.log(chalk.blue('打印参数列表'))
- semver 语义化版本 详细解释 semver.org/lang/zh-CN/
// 举个简单的例子版本号 2.0.1 // 版本号格式: 主版本号(major).次版本号(minor).修订号(patch) // 则 2为主版本号 0为次版本号 1为修订号 // major: 变化意味着本地变更发生了巨大的变化(当你做了不兼容的 API 修改) // minor: 通常只反映了一些较大的更改(当你做了向下兼容的功能性新增) // patch 通常称之为补丁版本(当你做了向下兼容的问题修正) // 再举个简单的例子: 2.0.1-beta.1 // 这个就相当于先行版本号 //release.js中涉及到的api //验证版本号 console.log(semver.valid('0.0.3'), 'valid验证版本号') // 0.0.3 ✔ console.log(semver.valid('0.0.3-beta.1'), 'valid验证版本号') // 0.0.3-beta.1 ✔ console.log(semver.valid('0.0.3.44'),'验证版本号0.0.3.44') // null ❌ // 获取先行版本号后的标识和版本号 console.log(semver.prerelease('0.0.3-beta.1'), 'prerelease1') // beta 1 ✔ console.log(semver.prerelease('1.0.0-alpha+001'), 'prerelease2') // alpha ❌ console.log(semver.prerelease('1.0.0-beta+exp.sha.5114f85'), 'prerelease3') // beta❌ console.log(semver.prerelease('1.0.0+b11111'), 'prerelease4') // null 错误❌ // 现有版本号为0.0.3,通过inc获取新的版本号 console.log(semver.inc(currentVersion, 'major'), 'inc-major') // 1.0.0 console.log(semver.inc(currentVersion, 'minor'), 'inc-minor') // 0.1.0 console.log(semver.inc(currentVersion, 'patch'), 'inc-patch') // 0.0.4
npm i semver
- enquirer 交互式询问CLI 简单说就是交互式询问用户输入。
npm i enquirer import enquirer from 'enquirer' let tempArray = ['major(1.0.0)','minor(0.1.0)', 'patch(0.0.4)', 'customer' ] const { release } = await enquirer.prompt({ type: 'select', name: 'release', message: 'Select release type', choices: tempArray }) if(release === 'custom') { console.log(release, 'customer') } else { const targetVersion = release.match(/\((.*)\)/)[1] console.log(targetVersion, 'targetVersion') }
执行命令后可以看到四个选项 major(1.0.0) minor(0.1.0) patch(0.0.4) customer 选择不同的选项,则根据不同的选项进行判断处理不同的逻辑
- execa 执行命令行的
import { execa } from 'execa' import {$} from 'zx' const arr = ['aaa', 'bbbb'] const { stdout } = await execa('echo', arr) console.log(stdout, 'stdout') // 这个是通过google/zx的神器调用的命令行,我自己感觉灰常好用 await $`echo -e ${arr} google/zx仓库`
- 在package.json中发现
run-s
查了半天没找到太多资料,原来是npm-run-all的缩写,虽然我对npm-run-all也不了解,但这个关键字的搜索信息就海量了。
- 串行执行 clean、 lint build命令
npm-run-all clean lint build // 同样可以使用缩写命令 run-s clean lint build
以前也可以使用 &&
进行串行执行命令
"XXX": "npm run clean && npm run lint && npm run build"
或者说是顺序执行三个命令,如果某个脚本退出时返回值为空值,那么后续脚本默认是不会执行的,不过你可以使用参数--continue-on-error 来规避这种行为。
- 并行执行 三个命令
npm-run-all --parallel clean lint build // 同样可以使用缩写命令 run-p clean lint build
以前也可以使用 &
进行并行执行命令
"XXX": "npm run clean & npm run lint & npm run build"
同时执行这三个任务,需要注意如果脚本退出时返回空值,所有其它子进程都会被 SIGTERM 信号中断,同样可以用 --continue-on-error 参数禁用行为。
8、vue3.0 release整个过程
- 1、选择要发布的版本:
- major
- minor
- patch
- custom
- 2、执行测试用例 执行测试用例分为了两个部分
await run(bin('jest'), ['--clearCache']) await run('pnpm', ['test', '--bail'])
- 第一行是执行
jest测试用例
第二行是执行命令行中的测试用例
"test": "run-s \"test-unit {@}\" \"test-e2e {@}\"", "test-unit": "jest --filter ./scripts/filter-unit.js", "test-e2e": "node scripts/build.js vue -f global -d && jest --filter ./scripts/filter-e2e.js --runInBand",
顺便来看看run
方法的实现
// 读取node_modules下.bin目录下传递进来的name const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name) // 通过execa来执行bin下的命名 const run = (bin, args, opts = {}) => execa(bin, args, { stdio: 'inherit', ...opts })
- 3、更新根目录package.json版本号
// const path = require('path') // 引用path模块,通过path模块读取并拼接路径、读取当前release.js文件所在路径,并返回上一级 let pkgRoot = path.resolve(__dirname, '..'), // 拼接根目录package.json所在路径 const pkgPath = path.resolve(pkgRoot, 'package.json') //const fs = require('fs') // 引用fs模块,通过fs模块读取package.json文件内容 // 再通过JSON.parse对读取的字符串内容进行转换,转换为JSON对象 const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) // 将第一步选择的新版本号version赋值给JSON对象的version pkg.version = version //因为在根目录下的package.json中不存在dependencies和peerDependencies节点,所以一下两行代码执行了也没有什么效果 //updateDeps(pkg, 'dependencies', version) //updateDeps(pkg, 'peerDependencies', version) // 根目录版本号设置完毕后,重新写会package.json文件 fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
- 4、更新packages文件夹下内部vue相关依赖的版本号
// 读取release.js的当前路径,并返回上一级目录,再拼接packages //读取此目录下的文件,并过滤掉以.ts结尾的文件和以'.'开头的文件 const packages = fs .readdirSync(path.resolve(__dirname, '../packages')) .filter(p => !p.endsWith('.ts') && !p.startsWith('.')) //通过循环去更新packages文件夹下单独包的版本号 packages.forEach(p => updatePackage(getPkgRoot(p), version)) // 传递pkg 文件夹名称(也就是包名)然后拼接到路径后面 const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg) //然后根据路径和版本号,跟第三部修改根目录下版本号代码师一致的
- 5、打包编译所有包 run方法上面有提到过,可以参看
await run('pnpm', ['run', 'build', '--release']) // test generated dts files step('\nVerifying type declarations...') await run('pnpm', ['run', 'test-dts-only']) 复制代码
- 6、生成changelog
await run(`pnpm`, ['run', 'changelog']) // 实际执行的是package.json中的scripts脚本 "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", // 可以发现实际使用的模块依赖是conventional-changelog-cli // 看到命令行中有一个angular很奇怪,查阅发现 // 如果你的所有commit都符合Angular commit规范,那么发布新版本时,就可以通过脚本自动生成changelog。
在conventional-changelog仓库下有一个推荐,standard-version
// 安装依赖 npm install -D standard-version // 通常使用前要先git add .,git commit -m ...,提交完成后再执行 如下命令 // 更新主版本号 1.0.0 => 2.0.0 standard-version -- --release-as major //更新次版本号1.0.0 => 1.1.0 standard-version -- --release-as minor // 更新修订版本号 1.0.0 =>1.0.1 standard-version -- --release-as patch
执行完上述命令时,如果提交存在feat和fix类型的话,就会自动生成CHANGELOG.md,当然你可以手动设置要更新到CHANGELOG.md中的类型。
- 7、更新pnpm-lock.yaml
await run(`pnpm`, ['install', '--prefer-offline'])
- 8、提交代码 通过
git diff
命令来判断是否有文件变更,如果有则通过git add . git commit -m ...
进行提交
const { stdout } = await run('git', ['diff'], { stdio: 'pipe' }) if (stdout) { step('\nCommitting changes...') await runIfNotDry('git', ['add', '-A']) await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`]) } else { console.log('No changes to commit.') }
顺便说一下runIfNotDry函数, run命名是真正的通过execa执行命名,而dryRun只是进行console.log打印,并没有真正的执行命令。然后isDryRun 读取命令行中是否存在dry参数,存在的话则不进行执行命令
const isDryRun = args.dry const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name) const run = (bin, args, opts = {}) => execa(bin, args, { stdio: 'inherit', ...opts }) const dryRun = (bin, args, opts = {}) => console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts) const runIfNotDry = isDryRun ? dryRun : run
- 9、将packages包发布到npmjs上 关于packages变量可以参考第四部分
// 循环对每个模块包进行yarn publish for (const pkg of packages) { await publishPackage(pkg, targetVersion, runIfNotDry) } //单个publish 核心代码(runIfNotDry可以参考第八部分中的说明) await runIfNotDry( 'yarn', [ 'publish', '--new-version', version, ...(releaseTag ? ['--tag', releaseTag] : []), '--access', 'public' ], { cwd: pkgRoot, stdio: 'pipe' } )
在npmjs.com中搜索你可以发现,packages文件夹下的所有包都单独存在的,说明都可以单独引用,也就是在某些项目中,可以单独下载引用某些库,这里可以说是一个发现。
- 10、将tag标签和commit进行push
await runIfNotDry('git', ['tag', `v${targetVersion}`]) await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`]) await runIfNotDry('git', ['push'])
第一行通过git tag打tag标签
第二行对tag标签,进行推送push
第三行对commit进行推送push
9、总结
- 了解到版本号可以自动设置
- changelog.md可以根据commit自动生成
- commit 规范 要搞起来,方便自动化
- 命令行的串行执行和并行执行
- node_modules下的.bin目录存放了各种命令工具
- 限制某个命令的运行,通过钩子函数去限制