Monorepo
Monorepo 并不是什么新技术,而是一种多包管理方案,一种思维,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。
Monorepo 演进
阶段一:单仓库巨石应用, 一个 Git 仓库维护着项目代码,随着迭代业务复杂度的提升,项目代码会变得越来越多,越来越复杂,大量代码构建效率也会降低,最终导致了单体巨石应用,这种代码管理方式称之为 Monolith。
阶段二:多仓库多模块应用, 于是将项目拆解成多个业务模块,并在多个 Git 仓库管理,模块解耦,降低了巨石应用的复杂度,每个模块都可以独立编码、测试、发版,代码管理变得简化,构建效率也得以提升,这种代码管理方式称之为 MultiRepo。
阶段三:单仓库多模块应用, 随着业务复杂度的提升,模块仓库越来越多,MultiRepo这种方式虽然从业务上解耦了,但增加了项目工程管理的难度,随着模块仓库达到一定数量级,会有几个问题:跨仓库代码难共享;分散在单仓库的模块依赖管理复杂(底层模块升级后,其他上层依赖需要及时更新,否则有问题);增加了构建耗时。于是将多个项目集成到一个仓库下,共享工程配置,同时又快捷地共享模块代码,成为趋势,这种代码管理方式称之为 MonoRepo
Monorepo 的优势
- 多项目代码都在一个仓库中,相同版本依赖提升到顶层只安装一次,节省磁盘内存。
- 依赖调试方便,依赖包迭代场景下,借助工具自动 npm link,直接使用最新版本依赖,简化了操作流程。
- 多项目在一个仓库,工程配置一致,代码质量标准及风格也很容易一致。
- 构建性 Monorepo 工具可以配置依赖项目的构建优先级,可以实现一次命令完成所有的部署。
Monorepo 项目搭建
想要搭建 Monorepo 项目关键在于两个点:
- 如何把包提取到顶层
- 如何让 A 项目 引入 B 项目
这就得借助 npm 的工作空间(pnpm,cnpm 等同理)
pnpm workspace
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中。
我们只需要在主目录下创建一个pnpm-workspace.yaml 文件即可开启pnpm的工作空间功能。并配置代码如下:
packages:
- packages
- docx
其配置意思是把主目录下的 packages 和 docx 当成一个单独的项目。如果是后面代 * 号,例如
packages:
- packages/*
那么说明把 packages 目录下的每个目录都当成一个单独的项目
TIP
需要注意的是,pnpm 并不是通过目录名称,而是通过目录下 package.json 文件的 name 字段来识别仓库内的包与模块的。
使用 Monorepo
接下来,我们创建 packages 和 docx 目录,这时整个项目目录如下,并初始化 pnpm init 各个文件下的 packages 文件.
然后我们尝试一下在主目录下安装 lodash
pnpm install -w D lodash
注意: -w 选项代表在 monorepo 模式下的根目录进行操作。代表每个子包都能访问根目录的依赖 然后我们进入 packages 子包创建 main.js 来测试一下
// packages/main.js
import _ from 'lodash'
console.log(_.chunk(['a', 'b', 'c', 'd'], 2)) // [ [ 'a', 'b' ], [ 'c', 'd' ] ]
执行 node main.js 后,能顺利输出 [ [ 'a', 'b' ], [ 'c', 'd' ] ],说明子包的 lodash 已经是从主包中获取。
TIP
Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension. (Use node --trace-warnings ...
to show where the warning was created)
如果出现这个报错,只需要在 packages.json 加上 "type": "module" 即可。
Monorepo 相关指令
-w
-w 选项代表在 monorepo 模式下的根目录进行操作。
例如在根目录下安装模块
pnpm install -w D lodash
--filter
--filter 选项用于指定操作应用的范围或目标。
例如安装 子项目a 独属的模块
pnpm install D lodash --filter a
或者与 -w 结合,指定 子项目a 安装依赖根目录某个模块
pnpm install D -w lodash --filter a
或者 指定 子项目a 依赖于 子项目 b
pnpm --filter a i -S b
然后我们就可以在 子项目a 的 packages.json 下看到
{
"name": "a",
// ...
"dependencies": {
"b": "workspace:^"
}
}
在实际发布 npm 包时,workspace:^ 会被替换成内部模块 b 的对应版本号(对应 package.json 中的 version 字段)。替换规律如下所示:
{
"dependencies": {
"a": "workspace:*", // 固定版本依赖,被转换成 x.x.x
"b": "workspace:~", // minor 版本依赖,将被转换成 ~x.x.x
"c": "workspace:^" // major 版本依赖,将被转换成 ^x.x.x
}
}