跟尤雨溪对话:我从vuejs/core发布中学到了什么?

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

读开源作者的源代码,就犹如跟作者一起对话一样,学到其中源码的精髓、思想,犹如大师在黑板上讲解项目的实现思路,收益匪浅,就说这么多,如果你有兴趣也一起来读源码吧。

1、找到开源项目

2、查看文件发现他们的大致共性

  • 管理依赖: pnpm(全部)

  • 实现仓库的monorepo: pnpm的workspace(全部)

  • typescript:使用ts进行编写代码(全部)

  • github yml: 工作流(全部)

  • Git Hook 工具:husky + lint-staged(element-plus和vant)

  • 代码规范: EditorConfig+Prettier + ESLint(除了vue3.0没有使用EditorConfig,其他三个仓库都用了)

  • 提交规范:Commitizen + Commitlint (element-plus)

  • 打包工具: Rollup(vue3.0和element-plus) 、esbuild(vant和vite)

  • 单元测试: vitest(element-plus和vite)、jest(vue3.0和vant)

3、接下来有时间我会学习一下以下知识

  • pnpm和monorepo下的项目库、二次封装组件库、工具库

  • 创建工程化项目

    • 代码规范: EditorConfig+Prettier + ESLint

    • 提交规范:Commitizen + Commitlint

    • Git Hook 工具:husky + lint-staged

    • github yml: 工作流

    • typescript:现在的vue3项目中零零散散的也使用了一下ts,但还不够深入,有时间继续深入学习一下

    • 打包工具: Rollup,通过Rollup开发一个组件库

    • 单元测试: vitest和jest要学习一个,来练练手,工具库的单元测试还是比较好处理,现在是组件库的单元测试要学习一下

4、本文主要重点知识

  • 上一篇博文通过google/zx写了自己公司15个项目的编译打包上传过程

  • 接下来就来看看vue3.0源码中是如何通过脚本或者自动化的手段去处理打包发布的过程

5、严格校验使用pnpm 安装依赖

  • 在项目根目录下使用yarn命令的话会有提示
        yarn
        // 提示如下:
        yarn install v1.22.17
        info No lockfile found.
        $ node ./scripts/preinstall.js
        This repository requires using pnpm as the package manager  for scripts to work properly.
    
        error Command failed with exit code 1.
        info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
    复制代码
  • 如果使用pnpm就不会有上述提示了,不过如果确实有要求要使用yarn命令
        // 会忽略相应的前置钩子 prexxxx,和后置钩子 postxxxx。
        yarn --ignore-scripts, 
    复制代码
  • 通过package.json中scripts脚本列表中的preinstall,做了判断处理
         "preinstall": "node ./scripts/preinstall.js",
        
    
        // 查看scripts/preinstall.js文件
        if (!/pnpm/.test(process.env.npm_execpath || '')) {
            console.warn(
                `\u001b[33mThis repository requires using pnpm as the package manager ` +
                ` for scripts to work properly.\u001b[39m\n`
            )
            process.exit(1)
        }
    复制代码

    查看vite脚手架 github.com/vitejs/vite…

    // 一个命令行便可以限定只能使用pnpm
     "preinstall": "npx only-allow pnpm",
    复制代码
    其实这两者干了同样一件事情,都是只允许使用pnpm进行执行scripts

6、调试script/release.js

  • 先到package.json中找到scripts的 release
"release": "node scripts/release.js",
复制代码
  • 开启调试的方式
    • 将鼠标悬浮于 release上,可以看到[运行脚本]和[调试脚本] 点击调试脚本即可调试,当然要提前设置断点
    • 或者可以看到scripts上方,会有一个调试按钮,点击选择 release即可进入调试状态
  • 开启后终端有会如下显示
        pnpm run release
        Debugger attached.
    
        > @3.2.36 release H:\github\sourceCode\core
        > node scripts/release.js
    
        Debugger attached.
        ? Select release type ... 
        > patch (3.2.37)
        minor (3.3.0)
        major (4.0.0)
        custom
    复制代码
  • 文件地址 github.com/vuejs/core/…

