简介
本文档记录了我参加 vue 3.2 是怎么发布的 vue-release 源码共读的过程中的学习和思考。
- 本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
- 这是源码共读的第3期,链接:Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?。
参与目的:
- 探索源码,理解工作原理
- 学习和成长
- 提升技术水平
作为一名前端开发者,参与这次源码共读活动。这是一个很好的机会,与志同道合的开发者一起深入探索源码、理解工作原理,并在过程中学习和成长。
本文参与学习内容:
- 熟悉 vuejs 发布流程
- 学会调试 nodejs 代码
- 动手优化公司项目发布流程
源码分析
1、源码准备
打开 vue-next, 开源项目一般都能在 README.md 或者 contributing.md 找到贡献指南先看说明,按照说明书进行配置准备运行环境
本文是直接使用的若川老师的源码。可根据自己想法下载对应源码
Node.js 版本要求是 10+
Yarn 的版本要求是 1.x Yarn 1.x 可自行下载
node -v
# v14.16.0
# 全局安装 yarn
#若川老师源码【建议】
git clone https://github.com/lxchuan12/vue-next-analysis.git
# macos clone可能需要添加 sudo
cd vue-next-analysis/vue-next
# 或者克隆官方项目源码
git clone https://github.com/vuejs/vue-next.git
cd vue-next
# 安装 yarn
npm install --global yarn
# 安装依赖
yarn # install the dependencies of the project
# yarn release
1.1、严格校验使用 yarn 安装依赖
先查看vue-next/package.json文件
preinstall设置一个前置文件,用于限制安装依赖,此处是限制的只能使用yarn ,如果使用其他包会报错
接下来看scripts/checkYarn.js文件,判断在项目中确保脚本通过Yarn 1.x版本来执行,如果不是则给出警告并退出程序
# 判断是否是通过yarn安装的或执行操作
if (!/yarn.js$/.test(process.env.npm_execpath || '')) {
console.warn(
'\u001b[33mThis repository requires Yarn 1.x for scripts to work properly.\u001b[39m\n'
)
# 如果不是给出警告并退出程序
process.exit(1)
}
1.2、调试 vue-next/scripts/release.js 文件
vue-next/package.json 文件打开,然后在 scripts 上方,会有debug(调试)按钮,点击后,选择 release。即可进入调试模式
此时会出现以下截图内容
注:此处我遇到一个报错,报错显示未找到minimist
我的解决方法是通过输入
sudo pnpm install minimist 命令安装了minimist包
2、文件开头的依赖引入和函数声明
2.1、第一部分
通过依赖找到对应安装的依赖
2.1.1、minimist 命令行参数
minimist 是一个轻量级的JavaScript库,用于在Node.js应用程序中解析命令行参数。它接收一个字符串数组(如 process.argv),并将其解析为一个对象,其中的键是选项名称,而值则是对应的选项值。
例如,如果你的命令行运行情况如下:
node myScript.js -f file.txt --verbose
在这段命令中,process.argv 将包含 ['node', 'myScript.js', '-f', 'file.txt', '--verbose'] 这样的数组。通过使用 minimist,可以将这些参数解析成:
{
f: 'file.txt',
verbose: true
}
这样就可以方便地在Node.js脚本中访问和处理这些命令行参数了。
2.1.2、chalk 终端多色彩输出
chalk 是一个流行的Node.js包,主要用于在终端(TTY)输出中添加色彩和样式。它可以让你在命令行界面以丰富多彩的方式展示文本信息,有助于提升代码日志、错误消息或者CLI工具的可读性和美观性。
例如,使用 chalk 可以这样编写颜色化的文本:
const chalk = require('chalk');
console.log(chalk.red('错误!')); // 输出红色的“错误!”
console.log(chalk.green.bold('成功!')); // 输出绿色加粗的“成功!”
console.log(chalk.blue.bgYellow('警告信息')); // 输出蓝色字体、黄色背景的“警告信息”
通过这种方式,可以更直观、清晰地呈现程序运行时的各种状态信息。
2.1.3、semver 语义化版本
semver 是一个遵循语义化版本规范(Semantic Versioning)的Node.js包。语义化版本规范(SemVer)是一种软件版本号命名约定,由主版本号、次版本号和修订号三部分组成(形如:MAJOR.MINOR.PATCH)。该规范规定了版本号更新时的规则,以便开发者理解和管理依赖关系。
semver 包提供了一系列方法来比较、解析、操作符合 SemVer 规范的版本字符串。例如,可以用来判断一个版本是否满足另一个版本的要求(如 ^1.2.3 是否兼容 1.3.0),或是在发布新版本时生成合适的版本号等。
以下是 semver 中的一些典型用法示例:
const semver = require('semver');
// 检查版本号是否满足条件
semver.satisfies('1.2.3', '^1.2'); // 返回 true
// 比较两个版本号
semver.gt('1.2.3', '1.2.2'); // 返回 true,表示'1.2.3'大于'1.2.2'
// 解析版本号
semver.parse('1.2.3-beta.4');
// 返回 { version: '1.2.3-beta.4', major: 1, minor: 2, patch: 3, prerelease: ['beta', '4'], build: [] }
// 生成下一个版本号
semver.inc('1.2.3', 'patch'); // 返回 '1.2.4'
2.1.4、 enquirer 交互式询问 CLI
Enquirer 是一个用于 Node.js 的强大交互式命令行用户界面 (CLI) 库
使用 Enquirer 可以简化命令行应用中的用户交互逻辑,并且其界面通常具有良好的用户体验。下面是一个简单的 Enquirer 使用示例:
const Enquirer = require('enquirer');
const prompt = new Enquirer.Prompt({
type: 'input',
name: 'username',
message: 'What is your username?'
});
prompt.run()
.then(answer => console.log('Answer:', answer))
.catch(console.error);
在这个例子中,Enquirer 创建了一个简单的问题提示用户输入他们的用户名。当用户提交答案后,程序会输出用户的输入
2.1.5、 execa 执行命令
execa 是一个 Node.js 库,用于执行外部命令并提供更好的控制和错误处理机制。相比于 Node.js 内置的 child_process 模块,execa 提供了更为便捷和强大的API,增强了错误处理功能,同时支持 Promise 和 async/await,使得异步命令执行更加流畅。
execa 主要特点包括:
- 支持同步和异步调用。
- 自动处理进程退出代码,并将其转换为可抛出的错误。
- 支持捕获子进程的标准输出和错误输出流。
- 能够轻松传递环境变量和选项给子进程。
- 兼容 Windows、Unix-like 系统,并对跨平台问题进行了良好处理。
以下是一个基本的 execa 使用示例:
const execa = require('execa');
(async () => {
try {
const { stdout } = await execa('ls', ['-lh', '/usr']);
console.log(stdout);
} catch (error) {
console.error(`执行命令失败: ${error.message}`);
}
})();
在上述代码中,我们异步地执行了 ls -lh /usr 命令,获取标准输出,并在出错时打印错误信息。
2.2、第二部分
const preId =
args.preid ||
(semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
const isDryRun = args.dry
const skipTests = args.skipTests
const skipBuild = args.skipBuild
const packages = fs
.readdirSync(path.resolve(__dirname, '../packages'))
.filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
以上描述的内容涉及到了参数解析、文件系统操作和版本号的预发布标识处理
2.3、第三部分
// 跳过的包列表
const skippedPackages = []
// 定义可以使用的版本增量
const versionIncrements = [
'patch',
'minor',
'major',
// 如果存在预发布标识,则添加预发布相关的版本增量
...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
// 创建一个函数来增量版本号
const inc = i => semver.inc(currentVersion, i, preId)
// 假设我们要发布一个补丁更新
const newVersion = inc('patch')
console.log('Current Version:', currentVersion) // 输出: Current Version: 1.0.0
console.log('New Version:', newVersion) // 输出: New Version: 1.0.1
更多相关内容可以查看semver文档
2.4、第四部分
// bin 命令
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
// 返回该包在目录下的绝对路径
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 控制台输出
const step = msg => console.log(chalk.cyan(msg))
2.4.1、bin函数
它接受一个命令行工具的名称(),并返回该工具在目录下的绝对路径。这是通过拼接(当前脚本的目录)、相对路径(指向项目根目录下的目录)和工具名称来实现的,name node_modules/.bin__dirname../node_modules/.bin/node_modules/.bin
2.4.2、run函数
const run = (bin, args, opts = {}) =>
execa(bin, args, { stdio: 'inherit', ...opts })
使用库来执行命令行命令。它接受三个参数:execa
bin:要执行的命令的路径(通常是通过函数获得的)。binargs:传递给命令的参数数组。opts:一个可选的对象,包含执行命令时的额外选项。
函数内部,被调用,并传入命令路径、参数数组和一个配置对象。配置对象中的确保子进程的输入、输出和错误流与父进程相同,这意味着命令的输出将直接显示在控制台上。execastdio: 'inherit'
2.4.3、dryRun函数
const dryRun = (bin, args, opts = {}) =>
console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
它模拟了函数的行为,但并不实际执行命令。它接受与函数相同的参数,并使用在控制台上以蓝色字体打印出将要执行的命令和选项。这通常用于在不实际更改系统的情况下预览脚本的行为。
2.4.4、runIfNotDry函数
const runIfNotDry = isDryRun ? dryRun : run
这里并没有直接定义一个函数,而是根据变量的值选择或函数。如果为真,则将是函数;否则,它将是函数。这样可以在脚本中方便地切换实际执行和模拟执行
3、main 主流程
3.1、函数大致内容
async function main() {
// 确定目标版本
let targetVersion = args._[0]
// 版本校验
if (!targetVersion) {
// no explicit version, offer suggestions
const { release } = await prompt({
type: 'select',
name: 'release',
message: 'Select release type',
choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
})
if (release === 'custom') {
targetVersion = (
await prompt({
type: 'input',
name: 'version',
message: 'Input custom version',
initial: currentVersion
})
).version
} else {
targetVersion = release.match(/\((.*)\)/)[1]
}
}
if (!semver.valid(targetVersion)) {
throw new Error(`invalid target version: ${targetVersion}`)
}
// 确认发布
const { yes } = await prompt({
type: 'confirm',
name: 'yes',
message: `Releasing v${targetVersion}. Confirm?`
})
if (!yes) {
return
}
// 运行测试
// run tests before release
step('\nRunning tests...')
if (!skipTests && !isDryRun) {
await run(bin('jest'), ['--clearCache'])
await run('yarn', ['test', '--bail'])
} else {
console.log(`(skipped)`)
}
// 更新包版本和依赖项
// update all package versions and inter-dependencies
step('\nUpdating cross dependencies...')
updateVersions(targetVersion)
// 生成包
// build all packages with types
step('\nBuilding all packages...')
// 验证类型声明
if (!skipBuild && !isDryRun) {
await run('yarn', ['build', '--release'])
// test generated dts files
step('\nVerifying type declarations...')
await run('yarn', ['test-dts-only'])
} else {
// 生成更改日志
console.log(`(skipped)`)
}
// 提交更改
// generate changelog
await run(`yarn`, ['changelog'])
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.')
}
// 发布包
// publish packages
step('\nPublishing packages...')
for (const pkg of packages) {
await publishPackage(pkg, targetVersion, runIfNotDry)
}
// 推送到 GitHub
// push to GitHub
step('\nPushing to GitHub...')
await runIfNotDry('git', ['tag', `v${targetVersion}`])
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
await runIfNotDry('git', ['push'])
// 试运行和跳过逻辑
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`)
}
if (skippedPackages.length) {
console.log(
chalk.yellow(
`The following packages are skipped and NOT published:\n- ${skippedPackages.join(
'\n- '
)}`
)
)
}
console.log()
}
- 确定目标版本:它首先尝试从命令行参数中获取版本的目标版本(可能使用类似 for 的内容)。如果未提供任何版本,则会提示用户选择发布类型或输入自定义版本。
minimist``args._[0] - 验证版本:使用类似实用程序的内容确保指定的目标版本是有效的语义版本。
semver - 确认发布:提示用户确认指定版本的发布。
- 运行测试:如果测试不打算跳过(并被选中),则它将继续运行与包关联的任何测试。
!skipTests!isDryRun - 更新包版本和依赖项:测试完成后,脚本将更新所有包的版本及其相互依赖项。
- 生成包:如果不跳过生成,则使用类型生成所有包。
- 验证类型声明:如果未跳过测试,则测试生成的 TypeScript 声明文件(文件)。
d.ts - 生成更改日志:运行脚本以生成新版本的更改日志。
- 提交更改:检查是否有修改(使用 ),并将它们提交到存储库,并显示一条指示发布版本的消息。
git diff - 发布包:循环访问包,如果不打算跳过包,则发布包。
- 推送到 GitHub:在 Git 中使用版本标记版本,并将标记和提交推送到源远程存储库。
- 试运行和跳过逻辑:输出试运行的结果,并通知发布期间是否跳过了任何包。
3.2、更新相关内容
3.2.1、updatePackage 更新包的版本号
// update all package versions and inter-dependencies
step('\nUpdating cross dependencies...')
updateVersions(targetVersion)
function updateVersions(version) {
// 1. update root package.json
updatePackage(path.resolve(__dirname, '..'), version)
// 2. update all packages
packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
function updatePackage(pkgRoot, version) {
// 获取package.json文件的绝对路径
const pkgPath = path.resolve(pkgRoot, 'package.json')
// 将 package.json 文件读取到内存中,并将字符串转换为 JavaScript 对象
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
// 将包对象的属性更新为传递到函数中的新版本
pkg.version = version
// packages.json 中 dependencies 中 vue 相关的依赖修改
updateDeps(pkg, 'dependencies', version)
// packages.json 中 peerDependencies 中 vue 相关的依赖修改
updateDeps(pkg, 'peerDependencies', version)
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
先执行 pnpm run release --dry 后执行 git diff查看git修改,部分截图如下:
3.2.2、updateDeps 更新内部 vue 相关依赖的版本号
function updateDeps(pkg, depType, version) {
const deps = pkg[depType]
if (!deps) return
Object.keys(deps).forEach(dep => {
if (
dep === 'vue' ||
(dep.startsWith('@vue') && packages.includes(dep.replace(/^@vue\//, '')))
) {
console.log(
chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`)
)
deps[dep] = version
}
})
}
然后执行yarn release --dry会得到如下内容
3.3、打包编译所有包
step('\nBuilding all packages...')
if (!skipBuild && !isDryRun) {
await run('yarn', ['build', '--release'])
// test generated dts files
step('\nVerifying type declarations...')
await run('yarn', ['test-dts-only'])
} else {
console.log(`(skipped)`)
}
3.4、生成 changelog
// generate changelog
await run(`yarn`, ['changelog'])
yarn changgelog对应的是如下
3.5、提交代码
经过更新版本号后,有文件改动,于是git diff。 是否有文件改动,如果有提交。
git add -A git commit -m 'release: v${targetVersion}'
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.')
}
3.6、发布包
// publish packages
step('\nPublishing packages...')
for (const pkg of packages) {
await publishPackage(pkg, targetVersion, runIfNotDry)
}
3.6.1、发布包函数
async function publishPackage(pkgName, version, runIfNotDry) {
if (skippedPackages.includes(pkgName)) {
return
}
const pkgRoot = getPkgRoot(pkgName)
const pkgPath = path.resolve(pkgRoot, 'package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
if (pkg.private) {
return
}
// For now, all 3.x packages except "vue" can be published as
// `latest`, whereas "vue" will be published under the "next" tag.
let releaseTag = null
if (args.tag) {
releaseTag = args.tag
} else if (version.includes('alpha')) {
releaseTag = 'alpha'
} else if (version.includes('beta')) {
releaseTag = 'beta'
} else if (version.includes('rc')) {
releaseTag = 'rc'
} else if (pkgName === 'vue') {
// TODO remove when 3.x becomes default
releaseTag = 'next'
}
// TODO use inferred release channel after official 3.0 release
// const releaseTag = semver.prerelease(version)[0] || null
step(`Publishing ${pkgName}...`)
try {
await runIfNotDry(
'yarn',
[
'publish',
'--new-version',
version,
...(releaseTag ? ['--tag', releaseTag] : []),
'--access',
'public'
],
{
cwd: pkgRoot,
stdio: 'pipe'
}
)
console.log(chalk.green(`Successfully published ${pkgName}@${version}`))
} catch (e) {
if (e.stderr.match(/previously published/)) {
console.log(chalk.red(`Skipping already published: ${pkgName}`))
} else {
throw e
}
}
}
执行结果部分代码
3.7、推送到 github
step('\nPushing to GitHub...')
await runIfNotDry('git', ['tag', `v${targetVersion}`])
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
await runIfNotDry('git', ['push'])
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`)
}
if (skippedPackages.length) {
console.log(
chalk.yellow(
`The following packages are skipped and NOT published:\n- ${skippedPackages.join(
'\n- '
)}`
)
)
}
console.log()
yarn release --dry后,在终端输出的如下
4、发布流程
- 确认要发布的版本
- 执行测试用例
- 更新所有包的版本号和内部 vue 相关依赖版本号
3.1 updatePackage 更新包的版本号
3.2 updateDeps 更新内部 vue 相关依赖的版本号 - 打包编译所有包
- 生成 changelog
- 提交代码
- 发布包
- 推送到 github
5、总结
通过本文内容学到了以下内容
- 熟悉 vuejs 发布流程
- 学会调试 nodejs 代码
- 动手优化公司项目发布流程
Vue 3.2的发布是一个结构化和自动化的过程,旨在确保代码质量、文档准确性和社区通知的及时性。这个过程通常涉及几个关键步骤,包括使用Git进行版本控制、运行自动化测试以确保代码稳定性、打包代码以准备生产部署,以及更新官方文档和变更日志来反映新版本的功能和更改。
在这个过程中,vue-release或类似的工具扮演了重要角色。这些工具帮助自动化发布流程中的多个步骤,如推送代码到NPM、生成版本标签、更新变更日志以及可能的社交媒体通知等。通过使用这些工具,Vue团队能够更高效地管理发布流程,同时减少人为错误。
总的来说,学习Vue 3.2的发布流程和vue-release工具,不仅让我们了解了Vue项目的发布实践,还为我们自己的项目提供了宝贵的自动化和流程优化经验。