示例仓库 https://github.com/example-repo/lerna-example
在讲 lerna workflow 前我们先粗话来谈下当今主流的项目代码管理方式
杂谈项目管理方式
multiRepos
multiRepos 它是一种管理 organisation 代码的方式, 在这种方式下, 独立功能会拆分成独立的 repo
这是最常见的项目管理方式
优点:
功能拆分颗粒度较细, 职责界线清晰, 功能性模块复用度较高
缺点:
由于历史原因或者拆分问题, 一个 repo 内的依赖可能来源于多个 organisation
issue 提哪是个问题
项目管理, 工作协同比较糟糕
维护成本较高
任何的基层 repo 版本变更, 将会引发一系列上层封装版本变动
changelog 梳理异常折腾
基本靠口口相传
monoRepo
Monorepo 它是一种管理 organisation 代码的方式, 在这种方式下会摒弃原先一个独立功能一个 repo 的方式, 取而代之的是把所有的 modules 都放在一个 repo 内来管理, 而 lerna 是基于此种理念在工具端集合 git 和 npm 的实现.
优点:
功能依旧可以拆分的细粒度
one repo multiple packages 让项目管理比较方便, issue 管理, 代码变更都能比较好清晰的体现给协同开发者
child package 版本变更会自动化同步至相关的 package
缺点:
monoRepo 体积都比较大
配套工具 lerna 有一定的使用成本, 开发者比较容易用错, 另外它有一些约定俗成, 不能妥协的规范, 以及限制
对 packages 内的依赖版本管理只能 ^
不支持多个 registry 的推送
等等
配套的 changelog 方案只适配于 github 详见我的另外一篇文章 - introduce lerna https://github.com/pigcan/blog/issues/3 , 如果是社区项目非常推荐走这一套方案
版本的生成还是存在一定的缺陷, 开发者并不知情 break 等信息
总结
项目开发中使用 multiRepos 和 monoRepo 都可以, 问题在于项目合不合适.
个人角度上:
合适的项目需要有以下特征
存在多个 package
package 与 package 之间相互依赖
符合以上条件我个人比较建议采用 monoRepo, 以及与之带来的 lerna workflow.
当前使用 lerna 确实还会有些小问题, 这也是我们需要解决的点.
lerna workflow
先再次简单的介绍下 lerna
Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.
lerna 模式
在初始化一个项目之前我们必须要清楚, lerna 对管理 monoRepo 有两种模式
- Fixed/Locked mode (default)
- Independent mode
Fixed/Locked 模式
: 官方默认推荐模式, 当前 babel 的项目管理模式, 在该模式下所有的 packages 都会遵循一个版本号, 该版本号维护在 lerna.json 的 version 字段中, 当需要版本发布时 lerna publish 时, 如果一个模块和上一次 release 相比有过变更的话, 会自动发布一个新版本.
这种模式的问题在于: 当有一个 major 变更的时候, 所有 packages 都会都会有一个新的 major 版本.
维护团队认为: 版本是一种非常 cheap 的东西, 所以不必纠结.
Independent 模式: 在该模式下所有 packages 新版本的生成将会由开发者决定, lerna.json 的 version 字段也会随之失效. 这种模式的弊端非常明显, 开发者必须要非常清晰该发什么版本, 事实上在多人协作项目上很难做到这一点.
简单命令速记
- init
- $ lerna init
初始化一个 lerna 项目
- add
- $ lerna add <package>[@version] [--dev]
默认给当前所有的 packages 添加一个依赖
这边需要推荐一个比较有用的命令
- $ lerna add module-1 --scope=module-2 # Install module-1 to module-2
- $ lerna add babel-core # Install babel-core in all modules
这种方式是可以快速建立 packages 的依赖关系, 而不用人为手动建立
- bootstrap
- $ lerna bootstrap
这个命令会安装好所有 packages 的依赖, 以及建立好 packages 相互依赖的软连接
正式流程为:
安装所有 package 的外部依赖.
对存在相互依赖的 package 创建软连接.
在所有已经 bootstrapped 的 package 中执行 npm run prepublish.
在所有已经 bootstrapped 的 package 中执行 npm run prepare.
- publish
- $ lerna publish
发布一个版本.
正式流程为:
执行 lerna updated 来确定哪些包需要被发布.
如有必要会升级 lerna.json 的 version 字段.
对所有需要 update 的 package 进行版本的更新, 并写入他们的 package.json.
队友有需要 update 的 package 进行依赖申明 specified with a caret (^).
创建一个 git commit 和 tag
把包发布至 npm
较为有用的附加参数
- --npm-tag
- $ lerna publish --npm-tag=beta
使用传入的 tag 把包发布至 npm 对应的 dist-tag
- --conventional-commits
- $ lerna publish --conventional-commits
遵从 Conventional Commits Specification https://conventionalcommits.org/ 进行版本生成和 changlog 生成.
- --skip-git
- $ lerna publish --skip-npm
跳过 git 打标
- --skip-npm
- $ lerna publish --skip-npm
跳过 npm 发布
--cd-version
$ lerna publish --cd-version (major | minor | patch | premajor | preminor | prepatch | prerelease)
# uses the next semantic version(s) value and this skips `Select a new version for...` prompt
指定发包的时的语义版本
- clean
- $ lerna clean
移除所有 package 下的 node_modules 目录.
- import
- $ lerna import <path-to-external-repository>
从现有仓库导入一个 package, 这种方式下会保留原有的 commit 的信息
- run
- $ lerna run <script> -- [..args] # runs npm run my-script in all packages that have it
$ lerna run test
$ lerna run build
- # watch all packages and transpile on change, streaming prefixed output
- $ lerna run --parallel watch
执行 package 下 npm script
- exec
- $ lerna exec -- <command> [..args] # runs the command in all packages
- $ lerna exec -- rm -rf ./node_modules
在任何 package 下执行任意的命令
- getting started
- step 1:
- $ npm install --global lerna
- step 2:
- $ mkdir lerna-example
- $ cd lerna-example
- step 3:
- $ lerna init
运行完后在 terminal 中执行 tree 后我们可以看到此时的目录结构为
- lerna-example git:(master) tree
- .
- lerna.json
- package.json
- packages
- step 4:
- $ packages git:(master) mkdir module-a && cd module-a && touch index.js && tnpm init
- $ packages git:(master) mkdir module-b && cd module-b && touch index.js && tnpm init
- $ packages git:(master) mkdir module-base && cd module-base && touch index.js && tnpm init
运行完后在 terminal 中执行 tree 后我们可以看到此时的目录结构为
- lerna-example git:(master) tree
- .
- lerna.json
- package.json
- packages
- module-a
- index.js
- package.json
- module-b
- index.js
- package.json
- module-base
- index.js
- package.json
- step 5:
如果已知 module-base 被 module-a 和 module-b 共同依赖, 同时 module-a 又 依赖 module-b
- lerna-example git:(master) lerna add @alipay/module-base
- lerna-example git:(master) lerna add @alipay/module-b --scope=@alipay/module-a
项目中使用的问题
在协同开发时, 假设如果开发人员在 module-base 上发布了一个并不兼容的提交, 此时做为 pm 的同学很难在没有提前沟通的情况下获知此次变更, 所以在选择版本发布时也很容易出现, 因为 lerna 默认对依赖的描述是 ^, 所以这在信息不对称的情况下很容易造成线上故障.
如何破局呢?
github 用户使用 introduce lerna https://github.com/pigcan/blog/issues/3 文中提及的 https://github.com/lerna/lerna-changelog 来依据 changelog 来管理, 这个方案的缺点是, 版本号生成时并不是完全自动化的, 还是需要人工介入.
非 github 用户或使用 commitizen 用户, 可以借由
--conventional-commits
, 来自动化生成版本以及 changelog
关于 commitzen 相关的可以看我另外一篇文章 用工具思路来规范化 git commit message https://github.com/pigcan/blog/issues/15
第二种方案也是目前我们项目中应用最多的.
应用 commitizen 方案后, package.json 变更为
- {
- "private": true,
- "scripts": {
- "ct": "git-cz",
- "changelog": "./tasks/changelog.js",
- "publish": "./tasks/publish.js"
- },
- "config": {
- "commitizen": {
- "path": "./node_modules/cz-lerna-changelog"
- }
- },
- "husky": {
- "hooks": {
- "commit-msg": "commitlint -e $GIT_PARAMS",
- "pre-commit": "lint-staged"
- }
- },
- "lint-staged": {
- "*.js": [
- "prettier --trailing-comma es5 --single-quote --write",
- "git add"
- ]
- },
- "devDependencies": {
- "@alipay/config-conventional-volans": "^0.1.0",
- "@commitlint/cli": "^6.1.3",
- "commitizen": "^2.9.6",
- "cz-lerna-changelog": "^1.2.1",
- "husky": "v1.0.0-rc.4",
- "lerna": "^2.10.2",
- "lint-staged": "^7.0.4",
- "prettier": "^1.11.1"
- },
- "dependencies": {
- "fs-extra": "^6.0.0",
- "inquirer": "^5.2.0",
- "shelljs": "^0.8.1"
- }
- }
commitizen 应用后仓库结构说明
packages 目录下存放的是所有的子仓库
tasks 目录下存放一些全局的任务脚本, 当前有用的是 publish.js 和 changelog.js
changelog.js, 当有发布任务时, 请事先执行 npm run changelog, 此举意为生成本次版本发布的 changelog, 执行脚本时会提醒, 本次发布是正式版还是 beta, 会予以生成不同版本信息供予发布
publish.js, 当 changelog 生成并调整相关内容完毕后, 执行 npm run publish, 会对如上所有的子 packages 进行版本发布, 执行脚本时会提醒, 本次发布是正式版还是 beta, 会予以不同 npm dist-tag 进行发布
日常开发流程
在常规开发中, 我们的操作方式会变更为如下:
第一步: 使用 commitizen 替代 git commit
即当我们需要 commit 时, 请使用如下命令
$ npm run ct
如果你在全局安装过 commitizen 那么, 直接在项目目录下执行
$ git ct
执行时, 会有引导式的方式让你书写 commit 的 message 信息
如果你是 sourceTree 用户, 其实也不用担心, 你完全可以可视化操作完后, 再在命令行里面执行 npm run ct 命令, 这一部分确实破坏了整体的体验, 当前并没有找到更好的方式来解决.
关于为什么需要 commitizen, 可以参考 这篇文章 https://github.com/pigcan/blog/issues/15
当前我们遵循的是 angular 的 commit 规范.
具体格式为:
- <type>(<scope>): <subject>
- <BLANK LINE>
- <body>
- <BLANK LINE>
- <footer>
type: 本次 commit 的类型, 诸如 bugfix docs style 等
scope: 本次 commit 波及的范围
subject: 简明扼要的阐述下本次 commit 的主旨, 在原文中特意强调了几点 1. 使用祈使句, 是不是很熟悉又陌生的一个词, 来传送门在此 祈使句 https://baike.baidu.com/item/祈使句/19650285 2. 首字母不要大写 3. 结尾无需添加标点
body: 同样使用祈使句, 在主体内容中我们需要把本次 commit 详细的描述一下, 比如此次变更的动机, 如需换行, 则使用 |
footer: 描述下与之关联的 issue 或 break change, 详见案例 https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#heading=h.gbbngquhe0qa
第二步: 格式化代码
这一步, 并不需要人为干预, 因为 precommit 中的 lint-staged 会自动化格式, 以保证代码风格尽量一致
第三步: commit message 校验
这一步, 同样也不需要人为介入, 因为 commitmsg 中的 commitlint 会自动校验 msg 的规范
第四步: 当有发布需求时, 先生成 changelog
使用
$ npm run changelog
在这一步中我们借助了 commitizen 标准化的 commit-msg 以及 lerna 中 publish 的
--conventional-commits
来自动化生成了版本号以及 changelog, 但过程中我们忽略了 git tag 以及 npm publish (
--skip-git --skip-npm
), 原因是我们需要一个时机去修改自动化生成的 changelog.
第五步: 再发布
由于第四步中, 我们并没有实质意义上做版本发布, 而是借以 lerna 的 publish 功能, 生成了 changelog, 所以后续的 publish 操作被实现在了自定义脚本中, 即 publish.js 中.
$ npm run publish
第六步: 打 tag
给当前分支打好对应的 git tag 信息, 推送到开发分支
来源: https://juejin.im/entry/5afba2d0f265da0ba567b757