第3期|【源码】vue.js的发布流程——Vue3 Release.js源码解读

学习目标:

1)学习 release.js 源码,输出记录文档。
2) 熟悉发布流程

准备工作:
  • Vue3 源码地址:https://github.com/vuejs/vue-next 代码拉下来以后直接yarn
  • Vue3 Release 源码解读:https://juejin.cn/post/6997943192851054606 链接
  • 确保 Node.js 版本是 10+, 而且 yarn 的版本是 1.x Yarn 1.x

贡献指南 .github/contributing.md
了解到 Vue.js 采用的是 monorepo 的方式进行代码的管理。
但是vue3用的是 Yarn Workspace
因为公司涉及到的项目都是一般项目,所以没有接触过,问了一下度娘,有几篇文章写的很清楚

Yarn Workspace
  • arn 从 1.0 版开始支持Workspace(工作区)
  • Workspace 可以统一管理有多个项目的仓库,
  • 可以在每个项目下使用独立的 package.json 管理依赖
  • 可以yarn 命令安装或者升级所有依赖等
  • 可以使多个项目共享同一个 node_modules 目录
  • 提升开发效率和降低磁盘空间占用。
  • 很多知名的开源项目也使用了 Yarn Workspace,如 vuereactjest
// vue-next/package.json (多余的代码已省略)
{
    "private": true,
    "version": "3.2.2",
    "workspaces": [
        "packages/*"
    ],
    "scripts": {
        "release": "node scripts/release.js"
    }
}
vue-next/scripts/release.js 文件代码解读
// 解析命令行参数
const args = require('minimist')(process.argv.slice(2))
// 文件模块
const fs = require('fs')
// 路径
const path = require('path')
/* 控制台  chalk 是一个可以给终端输出加上色彩的工具包,有了它,你的终端将变得五颜六色,花哨的一匹
 * GitHub 地址: https://github.com/chalk/chalk
 * npm i -D chalk
*/
const chalk = require('chalk')
/* semver 语义化版本 https://semver.org/lang/zh-CN/
 * 版本格式:主版本号.次版本号.修订号,版本号递增规则如下:
 * 主版本号:当你做了不兼容的 API 修改,
 * 次版本号:当你做了向下兼容的功能性新增,
 * 修订号:当你做了向下兼容的问题修正。
 * 先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。
 * 它的作用是用于版本校验比较。
*/
const semver = require('semver')
const currentVersion = require('../package.json').version
// 交互式询问用户输入。
const { prompt } = require('enquirer')
/* 执行子进程命令 就是在终端命令行执行命令
 * execa是可以调用shell和本地外部程序的javascript封装。
 * 会启动子进程执行。支持多操作系统,包括windows。如果父进程退出,则生成的全部子进程都被杀死
 * 在第一期用的是 childProcess去执行
 */
const execa = require('execa')


// vue-next/scripts/release.js

// 对应 yarn run release --preid=beta
// beta
const preId =
  args.preid ||
  (semver.prerelease(currentVersion) && semver.prerelease(currentVersion)[0])

// 对应 yarn run release --dry
// true
const isDryRun = args.dry
// 对应 yarn run release --skipTests
// true 跳过测试
const skipTests = args.skipTests
// 对应 yarn run release --skipBuild
// true
const skipBuild = args.skipBuild

// 读取 packages 文件夹,过滤掉 不是 .ts文件 结尾 并且不是 . 开头的文件夹
const packages = fs
  .readdirSync(path.resolve(__dirname, '../packages'))
  .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))


// vue-next/scripts/release.js

// 跳过的包
const skippedPackages = []

// 版本递增
const versionIncrements = [
  'patch',
  'minor',
  'major',
  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]
// inc是生成一个版本
// semver.inc('3.2.4', 'prerelease', 'beta')
// 3.2.5-beta.0
const inc = i => semver.inc(currentVersion, i, preId)

// vue-next/scripts/release.js

