1 学习目标
在这里,学习尤大大是怎么发布vuejs的,通过阅读releaseJs,将获得什么?
- 学习 vue 发布流程
- 学习调试 node 代码
- 学以致用
2 使用 npm 安装依赖
安装依赖前,先简单了解一下 npm install 的原理
/*
简略介绍一下npm install的实现原理
1 当我们进行npm install的时候,如果定义了preinstall,会先执行此钩子。
2 确定依赖模块
2.1 确定devDependencies中指定的模块
2.2 工程相当于是树的根节点,每个首层依赖模块就是根节点下面的一个子节点,npm开启多进程从子节点开始遍历子节点下的所有节点。
3 获取模块
3.1 下载模块之前,先判断版本,如果存在描述文件package-lock.json,直接从此文件中获取即可。如果没有则从仓库中获取,比如package.json中定义某个包是^1.1.1,那么npm会直接在仓库中找1.x.x的最新版本
3.2 下载模块,在上一步中获取到模块的压缩包地址,也就是package-lock.json中的resolved字段,npm会先检查本地npm缓存是否存在,如果存在直接取,如果没有才从仓库下载
3.3 查找模块的依赖,如果有依赖则回到3.1开始执行,没有则停止
4 模块dedupe
4.1 在上一步获取到的是一个完整的依赖,但是有可能会有大量重复的模块,比如同时有两个模块都依赖了loadsh,这时候,会有一个扁平化的处理过程,它会遍历所有节点,将模块放到node_modules中,并将重复模块丢弃
4.2 重复模块:指的是名称一样并且semver兼容(语义化版本,npm使用此工具处理版本相关的工作,比如比较两个版本大小lt、gt),如果重复模块的版本存在兼容版本,那么就会得到一个兼容版本,如果不兼容,就会保留下来两个版本,这样就可以多余模块就会被去掉。
5 安装模块,更新工程中的node_modules,并执行模块中的生命周期函数(preinstall、install、postinstall)
6 生成package-lock.json描述文件
*/
{
"script": {
"preinstall": "node ./scripts/checkYarn.js"
}
}
执行 npm install 后,发现有报错的,因为有一个安装前的钩子函数 preinstall node ./scripts/checkYarn.js ,检查是否使用 yarn 安装,所以只能使用 yarn 安装依赖。更多关于npm script钩子的详细介绍。
/*
当我们执行npm i和yarn,
process.env 环境变量 npm_execpath(执行路径)
npm i -- /usr/local/lib/node_modules/npm/bin/npm-cli.js
yarn -- /usr/local/lib/node_modules/yarn/bin/yarn.js
*/
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)
}
3 文件依赖库介绍
// 获取命令行参数
const args = require('minimist')(process.argv.slice(2))
// 文件模块
const fs = require('fs')
// 路径模块
const path = require('path')
// 控制台样式
const chalk = require('chalk')
// 语义化控制模块
const semver = require('semver')
// 获取package.js version
const currentVersion = require('../package.json').version
// 命令行交互式库
const { prompt } = require('enquirer')
// 执行命令库
const execa = require('execa')
3.1 minimist 命令行参数解析库( minimist地址 )
先了解一下 minimist 的使用
const args = require('minimist')(process.argv.slice(2))
/*
获取参数
process.argv: 返回启动node进程时传入的命令行参数的集合数组。
process.argv[0]: 启动进程文件路径 '/usr/local/bin/node'
process.argv[1]: 正在执行的js文件路径
...: 任何其他命令行参数,以空格分割
require('minimist'): 命令行参数解析工具。接收两个参数
1.参数数组
2.opt对象
opt.string: ['x'] 将参数名为x的值转为字符串
opt.alias: {x: 'c'} 将字符串参数名称映射到别名,同时保留原参数名
opt.default: {a: '6'} 添加默认的参数
opt.boolean 默认为false 为true 会将 '--hello' ==> hello: true
opt.-- 默认为false 为true 会将 '--' 后面的值放到 ==> '--': ['f']
例: require('minimist')(['-x', '3', '-y', '4', '-z=5', '-ghj', 'ccc', '--hello', 'bbb', 'dd', '--', 'f'])
{
_: [ 'dd', 'f'],
x: '3',
c: '3',
y: 4,
z: 5,
g: true,
h: true,
j: 'ccc',
hello: 'bbb',
a: '6'
}
*/
3.2 chalk 终端控制台字符串输出美化库( chalk地址 )
语法:
console.log(chalk.red.bold.bgWhite('Hello World'))
相等
console.log(chalk.rgb(255,0,0).bold.bgRgb(255,255,255)('Hello World'))
Hello World 加粗,字体颜色是红色,背景颜色是白色
支持模板使用
console.log(chalk`{red.bold.bgWhite Hello World}`)
相等
console.log(chalk`{rgb(255,0,0).bold.bgRgb(255,255,255) Hello World}`)
3.3 semver 语义化版本控制库( semver地址 )
语义化版本控制模块
版本号的基本规则
版本号一般有三个部分,以.隔开,就像X.Y.Z,其中
X:主版本号,不兼容的大改动
Y:次版本号,功能性的改动
Z:修订版本号,问题修复
package ~和^的区别
~: 安装x.y.z中z的最新版本,也就是只更新修订版本号
^: 安装x.y.z中y:z的最新版本,也就是不更新主版本号
语法:
valid(v): 返回解析后的版本,如果无效返回null
prerelease(v): 返回预发布组件的数组,如果不存在,则返回null。
例:prerelease('1.2.3-alpha.1') -> ['alpha', 1]
clean(version): 如果可能,将字符串清理为有效的 semver
inc(v, release, type): 返回由发布类型递增的版本,也就是生成一个版本如果无效则返回null。
例:inc('1.2.3', 'prerelease', 'beta') -> '1.2.4-beta.0'
3.4 enquirer 命令行交互式库( enquirer地址 )
参数介绍
type:表示提问的类型,包括:input, confirm, list, rawlist, expand, checkbox, password, editor
name: 存储当前问题回答的变量
message:问题的描述
default:默认值;
choices:列表选项,在某些type下可用,并且包含一个分隔符(separator);
validate:对用户的回答进行校验;
filter:对用户的回答进行过滤处理,返回处理后的值;
transformer:对用户回答的显示效果进行处理(如:修改回答的字体或背景颜色),但不会影响最终的答案的内容;
when:根据前面问题的回答,判断当前问题是否需要被回答;
pageSize:修改某些type类型下的渲染行数;
prefix:修改message默认前缀;
suffix:修改message默认后缀。
语法
基本使用
const { prompt } = require('enquirer')
prompt(参数数组).then(answer => { console.log(answer) })
另一种使用方式
const { Input } = require('enquirer');
const prompt = new Input({
message: 'What is your username?',
initial: 'jonschlinkert'
});
prompt.run()
.then(answer => console.log('Answer:', answer))
.catch(console.log);
3.5 execa 命令行交互式库( execa地址 )
execa(file, arguments?, options?)
第一个参数是一个字符串,运行脚本的命令
第二个参数是一个参数数组,给这个脚本传入的参数
第二个参数是一个参数对象 cwd: 子进程的工作目录、stdio: 标准输入等等
例1: const { stdout } = await execa('echo', ['unicorns']);
console.log(stdout) ==> 'unicorns'
例2: const { stdout } = await execa('ls', {cwd: path.resolve(__dirname)}); // 打印目录列表
4 声明变量介绍
/*
用于定义beta版本
yarn run release --preid=beta
preId = beta
*/
const preId =
args.preid ||
(semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
/*
空跑,只是输出结果
yarn run release --dry
dry = true
*/
const isDryRun = args.dry
/*
跳过测试
yarn run release --skipTests
skipTests = true
*/
const skipTests = args.skipTests
/*
跳过打包
yarn run release --skipBuild
skipBuild = true
*/
const skipBuild = args.skipBuild
// 跳过包,填写之后,会跳过此包的发布
const skippedPackages = []
/*
版本递增 patch 补丁 minor 次要的 major主要的
preId存在的话,增加beta版选择
*/
const versionIncrements = [
'patch',
'minor',
'major',
...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
/*
5 函数介绍
/*
semver.inc('1.2.3', 'prerelease', 'beta')
'1.2.4-beta.0'
*/
const inc = i => semver.inc(currentVersion, i, preId)
// 获取执行文件路径,全文只有一处使用了bin('jest')
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 真正执行的终端命令函数 yarn build --release
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)
/*
如: yarn run release --dry
如果dry存在,执行空跑,打印信息,否则真正跑终端命令
存在的意义:有时候需要看命令结果,但是又不是真正的提交发布
*/
const runIfNotDry = isDryRun ? dryRun : run
// 获取packages目录-pkg目录路径的函数
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 控制台输出信息函数
const step = msg => console.log(chalk.cyan(msg))
6 main主流程介绍
6.1 主流程概括
async function main() {
1.明确要发布的版本
2.发布之前先执行测试
// run tests before release
step('\nRunning tests...')
3.更新全部包的版本(包括packages目录下的文件)和dependencies版本
// update all package versions and inter-dependencies
step('\nUpdating cross dependencies...')
4.打包编译全部包
// build all packages with types
step('\nBuilding all packages...')
5.生成changelog日志文件
// generate changelog
6.提交代码,但未推送到GitHub
step('\nCommitting changes...')
7.发布包
// publish packages
step('\nPublishing packages...')
8.推送代码到GitHub
// push to GitHub
step('\nPushing to GitHub...')
}
6.2 明确要发布的版本
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?`
})
// 如果选择false,返回
if (!yes) {
return
}
6.3 发布之前先执行测试
// run tests before release
step('\nRunning tests...')
// 不是跳过测试 并且 不是空跑
if (!skipTests && !isDryRun) {
// 清除jest 缓存
await run(bin('jest'), ['--clearCache'])
// 运行测试用例
await run('yarn', ['test', '--bail'])
} else {
console.log(`(skipped)`)
}
6.4 更新全部包的版本(包括packages目录下的文件)和dependencies版本
// update all package versions and inter-dependencies
step('\nUpdating cross dependencies...')
// 更新本身的package.json版本号和所有package目录下的版本号
updateVersions(targetVersion)
function updateVersions(version) {
// 1. update root package.json
// 更新package.json中的dependencies和peerDependencies的vue版本
updatePackage(path.resolve(__dirname, '..'), version)
// 2. update all packages
// 更新packages目录下的所有符合条件目录的package.json的vue版本
packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
function updatePackage(pkgRoot, version) {
// 获取package文件路径
const pkgPath = path.resolve(pkgRoot, 'package.json')
// 读取package文件
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
pkg.version = version
// 更新package dependencies 目录的vue版本
updateDeps(pkg, 'dependencies', version)
// 更新package peerDependencies 目录的vue版本
updateDeps(pkg, 'peerDependencies', version)
// 更新后的package的重新写入。JSON.stringify 2 文本在每个级别缩进两个空格
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
function updateDeps(pkg, depType, version) {
const deps = pkg[depType]
if (!deps) return
// 遍历package deps对象下,key为vue 或者 key是以@vue开头并且需要匹配packages数组下和替换掉@vue/开头的字段严格匹配
// 问题: 为什么要判断packages目录下是否存在@vue
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
}
})
}
执行后,可以看到控制台输出信息
6.5 打包编译全部包
// 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)`)
}
6.6 生成changelog日志文件
// generate changelog
/*
conventional-changelog-cli 生成日志库
命令:conventional-changelog -p angular -i CHANGELOG.md -s
生成CHANGELOG.md更新日志
*/
await run(`yarn`, ['changelog'])
6.7 提交代码,但未推送到GitHub
// git diff 查看git的修改
const { stdout } = await run('git', ['diff'], {
stdio: 'pipe'
})
// 如果git对比有差异,git add\git commit 添加变更进去本地仓库。
if (stdout) {
step('\nCommitting changes...')
// git add -A
await runIfNotDry('git', ['add', '-A'])
// git commit -m 'release: v${targetVersion}'
await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
} else {
console.log('No changes to commit.')
}
git diff 查看一下更改的内容
6.8 发布包
// publish packages
step('\nPublishing packages...')
// 发布包
for (const pkg of packages) {
await publishPackage(pkg, targetVersion, runIfNotDry)
}
async function publishPackage(pkgName, version, runIfNotDry) {
// skippedPackages现在为空,跳过的包
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
// 当vue为 3.x时,删除名为next的tag。我们安装vue3时,npm i vue@next
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
}
}
}
6.9 推送代码到GitHub
// push to GitHub
step('\nPushing to GitHub...')
// 打tag
await runIfNotDry('git', ['tag', `v${targetVersion}`])
// 推送tag到远程 git push origin refs/tags/vx.x.x
await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
// 推送更改到远程
await runIfNotDry('git', ['push'])
// yarn release --dry
if (isDryRun) {
console.log(`\nDry run finished - run git diff to see package changes.`)
}
// 如果skippedPackages存在,则表示此包没有发布。
if (skippedPackages.length) {
console.log(
chalk.yellow(
`The following packages are skipped and NOT published:\n- ${skippedPackages.join(
'\n- '
)}`
)
)
}
console.log()
7 总结
现有项目基于 gitlab 的 webhook 实现自动发布程序,对比 vue 的发布流程,拓展了视野
熟悉 vue 的发布流程
了解一些 node 库