vue3_release流程源码保姆级解读

831 阅读6分钟

前言

保姆级对于vue3的版本发布流程解读。 详细解答都在每一行代码的注释里面,主要看注释。 学完后可以搭建自己的release流程了(再也不是手动挡了)。 文档较长,建议分次阅读并自己建一个走一遍。

准备

保姆级文档 vue3_release学习仓库代码 vue3_release源码

  1. 准备node-10+环境以及yarn1.x
  2. 克隆vue3_release学习仓库代码项目
  3. cd vue3_release
  4. yarn 安装依赖
  5. yarn release就可以控制台体验999等级的输出了

先上效果图: 7D68C316-EA4F-4670-BE65-9EED59F40A4F.png

阅读

整个文件分为两个部分阅读:

  1. 依赖以及辅助函数
  2. main进程

依赖以及辅助函数

args(获取命令行参数)

const args = require('minimist')(process.argv.slice(2))
// 命令行参数
// 例子:
// process.argv:
// 第一个是node路径
// 第二是执行脚步路径
// 后面是参数,所以去除两个路径
// node release.js -name zhou
// [
//   3:'zhou'
//   2:'-name'
//   1:'/Users/zhouguang/Desktop/learn/vue/源码阅读/release/release.js'
//   0:'/usr/local/bin/node'
// ]

// args: minimist转换后的参数对象
// { _: [], name: 'zhou' }

依赖包

const fs = require('fs')
// 文件操作,用于读取

const path = require('path')
// 路径解析

const chalk = require('chalk')
// 彩色终端

const semver = require('semver')
// 版本号生成和对比

const currentVersion = require('../package.json').version
// 现在的版本号

const { prompt } = require('enquirer')
// 命令行选择交互

const execa = require('execa');
// 子进程,在终端执行命令
// 例子:
// (async () => {
//   const {stdout} = await execa('echo', ['zhou'])
//   console.log(stdout) // zhou
// })();

preId(版本后缀类型)

const preId = args.preid || (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])
// 获取preid
// 命令行参数的发布版本的后缀,或者是当前版本的后缀1.0.0-alpha.x => alpha

isDryRun(是否只log不执行,简称空运行)

const isDryRun = args.dry
// 命令行参数控制是否空运行,打开方式:node release.js --dry
// 本项目package.json中release命令默认携带

skipTests(跳过测试)

const skipTests = args.skipTests
// 跳过测试,打开方式:node release.js --skipTests

skipBuild(跳过构建)

const skipBuild = args.skipBuild
// 跳过构建,打开方式:node release.js --skipBuild

packages(子包名称数组集合)

获取在scripts同层packages文件夹下子包名称的数组集合

const packages = fs
  .readdirSync(path.resolve(__dirname, '../packages'))
  .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
// 获取package下非ts结尾并且不是.开头的文件或者文件夹名称

skippedPackages(需要跳过的包)

const skippedPackages = []
// 跳过的包

versionIncrements(版本升级类型数组)

const versionIncrements = [
  'patch',
  'minor',
  'major',
  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
// 版本号类型数组,有preId则把后缀类型加入

inc(生成版本号函数)

const inc = i => semver.inc(currentVersion, i, preId)
// 生成版本号函数
// 例子
// semver.inc('1.0.0', 'patch', 'beta') => 1.0.1
// semver.inc('1.0.0', 'minor', 'beta') => 1.1.0
// semver.inc('1.0.0', 'major', 'beta') => 2.0.0
// pre就是增加后缀
// semver.inc('1.0.0', 'major', 'beta') => 2.0.0-beta.0
// prerelease增加后缀的版本号
// semver.inc('1.0.0-beta.0', 'prerelease', 'beta') => 1.0.0-beta.1

bin(生成命令执行函数)

const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 生成命令执行函数
// 利用node_modules下的bin,相当于bin(webapck) => node_modules/.bin/webpack

run(终端执行命令运行函数)

const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })
// 终端执行命令运行函数,对execa进行二次封装

