lerna workflow
soda-x opened this issue · 13 comments
在讲 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 的推送
- 等等
- 对 packages 内的依赖版本管理只能
- 配套的 changelog 方案只适配于 github 详见我的另外一篇文章 - introduce lerna,如果是社区项目非常推荐走这一套方案
- 版本的生成还是存在一定的缺陷,开发者并不知情 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 进行版本生成和 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 文中提及的 lerna-changelog 来依据 changelog 来管理,这个方案的缺点是,版本号生成时并不是完全自动化的,还是需要人工介入。
- 非 github 用户或使用 commitizen 用户,可以借由
--conventional-commits
,来自动化生成版本以及 changelog
关于 commitzen 相关的可以看我另外一篇文章 用工具思路来规范化 git commit message
第二种方案也是目前我们项目中应用最多的。
应用 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,可以参考 这篇文章
当前我们遵循的是 angular 的 commit 规范。
具体格式为:
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
type
: 本次 commit 的类型,诸如 bugfix docs style 等
scope
: 本次 commit 波及的范围
subject
: 简明扼要的阐述下本次 commit 的主旨,在原文中特意强调了几点 1. 使用祈使句,是不是很熟悉又陌生的一个词,来传送门在此 祈使句 2. 首字母不要大写 3. 结尾无需
添加标点
body
: 同样使用祈使句,在主体内容中我们需要把本次 commit 详细的描述一下,比如此次变更的动机,如需换行,则使用 |
footer
: 描述下与之关联的 issue 或 break change,详见案例
第二步:格式化代码
这一步,并不需要人为干预,因为 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 信息,推送到开发分支
更新: git tag 操作由 publish 脚本来承担
packages 下的 modules 相互依赖可以不使用 ^
控制版本,在发布时使用 publish --exact
以做到精确控制版本的更迭
Lerna最大的优点: Splitting up large codebases into separate independently versioned packages is extremely useful for code sharing. 作者说的优点其实没有lerna也可以实现。
@techbirds 感觉你是不是看错了什么,lerna 只是一种项目管理方式下的工具端实现。
可能我没表达明白,只是觉得作者的这篇文章没有表达monoRepo的核心作用,你说的优点和缺点都没问题。不过也可能是我没get到你的点。@pigcan
@techbirds 非常欢迎王同学把自己的观点亮出来。这样我写文章的意义也就大了。
@pigcan 我给前辈发了一封邮件(gmail),在lerna工作流中遇到了一些问题,希望有时间可以帮忙看看,回复一下~~thanks~
@Mr-jiangzhiguo 不是. 发布后续交给另外的流程来处理
@Mr-jiangzhiguo 那确实有可能 lerna 自身已经发生了很大的变化. 但这套方式我现在在内部工程里面还有在用. lerna 我是固定在某个版本的.