原文
www.lishuaishuai.com/engineering…
前言
对于维护过多个package的同学来说,都会遇到一个选择:这些package是放在一个仓库里维护还是放在多个仓库里单独维护,数量较少的时候,多个仓库维护不会有太大问题,但是当package数量逐渐增多时,一些问题逐渐暴露出来:
- package之间相互依赖,开发人员需要在本地手动执行npm link,维护版本号的更替;
- issue难以统一追踪,管理,因为其分散在独立的repo里;
- 每一个package都包含独立的node_modules,而且大部分都包含babel,webpack等开发时依赖,安装耗时冗余并且占用过多空间。
Monorepo vs Multirepo
Monorepo 的全称是 monolithic repository,即单体式仓库,与之对应的是 Multirepo (multiple repository),这里的“单”和“多”是指每个仓库中所管理的模块数量。
Multirepo 是比较传统的做法,即每一个模块都单独用一个仓库来进行管理,典型案例有 webpack,优缺点总结如下:
优点:
- 各模块管理自由度较高,可自行选择构建工具,依赖管理,单元测试等配套设施
- 各模块仓库体积一般不会太大
缺点:
- issue 管理混乱,在实际使用中会发现 core repo 中经常会出现对一些针对 module 提出的问题,需要做 issue 迁移或关联
- changlog 无法关联,无法很好的自动关联各个 module 与 core repo 之间的变动联系
- 版本更新繁琐,如果 core repo 的版本发生了变化,需要对所有的 module 进行依赖 core repo 的更新
- 测试复杂,对多个相关联 module 测试繁琐
Monorep 是把所有相关的 module 都放在一个仓库里进行管理,每个 module 独立发布,典型案例有 babel,优缺点总结如下:
优点:
- 管理简便,issue 和 PR 都放在一个仓库中进行维护
- changelog 维护简便,所有changelog 都基于同一份 commit 列表
- 版本更新简便,core repo 以及各模块版本发生变更后可以很简便的同步更新其余所有对其有依赖的 module
缺点:
- 仓库体积增长迅速,随着 module 的增多,仓库的体积会变得十分庞大
- 自由度较低,高度的统一导致各个模块的自由度较低,且对统一的配套工具(构建,测试)等要求较高,要能适配各个 module 的要求
Lerna 是什么
A tool for managing JavaScript projects with multiple packages.
Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.
Lerna 是一个管理多个 npm 模块的工具,是 Babel 自己用来维护自己的 Monorepo 并开源出的一个项目。优化维护多包的工作流,解决多个包互相依赖,且发布需要手动维护多个包的问题。Lerna 现在已经被很多著名的项目组织使用,如:Babel, React, Vue, Angular, Ember, Meteor, Jest 。
一个基本的 Lerna 仓库结构如下:
my-lerna-repo/
┣━ packages/
┃ ┣━ package-1/
┃ ┃ ┣━ ...
┃ ┃ ┗━ package.json
┃ ┗━ package-2/
┃ ┣━ ...
┃ ┗━ package.json
┣━ ...
┣━ lerna.json
┗━ package.json
使用
初始化
全局安装 lerna,再执行相关命令
$ npm i -g lerna
$ mkdir lerna-repo && cd $_
$ lerna init
lerna init 命令会创建一个用来配置的 lerna.json,文件以及用于存放所有 module 的 packages 文件夹,如下:
lerna-repo/
┣━ packages/
┣━ lerna.json
┗━ package.json
Lerna 提供两种不同的方式来管理你的项目:Fixed 或 Independent,默认采用 Fixed 模式,如果你想采用 Independent 模式,只需在执行 init 命令的时候加上 --independent 或 -i 参数即可。
Fixed/Locked 模式(默认)
固定模式下 Lerna 项目在单一版本线上运行。版本号保存在项目根目录下 lerna.json 文件中的 version 下。当你运行 lerna publish 时,如果一个模块自上次发布版本以后有更新,则它将更新到你将要发布的新版本。这意味着你在需要发布新版本时只需发布一个统一的版本即可。
Independent 模式(--independent)
独立模式下 Lerna 允许维护人员独立地的迭代各个包版本。每次发布时,你都会收到每个发生更改的包的提示,同时来指定它是 patch,minor,major 还是自定义类型的迭代。
在独立模式下,lerna.json 文件中 version 属性的值将被忽略。
安装依赖
为所有项目安装依赖,类似于 npm i
$ lerna bootstrap
当执行完上面的命令后,会发生以下的行为:
- 在各个模块中执行
npm install安装所有依赖 - 将所有相互依赖的 Lerna 模块 链接在一起
- 在安装好依赖的所有模块中执行
npm run prepublish - 在安装好依赖的所有模块中执行
npm run prepare
为packages文件夹下的package安装依赖
$ lerna add <package>[@version] [--dev] [--exact] # 命令签名
当我们执行此命令后,将会执行下面那2个动作:
- 在每一个符合要求的模块里安装指明的依赖包,类似于在指定模块文件夹中执行 `npm install <package>`。
- 更新每个安装了该依赖包的模块中的 `package.json` 中的依赖包信息
# 例如
$ lerna add module-1 --scope=module-2 # 将 module-1 安装到 module-2
$ lerna add module-1 --scope=module-2 --dev # 将 module-1 安装到 module-2 的 devDependencies 下
$ lerna add module-1 # 将 module-1 安装到除 module-1 以外的所有模块
$ lerna add babel-core # 将 babel-core 安装到所有模块
卸载依赖
$ lerna exec -- <command> [..args] # 在所有包中运行该命令
# 例如
$ lerna exec --scope=npm-list yarn remove listr # 将 npm-list 包下的 listr 卸载
$ lerna exec -- yarn remove listr # 将所有包下的 listr 卸载
清理依赖包
可以通过 clean 命令来快速删除所有模块中的 node_modules 文件夹。基本命令如下:
$ lerna clean
检测模块是否发生过变更
$ lerna updated
# 或
$ lerna diff
创建模块
Lerna 提供了两种创建或导入模块的方式,分别是 create,import。
create
创建一个 lerna 管理的模块。基本命令格式如下:
$ lerna create <name> [loc]
name 是模块的名称(必填项,可包含作用域,如 @uedlinker/module-a),必须唯一且可以发布(npm 仓库中无重名已发布包)
loc 是自定义的包路径(选填), 会根据你在 lerna.json 文件中的 packages 的值去匹配,默认采用该数组的第一个路径,指定其他路径时只要写明路径中的唯一值即可,例如想选择 /user/lerna-repo/modules 这个路径,只需要执行如下命令即可
命令执行完后,lerna 会帮我们在指定位置创建模块的文件夹,同时会默认在该文件夹下执行 npm init 的命令,在终端上根据根据提示填写所有信息后会帮我们创建对应的 package.json 文件,大致的结构如下
lerna-repo/
┣━ packages/
┃ ┗━ package-a/
┃ ┣━ ...
┃ ┗━ package.json
┣━ lerna.json
┗━ package.json
import
导入一个已存在的模块,同时保留之前的提交记录,方便将其他正在维护的项目合并到一起。基本命令格式如下:
$ lerna import <dir>
dir 是本项目外的包含 npm 包的 git 仓库路径(相对于本项目根路径的相对路径)
执行后会将该模块整体复制到指定的依赖包存放路径下,同时会把该模块之前所有提交记录合并到当前项目提交记录中
查看模块列表
创建完毕之后,我们可以通过 list 命令来查看和确认现在管理的包是否符合我们的预期,执行如下命令:
$ lerna list
运行 script 脚本
lerna run 运行 npm script,可以指定具体的 package。
$ lerna run <script> -- [..args] # 在所有包下运行指定
# 例如
$ lerna run test # 运行所有包的 test 命令
$ lerna run build # 运行所有包的 build 命令
$ lerna run --parallel watch # 观看所有包并在更改时发报,流式处理前缀输出
$ lerna run --scope my-component test # 运行 my-component 模块下的 test
版本迭代
lerna 通过 version 命令来为各个模块进行版本迭代。基本命令如下:
$ lerna version [major | minor | patch | premajor | preminor | prepatch | prerelease]
如果不选择此次迭代类型,则会进入交互式的提示流程来确定此次迭代类型
例如:
$ lerna version 1.0.1 # 按照指定版本进行迭代
$ lerna version patch # 根据 semver 迭代版本号最后一位
$ lerna version # 进入交互流程选择迭代类型
注意: 如果你的 lerna 项目中各个模块版本不是按照同一个版本号维护(即创建时选择 independent 模式),那么会分别对各个包进行版本迭代
当执行此命令时,会发生如下行为:
- 标记每一个从上次打过 tag 发布后产生更新的包
- 提示选择此次迭代的新版本号
- 修改
package.json中的version值来反映此次更新 - 提交记录此次更新并打 tag
- 推送到远端仓库
小技巧: 你可以在执行此命令的时候加上 ——no-push 来阻止默认的推送行为,在你检查确认没有错误后再执行 git push 推送
--conventional-changelog
$ lerna version --conventional-commits
version 支持根据符合规范的提交记录在每个模块中自动创建和更新 CHANGELOG.md 文件,同时还会根据提交记录来确定此次迭代的类型。只需要在执行命令的时候带上 --conventional-changelog 参数即可
--changelog-preset
$ lerna version --conventional-commits --changelog-preset angular-bitbucket
changelog 默认的预设是 angular,你可以通过这个参数来选择你想要的预设创建和更新 CHANGELOG.md
预设的名字在解析的时候会被增添 conventional-changelog- 前缀,如果你设置的是 angular,那么实际加载预设的时候会去找 conventional-changelog-angular 这个包,如果是带域的包,则需要按照 @scope/name 的规则去指明,最后会被解析成 @scope/conventional-changelog-name。
小技巧: 上述 2 个参数也可以直接写在 lerna.json 文件中,这样每次执行 lerna version 命令的时候就会默认采用上面的 2 个参数
"command": {
"version": {
"conventionalCommits": true,
"changelogPreset": "angular"
}
}
发布
在一切准备就绪后,我们可以通过 publish 命令实现一键发布多个模块。基本命令如下:
$ lerna publish
当执行此命令时,会发生如下行为:
- 发布自上次发布以来更新的包(在底层执行了
lerna version,2.x 版本遗留的行为) - 发布当前提交中打了 tag 的包
- 发布在之前的提交中更新的未经版本化的 “canary” 版本的软件包(及其依赖项)
注意: Lerna 不会发布在 package.json 中将 private 属性设置为 true 的模块,如果要发布带域的包,你还需要在 'package.json' 中设置如下内容:
"publishConfig": {
"access": "public"
}
如果之前已执行过 lerna version 命令,这里如果直接执行 lerna publish 会提示没有发现有更新的包需要更新,我们可以通过从远端的 git 仓库来发布:
lerna publish from-git
lerna.json 解析
{
"version": "1.1.3",
"npmClient": "npm",
"command": {
"publish": {
"ignoreChanges": [
"ignored-file",
"*.md"
]
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
},
"packages": ["packages/*"]
}
version:当前库的版本 npmClient: 允许指定命令使用的client, 默认是 npm, 可以设置成 yarn command.publish.ignoreChanges:可以指定那些目录或者文件的变更不会被publish command.bootstrap.ignore:指定不受 bootstrap 命令影响的包 command.bootstrap.npmClientArgs:指定默认传给 lerna bootstrap 命令的参数 command.bootstrap.scope:指定那些包会受 lerna bootstrap 命令影响 packages:指定包所在的目录
适用场景
最后我们来说说 Monorepo 的适用场景
- 不过分庞大的项目,整合到一起有 100G 源码的话,还是再考虑一下吧
- 多模块 / 插件化项目,把官方维护的插件都作为 package 非常合适
另外,还需要:
- 基础建设
- 团队信任
基础建设是指强大的构建工具,能满足所有模块的 build 需求(纯前端项目的话,build 压力不大)
Monorepo 环境下,并且鼓励改别人的代码,一方面需要持续集成机制(例如 React – CircleCI)确认修改带来的影响,另一方面还需要不同团队之间互相信任。
参考
sosout.github.io/2018/07/21/… www.uedlinker.com/2018/08/17/… juejin.cn/post/684490…