Monorepo 策略与方案选型小记
由于最近负责抽象公司项目模块化成 SDK,故预研了 Monorepo 、Lerna、yarn workspaces 管理多模块的方案,记一笔调研结论。
方案调研
采用维基百科的解释:
In version control systems, a monorepo ("mono" meaning 'single' and "repo" being short for 'repository') is a software development strategy where code for many projects is stored in the same repository.
也就是说 Monorepo 是版本管理系统中的一种“开发策略”,宏观上说就是单 repository 管理多 projects(packages) 。
采用 Monorepo 管理多模块的优势以及与单 package 单 repo 的对比:
- 利于跨模块 (package) 调试
若单模块 A 就是一个 repo,要 npm link 给别的 repo 调试,且 A 源码修改了,得重新 npm link 一次。 Monorepo 的话,则是动态引用的各模块。 - 利于各模块的版本发布管理
各模块统一发布,有一个依赖模块改变了,会自动改变依赖与被依赖模块的版本号,且可以选择统一|分别控制版本号的处理。(Fixed | Independent mode)
单模块单 repo 的话,改变了依赖模块 A,手动修改版本号发布后,得手动去找被依赖模块且改版本号然后发布。repo 越多越麻烦。 - 利于各模块的依赖管理
不会装各 packages 间的重复依赖,所有依赖提升到根目录。(导致的缺点就是只调试一个包也要装全部依赖) - 方便生成 CHANGELOG,管理 issue、pr
因为就一个 repo,commit 可以借工具规范生成 changelog 。
缺点就是一个 repo 的体积过大。但相对以上前三点来说,可以接受。
根据调研,一个理想的 monorepo 结构:
.
├── packages
│ ├─ module-a
│ │ ├─ src # 模块 a 的源码
│ │ └─ package.json # 自动生成的,仅模块 a 的依赖
│ └─ module-b
│ ├─ src # 模块 b 的源码
│ └─ package.json # 自动生成的,仅模块 b 的依赖
├── tsconfig.json # 配置文件,对整个项目生效
├── .eslintrc # 配置文件,对整个项目生效
├── node_modules # 整个项目只有一个外层 node_modules
└── package.json # 包含整个项目所有依赖
所有全局配置文件只有一个,这样不会导致 IDE 遇到子文件夹中的配置文件,导致全局配置失效或异常。
node_modules 也只有一个,既保证了项目依赖的一致性,又避免了依赖被重复安装,节省空间的同时还提高了安装速度。
兄弟模块之间通过模块 package.json 定义的 name 相互引用,保证模块之间的独立性,但又不需要真正发布或安装这个模块,通过 tsconfig.json 的 paths 与 webpack 的 alias 共同实现虚拟模块路径的效果。
再结合 Lerna 根据联动发布功能,使每个子模块都可以独立发布。
Lerna 是业界知名度最高的 Monorepo 管理工具,功能完整。
再根据对别的团队 Monorepo 方案调研结果,决定采用 yarn workspaces + lerna 方案管理,也是 yarn 官方推荐的方案,核心是:使用 yarn workspaces 来管理依赖,使用 lerna 来管理 npm 包的版本发布。
Lerna + yarn workspaces
以下是自己建 demo repo 跟着文档尝试后,小记的常用指令及含义。 也建议亲自折腾下体会更深。
Combine yarn workspaces and Lerna
// root -> lerna.json { "npmClient": "yarn", "useWorkspaces": true }
// root -> package.json { "workspaces": [ "packages/*" ], }
因 lerna 本身就是基于 yarn 、npm、git 开发,所以在利用 lerna 构建好项目后开启 yarn workspace 功能仅作如上配置即可。
Lerna
lerna init
创建 lerna 仓库,默认 Fixed 模式。lerna init --independent
可以创建 Independent 模式。
Fixed 模式 -> 所有 package 版本一致
Independent 模式 -> 可以独立控制各个 package 版本
lerna create xxx
创建新模块lerna add <package>[@version] [--dev] [--exact] [--peer]
假如有两个 package-1, package-2,。lerna add babel
, 该命令会在package-1和package-2下安装babellerna add react --scope=package-1
,该命令会在package-1下安装reactlerna add package-2 --scope=package-1
,该命令会在package-1下安装package-2 (软链)lerna bootstrap | yarn install
安装所有依赖项并链接所有的交叉依赖lerna exec
在 packages 中对应包下的执行任意命令。
如要执行 package-A 下的yarn start
:lerna exec --scope package-A -- yarn start
如果不带 --scope package,则默认在根目录执行,如lerna exec -- rm -rf ./node_modules
lerna run --scope my-component test
执行 my-component 下的 npm scriptstest
lerna ls
查看 packageslerna list --json
带路径一起查lerna changed/updated/diff
查出待 publish 的 packagesdiff
的话会可视化修改
不主动git commit | tag
的话,lerna 不会检测到,lerna 底层就是基于 git npm 开发的lerna clean
删除 packages 下的 node_modules
(lerna clean 不会删除项目最外层的根 node_modules)lerna publish
发布 packages 到 npm 仓库,发包前需要登录 npm 账号,否则会上传 git 成功,上传 npm 失败。
(package.json 中的 ”private“: true 不会发布)
publish 内部做得事情:
- 运行 lerna updated 来决定哪一个包需要被 publish
- 如果有必要,将会更新 lerna.json 中的 version (Fixed 模式)
- 将所有更新过的的包中的 package.json 的 version 字段更新
- 将所有更新过的包中的依赖更新
- 为新版本创建一个 git commit 或 tag
- 将包 publish 到 npm 上
该命令也有许多的参数,例如 --skip-git
将不会创建 git commit 或 tag,--skip-npm
将不会把包 publish 到 npm 上。
yarn workspaces
yarn install
跟lerna bootstrap
效果一致,会自动帮忙解决安装和 link 问题yarn workspaces info
各 package 依赖树关系安装 | 删除依赖
i. 给某个 package 安装 | 删除依赖:yarn workspace packageB add packageA@xx.xx.xx
将 packageA 作为 packageB 的依赖进行安装,如果想要不同 package 间的 link,必须明确指定版本号。yarn workspace packageB add -D react
删除:yarn workspace packageB remove packageA
ii. 给根目录 安装 | 删除依赖(适用所有 packages):yarn add -W -D commitizen
root package 安装 commitizenyarn remove -W commitizen
root package 移除 commitizen执行
scripts
运行 packageA 的 dev 命令:yarn workspace packageA dev
每个工作区运行命令:yarn workspaces run xxx
最佳实践
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论