1. 前言

大家好,我是若川。欢送关注我的公众号:若川视线,最近组织了源码共读流动,感兴趣的能够加我微信 ruochuan12,长期交流学习。

之前写的《学习源码整体架构系列》 蕴含jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4十篇源码文章。

写绝对很难的源码,消耗了本人的工夫和精力,也没播种多少浏览点赞,其实是一件挺受打击的事件。从浏览量和读者受害方面来看,不能促成作者继续输入文章。

所以转变思路,写一些绝对通俗易懂的文章。其实源码也不是设想的那么难,至多有很多看得懂

最近尤雨溪公布了3.2版本。小版本曾经是3.2.4了。本文来学习下尤大是怎么公布vuejs的,学习源码为本人所用。

本文波及到的 vue-next/scripts/release.js文件,整个文件代码行数尽管只有 200 余行,但十分值得咱们学习。

歌德曾说:读一本好书,就是在和崇高的人谈话。 同理可得:读源码,也算是和作者的一种学习交换的形式。

浏览本文,你将学到:

1. 相熟 vuejs 公布流程2. 学会调试 nodejs 代码3. 入手优化公司我的项目公布流程

环境筹备之前,咱们先预览下vuejs的公布流程。

2. 环境筹备

关上 vue-next,
开源我的项目个别都能在 README.md 或者 .github/contributing.md 找到奉献指南。

而奉献指南写了很多对于参加我的项目开发的信息。比方怎么跑起来,我的项目目录构造是怎么的。怎么投入开发,须要哪些常识储备等。

你须要确保 Node.js 版本是 10+, 而且 yarn 的版本是 1.x Yarn 1.x。

你装置的 Node.js 版本很可能是低于 10。最简略的方法就是去官网重新安装。也能够应用 nvm等治理Node.js版本。

node -v# v14.16.0# 全局装置 yarn# 克隆我的项目git clone https://github.com/vuejs/vue-next.gitcd vue-next# 或者克隆我的我的项目git clone https://github.com/lxchuan12/vue-next-analysis.gitcd vue-next-analysis/vue-next# 装置 yarnnpm install --global yarn# 装置依赖yarn # install the dependencies of the project# yarn release

2.1 严格校验应用 yarn 装置依赖

接着咱们来看下 vue-next/package.json 文件。

// vue-next/package.json{    "private": true,    "version": "3.2.4",    "workspaces": [        "packages/*"    ],    "scripts": {        // --dry 参数是我加的,如果你是调试 代码也倡议加        // 不执行测试和编译 、不执行 推送git等操作        // 也就是说空跑,只是打印,后文再具体讲述        "release": "node scripts/release.js --dry",        "preinstall": "node ./scripts/checkYarn.js",    }}

如果你尝试应用 npm 装置依赖,应该是会报错的。为啥会报错呢。
因为 package.json 有个前置 preinstall node ./scripts/checkYarn.js 判断强制要求是应用yarn装置。

scripts/checkYarn.js文件如下,也就是在process.env环境变量中找执行门路npm_execpath,如果不是yarn就输入正告,且过程完结。

// scripts/checkYarn.jsif (!/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)}

如果你想疏忽这个前置的钩子判断,能够应用yarn --ignore-scripts 命令。也有后置的钩子post。更多具体的能够查看 npm 文档

2.2 调试 vue-next/scripts/release.js 文件

接着咱们来学习如何调试 vue-next/scripts/release.js文件。

这里申明下我的 VSCode 版本 是 1.59.0 应该 1.50.0 起就能够按以下步骤调试了。

code -v# 1.59.0

找到 vue-next/package.json 文件关上,而后在 scripts 上方,会有debug(调试)按钮,点击后,抉择 release。即可进入调试模式。

这时终端会如下图所示,有 Debugger attached. 输入。这时放张图。

更多 nodejs 调试相干 能够查看官网文档

学会调试后,先大抵走一遍流程,在要害中央多打上几个断点多走几遍,就能猜测到源码用意了。

3 文件结尾的一些依赖引入和函数申明

咱们能够跟着断点来,先看文件结尾的一些依赖引入和函数申明

3.1 第一局部

// vue-next/scripts/release.jsconst args = require('minimist')(process.argv.slice(2))// 文件模块const fs = require('fs')// 门路const path = require('path')// 控制台const chalk = require('chalk')const semver = require('semver')const currentVersion = require('../package.json').versionconst { prompt } = require('enquirer')// 执行子过程命令   简略说 就是在终端命令行执行 命令const execa = require('execa')

通过依赖,咱们能够在 node_modules 找到对应装置的依赖。也能够找到其READMEgithub仓库。

3.1.1 minimist 命令行参数解析

minimist

简略说,这个库,就是解析命令行参数的。看例子,咱们比拟容易看懂传参和解析后果。

