我是怎么把源码应用到实际开发中的

299 阅读4分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

首先感谢若川大佬组织的这次源码阅读

前言

本文的主要目的是为了学习vue-next的发布流程。学习本文后,将获得三个技能

1.nodejs调试技能
2.知晓vue-next发布流程
3.改进自己项目的发布流程

准备工作

clone项目到本地 vue-next,并且确保node 版本10+ yarn 1.x,源码在/vue-next/blob/master/scripts/release

打开项目后,在node scripts中可以看到 发布是执行的 /vue-next/blob/master/scripts/release.js

nodejs 如何调试可以看前面的文章你可能不知道的Vue-devtools打开文件的原理

发布流程-一图胜千言

image.png

源码解析-逐行分析

1-30行,这里主要是引入一些包以及声明一些变量。

minimist解析参数

semvernpm版本管理工具

enquirer 交互式命令行工具

preid 发布的版本好,如果有就采用自定义的,如果没用就采用semver管理。

isDryRun 是否是调试状态

packages 需要打包的package

skippedPackages 需要跳过的

versionIncrements 哪种方式修改版本号

const args = require('minimist')(process.argv.slice(2))
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const semver = require('semver')
// The semantic versioner for npm
const currentVersion = require('../package.json').version
const { prompt } = require('enquirer')
// 交互
const execa = require('execa')

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('.'))

const skippedPackages = []

const versionIncrements = [
  'patch',
  'minor',
  'major',
  ...(preId ? ['prepatch', 'preminor', 'premajor', 'prerelease'] : [])
]

32-40行 一些工具方法

inc 接受一个额外的identifier字符串参数,该参数将附加字符串的值作为预发布标识符 例如:

inc ( '1.2.3' ,  ' prerelease' ,  'beta' )  // '1.2.4-beta.0'

bin 执行命令

run execa 执行

```js
const inc = i => semver.inc(currentVersion, i, preId)
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))

Main函数

43-80选择发行什么版本

  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
  }

targetVersion 目标版本

args 为minimist 获取到的参数,args._中的参数为

npm run release 1.1.2 那么 args._ => [1.1.2]

具体可以参考minimist

如果没有 目标版本那么就会触发一个交互,如下:

image.png

versionIncrements 就是上面定义的,如果选择了custom自定义版本号,那么就继续要求输入:

image.png.

输入完成后由semver校验是否符合npm版本号要求。

如果校验通过就会开启开始要求确认,是否发布。

Running tests...

81-90执行测试

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

这一段很简单。就是是否跳过测试或则只是打印调试。

run函数

const run = (bin, args, opts = {}) =>
  execa(bin, args, { stdio: 'inherit', ...opts })

run(bin('jest'), ['--clearCache']) => 相当于在命令行跑步jest --clearCache,为什么使用bin,是因为可能没有全局安装,使用node_modules中jest bin执行。

run('yarn', ['test', '--bail']) => 相当于 yarn test --bail,yarn是前置安装的所以不需要 bin.

91-94 更新版本号

  // 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)`)
  }

这一段开始更新packages内部以来的版本号。如果更新,那么我们需要搞清楚updateVersions

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) {
  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')
}

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

主要是以上三个函数:updateVersions, updatePackage, updateDeps

updateVersions: 在packages.forEach(p => updatePackage(getPkgRoot(p), version))打一个断点。

image.png

这是我们看到packages是以下包,然后遍历执行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')
}

内容很简单,更新根目录下的版本号,然后在更新其中dependencies, peerDependencies的版本。如下图:

image.png

94-104 开始build

// 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)`)
  }

build执行了两个命令 yarn build --release yarn test-dts-only

106-116 生成日志并且提交

  // 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.')
  }

这一部分执行了 yarn changelog

image.png

然后执行 git diff如果有变更就提交。提交comment是:release: vxxx.

123-142行 打tag&push

// 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- '
        )}`
      )
    )
  }

这一部分含简单就是打tag,然后push,最后如果有跳过的包,会打印出黄色的字体。到底发布就结束了~

总结

通过看完上面的源码,能够学习到一个优秀开源项目的发布流程是什么样的。

其实业界也有很多辅助发布的工具例如:

  • 使用 husky 和 lint-staged 提交commit时用ESLint等校验代码提交是否能够通过检测,如果检测不通过将无法成功提交,从而规范团队编码

  • 使用git-cz来规范提交格式等等