dryRun(空运行函数)

const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 空运行函数,使用chalk进行打印要执行的命令

runIfNotDry(执行判断函数)

const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
// 执行判断函数,利用外部isDryRun变量判断要进行哪个执行

getPkgRoot(获取单个包的路径函数)

const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 获取单个包的路径函数

step(终端console)

const step = msg => console.log(chalk.cyan(msg))
// 终端彩色console,默认蓝色哦

main进程

直接跳到main函数进行阅读即可,main相关操作函数放置在main前面(个人习惯,哈哈),vue源码是放在main后面。

1. 生成版本号

取出命令行内的版本号

  let targetVersion = args._[0]
  // 取出命令行内的版本号,例如node release 3.1.2 
  // args._一个包含未指定参数内容组成的数组 

交互生成版本号

if (!targetVersion) {
  // 未指定版本号

    const { release } = await prompt({
      type: 'select',
      name: 'release',
      message: 'Select release type',
      choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
    })
    // 命令交互选择版本类型 例子:minor (1.0.1)
    
    if (release === 'custom') {
      // 自定义输入版本号
      targetVersion = (
        await prompt({
          type: 'input',
          name: 'version',
          message: 'Input custom version',
          initial: currentVersion
        })
      ).version
    } else {
      targetVersion = release.match(/\((.*)\)/)[1]
      // 取出release中版本号 例子:minor (1.0.1) => 1.0.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
  }

2. jest执行测试

  step('\nRunning tests...')
  // 终端输出正在运行测试

  if (!skipTests && !isDryRun) {
    // 非跳过测试以及非空运行才运行测试
    await run(bin('jest'), ['--clearCache'])
    await run('yarn', ['test', '--bail'])
  } else {
    console.log(`(skipped)`)
  }

3. 更新vue以及所有相关包的版本

  step('\nUpdating cross dependencies...')
  // 终端输出更新所有包版本中

  updateVersions(targetVersion)
  // 执行更新函数

updateVersions(更新相关包版本)

当前项目中packages中建立了package1和2两个示例, 它们的版本号会一起变更

// 更新版本号
function updateVersions (version) {
  // 1. 更新根目录package.json
  updatePackage(path.resolve(__dirname, '..'), version)
  // 2. 更新packages下所有package.json
  packages.forEach(p => updatePackage(getPkgRoot(p), version))
}

updatePackage(更新单个包)

作用是更新三个地方的version

  • package.json的version
  • dependencies内和vue相关包的版本号
  • peerDependencies内和vue相关包的版本号
// 更新package
function updatePackage(pkgRoot, version) {

  // 获取路径下的package.json
  const pkgPath = path.resolve(pkgRoot, 'package.json')

  // 读取package.json并转换json对象
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))

  // 更新version
  pkg.version = version

  // 更新dependencies内和vue相关包的版本号
  // dependencies:作为依赖安装的 npm 软件包的列表
  updateDeps(pkg, 'dependencies', version)

  // 更新peerDependencies内和vue相关包的版本号
  // peerDependencies:[对等依赖](https://nodejs.org/en/blog/npm/peer-dependencies/),当前包使用必须的其它依赖包
  updateDeps(pkg, 'peerDependencies', version)

  // 新数据写入package.json
  // JSON.stringify(json对象, 可选:序列化格式函数, 可选:缩进空白字符个数)
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}

updateDeps(更新查找到的vue相关包的版本号)

function updateDeps(pkg, depType, version) {
  // 获取依赖数组
  const deps = pkg[depType]
  // 没有则返回
  if (!deps) return
  // 循环对比依赖是否符合以下两个条件,符合则更新版本
  // 1。=== vue
  // 2.@vue开头,并且剔除@vue/后的内容在packages内
  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
    }
  })
}

chalk打印出所有更新相关包的对应依赖的版本

4400F68A-A902-489A-B019-754B75126BF1.png