$ node example/parse.js -a beep -b boop{ _: [], a: 'beep', b: 'boop' }$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz{ _: [ 'foo', 'bar', 'baz' ],  x: 3,  y: 4,  n: 5,  a: true,  b: true,  c: true,  beep: 'boop' }
const args = require('minimist')(process.argv.slice(2))

其中process.argv的第一和第二个元素是Node可执行文件和被执行JavaScript文件的齐全限定的文件系统门路,无论你是否这样输出他们。

3.1.2 chalk 终端多色调输入

chalk

简略说,这个是用于终端显示多色调输入。

3.1.3 semver 语义化版本

semver

语义化版本的nodejs实现,用于版本校验比拟等。对于语义化版本能够看这个语义化版本 2.0.0 文档

版本格局:主版本号.次版本号.订正号,版本号递增规定如下:

主版本号:当你做了不兼容的 API 批改,

次版本号:当你做了向下兼容的功能性新增,

订正号:当你做了向下兼容的问题修改。

后行版本号及版本编译信息能够加到“主版本号.次版本号.订正号”的前面,作为延长。

3.1.4 enquirer 交互式询问 CLI

简略说就是交互式询问用户输出。

enquirer

3.1.5 execa 执行命令

简略说就是执行命令的,相似咱们本人在终端输出命令,比方 echo 若川

execa

// 例子const execa = require('execa');(async () => {  const {stdout} = await execa('echo', ['unicorns']);  console.log(stdout);  //=> 'unicorns'})();

看完了第一局部,接着咱们来看第二局部。

3.2 第二局部

// vue-next/scripts/release.js// 对应 yarn run release --preid=beta// betaconst preId =  args.preid ||  (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])// 对应 yarn run release --dry// trueconst isDryRun = args.dry// 对应 yarn run release --skipTests// true 跳过测试const skipTests = args.skipTests// 对应 yarn run release --skipBuild // trueconst skipBuild = args.skipBuild// 读取 packages 文件夹,过滤掉 不是 .ts文件 结尾 并且不是 . 结尾的文件夹const packages = fs  .readdirSync(path.resolve(__dirname, '../packages'))  .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))

第二局部绝对简略,持续看第三局部。

3.3 第三局部

// vue-next/scripts/release.js// 跳过的包const skippedPackages = []// 版本递增const versionIncrements = [  'patch',  'minor',  'major',  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])]const inc = i => semver.inc(currentVersion, i, preId)

这一块可能不是很好了解。inc是生成一个版本。更多能够查看semver文档

semver.inc('3.2.4', 'prerelease', 'beta')// 3.2.5-beta.0

3.4 第四局部

第四局部申明了一些执行脚本函数等

// vue-next/scripts/release.js// 获取 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))

3.4.1 bin 函数

获取 node_modules/.bin/ 目录下的命令,整个文件就用了一次。

bin('jest')

相当于在命令终端,我的项目根目录 运行 ./node_modules/.bin/jest 命令。

3.4.2 run、dryRun、runIfNotDry

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

run 实在在终端跑命令,比方 yarn build --release

dryRun 则是不跑,只是 console.log(); 打印 'yarn build --release'

runIfNotDry 如果不是空跑就执行命令。isDryRun 参数是通过控制台输出的。yarn run release --dry这样就是truerunIfNotDry就是只是打印,不执行命令。这样设计的益处在于,能够有时不想间接提交,要先看看执行命令的后果。不得不说,尤大就是会玩。

main 函数开端,也能够看到相似的提醒。能够用git diff先看看文件批改。

if (isDryRun) {  console.log(`\nDry run finished - run git diff to see package changes.`)}

看完了文件结尾的一些依赖引入和函数申明等,咱们接着来看main主入口函数。

4 main 主流程

第4节,次要都是main 函数拆解剖析。

4.1 流程梳理 main 函数

const chalk = require('chalk')const step = msg => console.log(chalk.cyan(msg))// 后面一堆依赖引入和函数定义等async function main(){  // 版本校验  // run tests before release  step('\nRunning tests...')  // update all package versions and inter-dependencies  step('\nUpdating cross dependencies...')  // build all packages with types  step('\nBuilding all packages...')  // generate changelog  step('\nCommitting changes...')  // publish packages  step('\nPublishing packages...')  // push to GitHub  step('\nPushing to GitHub...')}main().catch(err => {  console.error(err)})

下面的main函数省略了很多具体函数实现。接下来咱们拆解 main 函数。

4.2 确认要公布的版本

第一段代码尽管比拟长,然而还好了解。
次要就是确认要公布的版本。

调试时,咱们看下这段的两张截图,就好了解啦。

// 依据上文 mini 这句代码意思是 yarn run release 3.2.4 // 取到参数 3.2.4let 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}`)}// 确认要 releaseconst { yes } = await prompt({  type: 'confirm',  name: 'yes',  message: `Releasing v${targetVersion}. Confirm?`})// false 间接返回if (!yes) {  return}

4.3 执行测试用例