7、release.js中引用的依赖说明

  • 依赖minimist:解析命令行中的参数

    // 安装依赖
    npm i minimist
    
    // 引入依赖
    import minimist from 'minimist'
    
    console.log(process.argv, 'process')
    const argv = minimist(process.argv.slice(2))
    
    console.log(argv, '打印参数列表')
    
    //通过node环境直接执行 
    node ./other/minimist.js -a aa -b bb -c cc
    // [
    // 'C:\\Program Files\\nodejs\\node.exe',
    // 'H:\\github\\2022\\zx-ts\\other\\minimist.js',
    // '-a',
    // 'aa',
    // '-b',
    // 'bb',
    // '-c',
    // 'cc'
    // ] process
    
    // { _: [], a: 'aa', b: 'bb', c: 'cc' } 打印参数列表
    复制代码

    可以发现其中process.argv的第一和第二个元素是Node可执行文件路径和被执行js文件的路径。

  • chalk终端多色彩输出

    npm i chalk
    
    import chalk from 'chalk'
    
    console.log(chalk.blue('打印参数列表'))
    复制代码
  • semver 语义化版本 详细解释 semver.org/lang/zh-CN/

    // 举个简单的例子版本号 2.0.1
    // 版本号格式: 主版本号(major).次版本号(minor).修订号(patch)
    // 则 2为主版本号  0为次版本号  1为修订号
    // major: 变化意味着本地变更发生了巨大的变化(当你做了不兼容的 API 修改)
    // minor: 通常只反映了一些较大的更改(当你做了向下兼容的功能性新增)
    // patch 通常称之为补丁版本(当你做了向下兼容的问题修正)
    
    // 再举个简单的例子: 2.0.1-beta.1 
    // 这个就相当于先行版本号
    
    //release.js中涉及到的api
    
    //验证版本号
    console.log(semver.valid('0.0.3'), 'valid验证版本号')   // 0.0.3 ✔
    console.log(semver.valid('0.0.3-beta.1'), 'valid验证版本号')   // 0.0.3-beta.1 ✔
    console.log(semver.valid('0.0.3.44'),'验证版本号0.0.3.44')  // null ❌
    
    // 获取先行版本号后的标识和版本号
    console.log(semver.prerelease('0.0.3-beta.1'), 'prerelease1')  // beta  1 ✔
    console.log(semver.prerelease('1.0.0-alpha+001'), 'prerelease2')  // alpha ❌
    console.log(semver.prerelease('1.0.0-beta+exp.sha.5114f85'), 'prerelease3')   // beta❌
    console.log(semver.prerelease('1.0.0+b11111'), 'prerelease4')  // null  错误❌
    
    // 现有版本号为0.0.3,通过inc获取新的版本号
    console.log(semver.inc(currentVersion, 'major'), 'inc-major')  // 1.0.0
    console.log(semver.inc(currentVersion, 'minor'), 'inc-minor')  // 0.1.0
    console.log(semver.inc(currentVersion, 'patch'), 'inc-patch')  // 0.0.4
    复制代码
    npm i semver
    复制代码
  • enquirer 交互式询问CLI 简单说就是交互式询问用户输入。

    npm i enquirer
    
    import enquirer from 'enquirer'
    
    let tempArray = ['major(1.0.0)','minor(0.1.0)', 'patch(0.0.4)', 'customer' ]
    
    const { release } = await enquirer.prompt({
        type: 'select',
        name: 'release',
        message: 'Select release type',
        choices: tempArray
    })
    
    if(release === 'custom') {
        console.log(release, 'customer')
    } else {
        const targetVersion = release.match(/\((.*)\)/)[1]
        console.log(targetVersion, 'targetVersion')
    }
    复制代码

    执行命令后可以看到四个选项 major(1.0.0) minor(0.1.0) patch(0.0.4) customer 选择不同的选项,则根据不同的选项进行判断处理不同的逻辑

  • execa 执行命令行的

    import { execa } from 'execa'
    import {$} from 'zx'
    
    const arr = ['aaa', 'bbbb']
    const { stdout } = await execa('echo', arr)
    console.log(stdout, 'stdout')
    
    // 这个是通过google/zx的神器调用的命令行,我自己感觉灰常好用
    await $`echo -e  ${arr}  google/zx仓库`
    复制代码
  • 在package.json中发现 run-s 查了半天没找到太多资料,原来是npm-run-all的缩写,虽然我对npm-run-all也不了解,但这个关键字的搜索信息就海量了。

    • 串行执行 clean、 lint build命令
    npm-run-all clean lint build
    
    // 同样可以使用缩写命令
    run-s clean lint build
    复制代码

    以前也可以使用 && 进行串行执行命令

    "XXX": "npm run clean && npm run lint && npm run build" 
    复制代码

    或者说是顺序执行三个命令,如果某个脚本退出时返回值为空值,那么后续脚本默认是不会执行的,不过你可以使用参数--continue-on-error 来规避这种行为。

    • 并行执行 三个命令
    npm-run-all --parallel clean lint build
    
    // 同样可以使用缩写命令
    run-p clean lint build
    复制代码

    以前也可以使用 & 进行并行执行命令

    "XXX": "npm run clean & npm run lint & npm run build" 
    复制代码

    同时执行这三个任务,需要注意如果脚本退出时返回空值,所有其它子进程都会被 SIGTERM 信号中断,同样可以用 --continue-on-error 参数禁用行为。

