一、为什么要从 Lerna 换到 Changesets
很多做前端项目的团队,尤其是维护多个工具包、组件库的团队,早期都会选 Lerna 来管理多包项目——说白了就是一个大仓库里放一堆小项目(比如一个组件库有按钮包、输入框包、工具函数包,每个都是独立的 npm 包),Lerna 能帮你批量发版、批量改依赖,省了不少手动操作的麻烦。但用久了会发现 Lerna 的两个大问题:一个是它的发版规则太“一刀切”,比如你改了一个工具函数包的小 bug,它会强制把所有依赖这个工具包的组件包都同步升级版本,哪怕组件包本身没改任何代码;另一个是它生成的 changelog 全是自动拼接的提交记录,完全没可读性,甚至很多时候会把无关的提交混进去,后续要查某个版本到底改了啥根本找不到。
而 Changesets 刚好解决了这两个问题:它的核心是“按需记录变更”,你改了哪个包就只给哪个包加变更记录,不会牵连其他包;而且它生成的 changelog 是你自己写的变更说明,不是乱拼的提交记录,可读性强太多。不过很多团队不敢换的核心顾虑是:换了之后之前 Lerna 生成的历史 changelog 会不会丢?之前的版本记录会不会断?这就是我们要讲的增量迁移方案的核心——平滑过渡,不丢历史。
二、迁移前的准备工作
在动手改之前,得先把基础环境和历史数据理清楚,不然很容易半路出错。
2.1 明确迁移的技术栈
我们的示例统一基于前端多包项目常用的技术栈:Node.js 16+(保证兼容两个工具)、pnpm 包管理器(当然 npm 也能用,只是示例用 pnpm 更通用)、Lerna 4.x(最常用的稳定版)、Changesets 2.x(最新稳定版)。
2.2 备份所有历史数据
迁移前必须做备份,这是底线。首先要把整个项目的仓库代码备份,包括所有分支,尤其是历史提交分支;然后要把 Lerna 生成的所有历史 changelog 文件备份——比如每个包下面的 CHANGELOG.md 文件,还有项目根目录的 CHANGELOG.md(如果有的话);另外还要备份项目的版本记录,比如每个包的 package.json 里的 version 字段,还有 Lerna 的版本锁文件(如果有的话)。
备份完之后,要在一个干净的测试分支上做迁移,不要直接在主分支上改,避免影响线上代码。比如你可以在主分支上拉一个叫 test-migrate-changesets 的分支,所有操作都在这个分支上做。
2.3 整理 Lerna 的历史 changelog
Lerna 生成的 changelog 通常有两种形式:一种是每个包自己的 CHANGELOG.md,另一种是根目录的 CHANGELOG.md(如果用了 lerna-changelog 插件的话)。我们要做的是把这些历史 changelog 整理成 Changesets 能识别的格式,或者说,能拼接进新 changelog 的格式。
比如你有一个叫 @my-ui/utils 的工具包,它的 Lerna 生成的 CHANGELOG.md 可能是这样的:
# @my-ui/utils Changelog
## 1.2.0 (2023-05-10)
### Features
- 新增格式化日期的方法
- 新增校验手机号的方法
## 1.1.0 (2023-03-15)
### Bug Fixes
- 修复格式化金额时的精度问题
我们要把这些内容复制下来,后面用来拼接进新的 changelog。
三、增量迁移的核心步骤
增量迁移的核心逻辑是:先保留 Lerna 的历史版本记录,然后逐步用 Changesets 接管新的变更,最后再完全替换 Lerna。
3.1 安装并初始化 Changesets
首先在项目根目录安装 Changesets 的核心包:
# 用 pnpm 安装,也可以用 npm install @changesets/cli -D
pnpm add @changesets/cli -D
安装完之后初始化 Changesets,这会在项目根目录生成一个 .changesets 文件夹,用来存所有的变更记录:
pnpm changeset init
初始化完之后,你会在 .changesets 文件夹里看到一个 config.json 文件,这是 Changesets 的配置文件,我们可以先保持默认,后面再调整。
3.2 保留历史 changelog 的核心操作
很多人以为换了工具就必须重新生成 changelog,其实不用,我们可以把 Lerna 的历史 changelog 直接拼接进 Changesets 生成的新 changelog 里,只要格式对就行。
Changesets 生成的 changelog 格式通常是这样的(以 @my-ui/utils 包为例):
# @my-ui/utils Changelog
## 2.0.0 (2024-01-20)
### Major Changes
- 重构了工具包的导出结构,所有方法都用默认导出改为命名导出
## 1.3.0 (2023-10-05)
### Minor Changes
- 新增校验邮箱的方法
你会发现它和 Lerna 生成的 changelog 格式几乎一样,都是“版本号 + 日期 + 变更类型 + 变更内容”,这就给我们拼接带来了方便。
具体操作是:找到每个包的 CHANGELOG.md 文件(如果 Lerna 没生成每个包的 changelog,就从根目录的 changelog 里拆分出来),把 Lerna 生成的历史版本内容,按版本号从旧到新的顺序,拼接在 Changesets 生成的 changelog 的最前面。
比如上面的 @my-ui/utils 包,拼接后的 changelog 就是:
# @my-ui/utils Changelog
## 1.2.0 (2023-05-10)
### Features
- 新增格式化日期的方法
- 新增校验手机号的方法
## 1.1.0 (2023-03-15)
### Bug Fixes
- 修复格式化金额时的精度问题
## 2.0.0 (2024-01-20)
### Major Changes
- 重构了工具包的导出结构,所有方法都用默认导出改为命名导出
## 1.3.0 (2023-10-05)
### Minor Changes
- 新增校验邮箱的方法
这里要注意版本号的顺序,必须是从旧到新,不然会影响后续的版本升级。
另外,如果你项目根目录有一个总的 changelog,用来记录所有包的变更,那也要把 Lerna 生成的根目录 changelog 拼接进 Changesets 生成的根目录 changelog 里,方法和上面一样。
3.3 用 Changesets 记录新的变更
拼接完历史 changelog 之后,我们就可以用 Changesets 来记录新的变更了。比如你改了 @my-ui/utils 包的一个 bug,你可以运行下面的命令来生成一个变更记录:
pnpm changeset add
运行这个命令之后,会弹出一个交互界面,让你选择你改了哪个包,然后选择变更类型(major 主版本、minor 次版本、patch 补丁版本),最后输入变更的内容。
比如你改了 @my-ui/utils 包的一个 bug,交互过程大概是这样的:
? Which packages were changed? (@my-ui/utils)
? What kind of change is this? (major, minor, patch) patch
? Please write a summary of the change: 修复了格式化日期时的时区问题
选完之后,会在 .changesets 文件夹里生成一个新的变更记录文件,文件名是随机的,内容大概是这样的:
---
"@my-ui/utils": patch
---
修复了格式化日期时的时区问题
这个文件就是 Changesets 用来记录变更的核心,后面发版的时候会根据这个文件生成 changelog 和版本号。
3.4 过渡阶段:同时保留 Lerna 和 Changesets
在迁移的过渡阶段,我们可以同时保留 Lerna 和 Changesets,这样既能用 Changesets 记录新的变更,又能保证旧的版本记录不丢。
具体操作是:发版的时候,先运行 Changesets 的发版命令,生成新的版本号和 changelog,然后再运行 Lerna 的发版命令,同步版本号到所有包的 package.json 里。
比如你要发一个新的版本,操作步骤是:
# 用 Changesets 生成新的版本号和 changelog
pnpm changeset version
# 用 Lerna 同步版本号,这里要加上 --no-push 和 --no-git 选项,避免 Lerna 自动提交和推送
pnpm lerna version --no-push --no-git
运行完这两个命令之后,你会看到所有改了的包的 package.json 里的版本号都更新了,changelog 也更新了,而且历史记录还在。
这个过渡阶段可以持续几个版本,等团队都熟悉了 Changesets 的操作之后,再完全替换 Lerna。
3.5 完全替换 Lerna
当过渡阶段结束,团队已经习惯了用 Changesets 记录变更和发版之后,就可以完全替换 Lerna 了。
具体操作是:
- 卸载 Lerna 相关的包:
pnpm remove lerna @lerna/cli
- 删除 Lerna 相关的配置文件,比如 lerna.json、.lerna 文件夹等。
- 配置 Changesets 的发版命令,把 Changesets 的发版命令加到 package.json 的 scripts 里,比如:
{
"scripts": {
"changeset:add": "changeset add",
"changeset:version": "changeset version",
"changeset:publish": "changeset publish"
}
}
这样以后发版就只需要运行 Changesets 的命令就行了。
四、迁移中的常见问题和注意事项
4.1 版本号冲突的问题
在过渡阶段,可能会出现版本号冲突的问题,比如 Changesets 生成的版本号和 Lerna 生成的版本号不一致。这个问题的解决方法是:在运行 Changesets 的 version 命令之后,再运行 Lerna 的 version 命令,加上 --force-publish 选项,强制 Lerna 同步版本号:
pnpm lerna version --no-push --no-git --force-publish
4.2 changelog 格式不一致的问题
如果 Lerna 生成的 changelog 格式和 Changesets 生成的格式不一致,比如 Lerna 用的是“## 版本号 (日期)”,而 Changesets 用的是“## 版本号 (日期)”,那就要调整 Lerna 的 changelog 格式,或者调整 Changesets 的 changelog 格式,让两者一致。
调整 Changesets 的 changelog 格式可以通过修改 .changesets/config.json 文件来实现,比如你可以设置 changelog 字段来指定 changelog 的格式:
{
"changelog": "keep-a-changelog"
}
4.3 历史提交记录丢失的问题
在迁移过程中,一定要注意备份历史提交记录,不要随意删除 Lerna 生成的提交记录,也不要随意修改历史版本的 changelog。如果不小心删除了,可以从备份的仓库里恢复。
4.4 团队培训的问题
迁移到 Changesets 之后,要给团队做简单的培训,告诉大家怎么用 changeset add 命令记录变更,怎么选择变更类型,怎么写变更内容。比如要告诉大家,改了小 bug 就选 patch,加了新功能就选 minor,改了不兼容的内容就选 major。
五、应用场景和技术优缺点分析
5.1 应用场景
这种增量迁移方案适合所有已经在用 Lerna 管理多包项目,并且想要切换到 Changesets 的团队,尤其是那些对历史 changelog 有严格要求的团队,比如做开源项目的团队,或者做企业级组件库的团队。
如果你的团队刚起步,还没有用 Lerna 管理多包项目,那可以直接用 Changesets,不用走迁移流程。
5.2 技术优缺点
优点
- 平滑过渡:不会丢失历史 changelog 和版本记录,不会影响线上代码。
- 增量迁移:可以逐步替换,不用一次性改完,降低了迁移的风险。
- 保留原有习惯:过渡阶段可以同时用 Lerna 和 Changesets,让团队有一个适应的过程。
- 可读性强:Changesets 生成的 changelog 是人工写的,比 Lerna 自动生成的可读性强很多。
缺点
- 过渡阶段操作繁琐:过渡阶段要同时运行两个工具的命令,增加了操作的复杂度。
- 格式对齐麻烦:如果 Lerna 和 Changesets 的 changelog 格式不一致,要花时间调整。
- 团队适应成本:团队要学习 Changesets 的操作,有一定的适应成本。
六、文章总结
从 Lerna 切换到 Changesets 是很多多包项目团队的必然选择,因为 Changesets 解决了 Lerna 的很多痛点,比如发版规则一刀切、changelog 可读性差等。而增量迁移方案是最适合这种切换的,因为它能保留历史 changelog 和版本记录,平滑过渡,降低迁移的风险。
迁移的核心步骤是:备份历史数据、安装初始化 Changesets、拼接历史 changelog、用 Changesets 记录新的变更、过渡阶段同时保留两个工具、最后完全替换 Lerna。在迁移过程中,要注意版本号冲突、changelog 格式不一致、历史提交记录丢失等问题,还要给团队做简单的培训。
总的来说,只要按照这个方案操作,就能顺利从 Lerna 切换到 Changesets,并且保留所有的历史记录。
Comments