// run tests before releasestep('\nRunning tests...')if (!skipTests && !isDryRun) {  await run(bin('jest'), ['--clearCache'])  await run('yarn', ['test', '--bail'])} else {  console.log(`(skipped)`)}

4.4 更新所有包的版本号和外部 vue 相干依赖版本号

这一部分,就是更新根目录下package.json 的版本号和所有 packages 的版本号。

// update all package versions and inter-dependenciesstep('\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))}

4.4.1 updatePackage 更新包的版本号

function updatePackage(pkgRoot, version) {  const pkgPath = path.resolve(pkgRoot, 'package.json')  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))  pkg.version = version  updateDeps(pkg, 'dependencies', version)  updateDeps(pkg, 'peerDependencies', version)  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')}

次要就是三种批改。

1. 本人自身 package.json 的版本号2. packages.json 中 dependencies 中 vue 相干的依赖批改3. packages.json 中 peerDependencies 中 vue 相干的依赖批改

一图胜千言。咱们执行yarn release --drygit diff 查看的 git 批改,局部截图如下。

4.4.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。会看到这样是输入。

也就是这句代码输入的。

console.log(  chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`))

4.5 打包编译所有包

// build all packages with typesstep('\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)`)}

4.6 生成 changelog

// generate changelogawait run(`yarn`, ['changelog'])

yarn changelog 对应的脚本是conventional-changelog -p angular -i CHANGELOG.md -s

4.7 提交代码

通过更新版本号后,有文件改变,于是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.')}

4.8 公布包

// publish packagesstep('\nPublishing packages...')for (const pkg of packages) {  await publishPackage(pkg, targetVersion, runIfNotDry)}

这段函数比拟长,能够不必细看,简略说就是 yarn publish 公布包。
咱们 yarn release --dry后,这块函数在终端输入的如下:

值得一提的是,如果是 vue 默认有个 tagnext。当 Vue 3.x 是默认时删除。

} else if (pkgName === 'vue') {  // TODO remove when 3.x becomes default  releaseTag = 'next'}

也就是为什么咱们当初装置 vue3 还是 npm i vue@next命令。

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    }  }}

4.9 推送到 github

// push to GitHubstep('\nPushing to GitHub...')// 打 tagawait runIfNotDry('git', ['tag', `v${targetVersion}`])// 推送 tagawait runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])// git push 所有改变到 近程  - githubawait runIfNotDry('git', ['push'])
// yarn run release --dry// 如果传了这个参数则输入 能够用 git diff 看看更改// const isDryRun = args.dryif (isDryRun) {  console.log(`\nDry run finished - run git diff to see package changes.`)}// 如果 跳过的包,则输入以下这些包没有公布。不过代码 `skippedPackages` 里是没有包。// 所以这段代码也不会执行。// 咱们习惯写 arr.length !== 0 其实 0 就是 false 。能够不写。if (skippedPackages.length) {  console.log(    chalk.yellow(      `The following packages are skipped and NOT published:\n- ${skippedPackages.join(        '\n- '      )}`    )  )}console.log()

咱们 yarn release --dry后,这块函数在终端输入的如下:

到这里咱们就拆解剖析完 main 函数了。

整个流程很清晰。

1. 确认要公布的版本2. 执行测试用例3. 更新所有包的版本号和外部 vue 相干依赖版本号    3.1 updatePackage 更新包的版本号    3.2 updateDeps 更新外部 vue 相干依赖的版本号4. 打包编译所有包5. 生成 changelog6. 提交代码7. 公布包8. 推送到 github

用一张图总结则是:

看完vue-next/scripts/release.js,感兴趣还能够看vue-next/scripts文件夹下其余代码,绝对行数不多,但收益较大。

5. 总结

通过本文学习,咱们学会了这些。

1. 相熟 vuejs 公布流程2. 学会调试 nodejs 代码3. 入手优化公司我的项目公布流程

同时倡议本人入手用 VSCode 多调试,在终端多执行几次,多理解消化。

vuejs公布的文件很多代码咱们能够间接复制粘贴批改,优化咱们本人公布的流程。比方写小程序,绝对可能公布频繁,齐全能够应用这套代码,配合miniprogram-ci,再加上一些自定义,加以优化。

当然也能够用开源的 release-it。

同时,咱们能够:

引入 git flow,治理git分支。预计很多人不晓得windows git bash曾经默认反对 git flow命令。

引入 husky 和 lint-staged 提交commit时用ESLint等校验代码提交是否可能通过检测。

引入 单元测试 jest,测试要害的工具函数等。

引入 conventional-changelog

引入 git-cz 交互式git commit

等等标准本人我的项目的流程。如果一个候选人,通过看vuejs公布的源码,积极主动优化本人我的项目。我感觉面试官会认为这个候选人比拟加分。

看开源我的项目源码的益处在于:一方面能够拓展视线,另外一方面能够为本人所用,收益绝对较高。