// 获取 bin 命令
// path.resolve  方法会把一个路径或路径片段的序列解析为一个绝对路径。
// 给定的路径的序列是从右往左被处理的,后面每个 path 被依次解析,直到构造完成一个绝对路径。
// 例如,给定的路径片段的序列为:/foo、/bar、baz,则调用 path.resolve('/foo', '/bar', 'baz') 会返回 /bar/baz。
// ( __dirname指的是当前文件夹, ./当前目录  ../上一层目录),
// 从后往前找,找到../ 或者./时 就去对比是否与当前重复,重复替换否则拼接

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

  bin('jest')
  相当于在命令终端,项目根目录 运行 ./node_modules/.bin/jest 命令
*/
const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)


// run 是在终端命令行执行命令
const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })

// dryRun 不执行,只是 console.log(); 打印 'yarn build --release'
const dryRun = (bin, args, opts = {}) =>
  console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts)

// runIfNotDry 如果不是空跑就执行命令。isDryRun 参数是通过控制台输入的。
// yarn run release --dry这样就是true。runIfNotDry就是只是打印,不执行命令。
// 这样设计的好处在于,可以有时不想直接提交,要先看看执行命令的结果
const runIfNotDry = isDryRun ? dryRun : run
// 获取包的路径
const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
// 控制台输出
const step = msg => console.log(chalk.cyan(msg))

// vue-next/scripts/release.js
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]
    }
  }

  // 校验 版本是否符合 规范
  // 验证版本号是否合法,返回null即不合法
  // semver.valid(‘1.2.3’) // ‘1.2.3’
  // semver.valid(‘a.b.c’) // null
  if (!semver.valid(targetVersion)) {
    throw new Error(`invalid target version: ${targetVersion}`)
  }

  // 确认要 release
  const { yes } = await prompt({
    type: 'confirm',
    name: 'yes',
    message: `Releasing v${targetVersion}. Confirm?`
  })

  // false 直接返回
  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 更新所有包的版本号和内部 vue 相关依赖版本号
  // 就是更新根目录下package.json 的版本号和所有 packages 的版本号
  step('\nUpdating cross dependencies...')
  updateVersions(targetVersion)

  // build all packages with types 打包编译所有包 主要在build.js里
  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 生成变更日志 
  // yarn changelog 对应的脚本是conventional-changelog -p angular -i CHANGELOG.md -s
  await run(`yarn`, ['changelog'])

  // 提交代码
  // 更新版本号以后,有文件改动,那就git diff, 如果有 就提交 执行 git add 和 git commit 命令
  // git add -A git commit -m 'release: v${targetVersion}'

  // ·  git add -A  提交所有变化 -A(all)
  // ·  git add -u  提交被修改(modified)和被删除(deleted)文件,不包括新文件(new)  -u(update)
  // ·  git add .  提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件
  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)
  }

  // push to GitHub 推送到GitHub
  // 打 tag:git tag ${version}
  // 推送 tag:git push origin refs/tags/${version}
  // 提交代码到远程仓库:git push
  step('\nPushing to GitHub...')
  // 打tag
  await runIfNotDry('git', ['tag', `v${targetVersion}`])
  // 推送tag
  await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
  // git push 所有改动到 远程  - github
  await runIfNotDry('git', ['push'])

  // yarn run release --dry

  // 如果传了这个参数则输出 可以用 git diff 看看更改
  if (isDryRun) {
    console.log(`\nDry run finished - run git diff to see package changes(运行git diff以查看包的更改。).`)
  }

  // 如果 跳过的包,则输出以下这些包没有发布。不过代码 `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()
}

function updateVersions(version) {
  // 1. update root package.json
  updatePackage(path.resolve(__dirname, '..'), version)
  // 2. update all packages
  packages.forEach(p => updatePackage(getPkgRoot(p), version))
}
// 更新包的版本号
// 1. 自己本身 package.json 的版本号
// 2. packages.json 中 dependencies 中 vue 相关的依赖修改
// 3. packages.json 中 peerDependencies 中 vue 相关的依赖修改
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')
}