4. 构建所有包

  step('\nBuilding all packages...')
  // 终端输出构建所有包中

  if (!skipBuild && !isDryRun) {
    // 非跳过测试以及非空运行才运行构建

    await run('yarn', ['build', '--release'])
    // 运行构建

    step('\nVerifying type declarations...')
    // 终端输出校验TS类型声明
    await run('yarn', ['test-dts-only'])
    // 执行校验TS类型声明
  } else {
    console.log(`(skipped)`)
  }

5. 生成changelog说明md文件

借助conventional-changelog-cli将git commit history中符合angular规范的整合输出changelog.md文件

  // 执行conventional-changelog -p angular -i CHANGELOG.md -s
  // 依赖包:conventional-changelog-cli
  // 按照angular规范生成changelog文件,即版本下列出fix,feat,revert的更新
  await run(`yarn`, ['changelog'])

6. git提交commit

  const { stdout } = await run('git', ['diff'], { stdio: 'pipe' })
  // 命令行执行git diff,检查是否有更改

  if (stdout) {
    // 如果有改动

    step('\nCommitting changes...')
    // 终端输出git commit中

    await runIfNotDry('git', ['add', '-A'])
    // 提交到git暂存区

    await runIfNotDry('git', ['commit', '-m', `release: v${targetVersion}`])
    // git commit release: v1.0.1,生成版本commit信息
  } else {
    console.log('No changes to commit.')
  }

7. yarn发布包

  step('\nPublishing packages...')
  // 终端输出包发布中

  // 循环packages中的包并发布
  for (const pkg of packages) {
    await publishPackage(pkg, targetVersion, runIfNotDry)
  }

publishPackage(发布包

async function publishPackage(pkgName, version, runIfNotDry) {

  // 如果包含在跳过包名单里,则跳过
  if (skippedPackages.includes(pkgName)) {
    return
  }

  // 根据包名组合成包的文件夹路径
  const pkgRoot = getPkgRoot(pkgName)

  // 包路径+package.json组成完整路径
  const pkgPath = path.resolve(pkgRoot, 'package.json')

  // fs读取包下面的package.json内容并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.
  // 上面的意思是vue3.x 发布为 next标签,对应安装vue3时是 npm i vue@next

  // 发布的标签
  let releaseTag = null
  if (args.tag) {
    // 命令行如果有,则用命令行的,例子:node release --tag alpha
    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
    // 当vue3变成默认版本时,则去除next标签,安装就变成 npm i vue
    releaseTag = 'next'
  }

  // TODO use inferred release channel after official 3.0 release
  // const releaseTag = semver.prerelease(version)[0] || null
  // 后面计划用semvers生成tag

  step(`Publishing ${pkgName}...`)
  // 终端输出 发布xxx包中

  // yarn发布包
  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
    }
  }
}

8. 打标签并推送远程仓库

  step('\nPushing to GitHub...')
  // 终端输出推送到github中

  await runIfNotDry('git', ['tag', `v${targetVersion}`])
  // git tag v1.0.0 => git打标签

  await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
  // git push origin refs/tags/v1.0.0 => git推送标签到远程仓库

  await runIfNotDry('git', ['push'])
  // git push => git提交

9. 结束log

  // 如果是空运行,则打印空运行结束,可以使用git diff查看包的变化
  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- '
        )}`
      )
    )
  }

执行main

// 执行main
main().catch((err) => console.error(err))

总结

  1. 辅助函数内包装依赖包API使用,并且语义化,非常利于写业务进程时辅助使用,用起来。栗子:
const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })
// 终端执行命令运行函数,对execa进行二次封装

const dryRun = (bin, args, opts = {}) =>
  console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)
// 空运行函数,使用chalk进行打印要执行的命令

const runIfNotDry = isDryRun ? dryRun : run
// 执行判断函数,利用外部isDryRun变量判断要进行哪个执行
  1. package.json中peerDependencies(对等依赖)了解,官方文档
  2. 终端交互式发布流程,真香。

参考

若川-尤雨溪是怎么发布 Vue.js 的?