8、vue3.0 release整个过程

  • 1、选择要发布的版本:

    • major
    • minor
    • patch
    • custom
  • 2、执行测试用例 执行测试用例分为了两个部分

    await run(bin('jest'), ['--clearCache'])
    await run('pnpm', ['test', '--bail'])
    复制代码

    第一行是执行jest测试用例 第二行是执行命令行中的测试用例

    "test": "run-s \"test-unit {@}\" \"test-e2e {@}\"",
    
    "test-unit": "jest --filter ./scripts/filter-unit.js",
    "test-e2e": "node scripts/build.js vue -f global -d && jest --filter ./scripts/filter-e2e.js --runInBand",
    复制代码

    顺便来看看run方法的实现

    // 读取node_modules下.bin目录下传递进来的name
    const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name)
    
    // 通过execa来执行bin下的命名
    const run = (bin, args, opts = {}) =>
        execa(bin, args, { stdio: 'inherit', ...opts })
    复制代码
  • 3、更新根目录package.json版本号

    
    // const path = require('path') 
    // 引用path模块,通过path模块读取并拼接路径、读取当前release.js文件所在路径,并返回上一级 
    let pkgRoot = path.resolve(__dirname, '..'), 
    
    // 拼接根目录package.json所在路径
    const pkgPath = path.resolve(pkgRoot, 'package.json')
    
    //const fs = require('fs') 
    // 引用fs模块,通过fs模块读取package.json文件内容
    // 再通过JSON.parse对读取的字符串内容进行转换,转换为JSON对象
    const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
    
    // 将第一步选择的新版本号version赋值给JSON对象的version
    pkg.version = version
    
    
    //因为在根目录下的package.json中不存在dependencies和peerDependencies节点,所以一下两行代码执行了也没有什么效果
    //updateDeps(pkg, 'dependencies', version)
    //updateDeps(pkg, 'peerDependencies', version)
    
    // 根目录版本号设置完毕后,重新写会package.json文件
    fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
    复制代码
  • 4、更新packages文件夹下内部vue相关依赖的版本号

    // 读取release.js的当前路径,并返回上一级目录,再拼接packages
    //读取此目录下的文件,并过滤掉以.ts结尾的文件和以'.'开头的文件
    const packages = fs
    .readdirSync(path.resolve(__dirname, '../packages'))
    
    .filter(p => !p.endsWith('.ts') && !p.startsWith('.'))
    
    
    //通过循环去更新packages文件夹下单独包的版本号
    packages.forEach(p => updatePackage(getPkgRoot(p), version))
    
    // 传递pkg 文件夹名称(也就是包名)然后拼接到路径后面
    const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg)
    
    //然后根据路径和版本号,跟第三部修改根目录下版本号代码师一致的
    复制代码
  • 5、打包编译所有包 run方法上面有提到过,可以参看

    await run('pnpm', ['run', 'build', '--release'])
    // test generated dts files
    step('\nVerifying type declarations...')
    await run('pnpm', ['run', 'test-dts-only'])
    复制代码
  • 6、生成changelog

    await run(`pnpm`, ['run', 'changelog'])
    
    // 实际执行的是package.json中的scripts脚本
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    
    // 可以发现实际使用的模块依赖是conventional-changelog-cli
    
    // 看到命令行中有一个angular很奇怪,查阅发现
    // 如果你的所有commit都符合Angular commit规范,那么发布新版本时,就可以通过脚本自动生成changelog。
    复制代码

    在conventional-changelog仓库下有一个推荐,standard-version

    // 安装依赖
    npm install -D standard-version
    
    // 通常使用前要先git add .,git commit -m ...,提交完成后再执行 如下命令
    // 更新主版本号 1.0.0 => 2.0.0
    standard-version -- --release-as major
    
    //更新次版本号1.0.0 => 1.1.0
    standard-version -- --release-as minor
    
    // 更新修订版本号 1.0.0 =>1.0.1
    standard-version -- --release-as patch
    复制代码

    执行完上述命令时,如果提交存在feat和fix类型的话,就会自动生成CHANGELOG.md,当然你可以手动设置要更新到CHANGELOG.md中的类型。

  • 7、更新pnpm-lock.yaml

    await run(`pnpm`, ['install', '--prefer-offline'])
    复制代码
  • 8、提交代码 通过git diff 命令来判断是否有文件变更,如果有则通过git add . git commit -m ...进行提交

    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.')
    }
    复制代码

    顺便说一下runIfNotDry函数, run命名是真正的通过execa执行命名,而dryRun只是进行console.log打印,并没有真正的执行命令。然后isDryRun 读取命令行中是否存在dry参数,存在的话则不进行执行命令

    const isDryRun = args.dry
    
    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
    复制代码
  • 9、将packages包发布到npmjs上 关于packages变量可以参考第四部分

    // 循环对每个模块包进行yarn publish
    
    for (const pkg of packages) {
        await publishPackage(pkg, targetVersion, runIfNotDry)
    }
    
    //单个publish 核心代码(runIfNotDry可以参考第八部分中的说明)
    await runIfNotDry(
      'yarn',
      [
        'publish',
        '--new-version',
        version,
        ...(releaseTag ? ['--tag', releaseTag] : []),
        '--access',
        'public'
      ],
      {
        cwd: pkgRoot,
        stdio: 'pipe'
      }
    )
    复制代码

    在npmjs.com中搜索你可以发现,packages文件夹下的所有包都单独存在的,说明都可以单独引用,也就是在某些项目中,可以单独下载引用某些库,这里可以说是一个发现。

  • 10、将tag标签和commit进行push

    await runIfNotDry('git', ['tag', `v${targetVersion}`])
    await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
    await runIfNotDry('git', ['push'])
    复制代码

    第一行通过git tag打tag标签

    第二行对tag标签,进行推送push

    第三行对commit进行推送push

9、总结

  • 了解到版本号可以自动设置
  • changelog.md可以根据commit自动生成
  • commit 规范 要搞起来,方便自动化
  • 命令行的串行执行和并行执行
  • node_modules下的.bin目录存放了各种命令工具
  • 限制某个命令的运行,通过钩子函数去限制

猜你喜欢

转载自juejin.im/post/7108708331711103013