// 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 publish 发布包
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') {
    // 如果是 vue 默认有个 tag 为 next。当 Vue 3.x 是默认时删除。
    // 也就是为什么我们现在安装 vue3 还是 npm i vue@next命令。
    // 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
    }
  }
}

main().catch(err => {
  console.error(err)
})


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


// lerna 解决的是发包流程中版本号处理,自动生成 CHANGELOG 文件,提交代码,发布包,推送到仓库这几个问题,
// 它把这几个流程封装成命令供用户使用。它不是搭建组件库非必要引入的工具,虽然引用了 lerna 会增加了新的复杂度,
// 但在不了解发包流程的前期使用 lerna 会让组件库开发者更专注于组件开发的工作上,而不需要过度关注如何发包。

// 关于调试

// 如何调试代码看这篇,动手练习可以学会,Node.js 也类似。
// https://mp.weixin.qq.com/s/VOoDHqIo4gh3scHVNxk3lA

// 慕课网调试课程:https://www.imooc.com/learn/1164

// 掘金chrome免费小册:https://juejin.cn/book/6844733783166418958

// 慕课网 nodejs调试入门 https://www.imooc.com/learn/1093

// vscode nodejs 调试 https://code.visualstudio.com/docs/nodejs/nodejs-debugging

// vscode还有很多调试文档,都推荐看看
流程总结
  1. 确认要发布的版本
  2. 执行测试用例
  3. 更新所有包的版本号和内部 vue 相关依赖版本号
    1. updatePackage 更新包的版本号
    2. updateDeps 更新内部 vue 相关依赖的版本号
  4. 打包编译所有包
  5. 生成 changelog
  6. 提交代码
  7. 发布包
  8. 推送到 github
    在这里插入图片描述
有用的链接

vuejs发布的文件很多代码我们可以直接复制粘贴修改,优化我们自己发布的流程。比如写小程序,相对可能发布频繁,完全可以使用这套代码,配合miniprogram-ci,再加上一些自定义,加以优化。
当然也可以用开源的 release-it
同时,我们可以:
引入 git flow,管理git分支。估计很多人不知道windows git bash已经默认支持 git flow命令。
引入 huskylint-staged 提交commit时用ESLint等校验代码提交是否能够通过检测。
引入 单元测试 jest,测试关键的工具函数等。
引入 conventional-changelog
引入 git-cz 交互式git commit
等等规范自己项目的流程。如果一个候选人,通过看vuejs发布的源码,积极主动优化自己项目。我觉得面试官会认为这个候选人比较加分。

调试文档
  • 如何调试代码看这篇,动手练习可以学会,Node.js 也类似:
    https://mp.weixin.qq.com/s/VOoDHqIo4gh3scHVNxk3lA

  • 慕课网调试课程:https://www.imooc.com/learn/1164

  • 掘金chrome免费小册:https://juejin.cn/book/6844733783166418958

  • 慕课网 nodejs调试入门: https://www.imooc.com/learn/1093

  • vscode nodejs 调试:https://code.visualstudio.com/docs/nodejs/nodejs-debugging

总结
  1. 阅读源码能感受到代码可读性很高,不管是起名字,还是代码的简洁度,都能学习到,赶紧改掉自己的野路子
  2. 平时做项目都在一个业务功能中转,阅读源码不仅可以学习到新知识,还可以深入学习到平时看到的一些常用的node.js方法 ,读代码的能力也提高了,很有意思
  3. 多多练习调试,调试大法好啊
  4. 并且看完这一期以后发现自己的解读方法:自己先看一遍好好读读,然后再跟着源码解读走一遍,最后思考一下自己吸收了什么,开始动笔。不要一开始上来就完全沉浸在源码解读文章中,这样会失去自我,看完了,很多时候也不知道看了什么,很虚,当然总结能力,跟阅读理解能力,还需要慢慢的加强。
  5. 关键是看完这个发现我们公司目前我接触的没有任何一点关于版本管理的项目和信息,这让我很震惊,学习果然扫盲,可以自己试着一块砖一块砖的加进去😄
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值