Monorepo

背景

Monorepo 就是把多个包放到一个仓库中,通过统一的流程来管理。这些包可能相互独立,也可能相互关联,或者有依赖关系。

Google、Facebook、Twitter 这些公司都会把很多包放到一个仓库中管理,所以会有巨大的 Monorepo。仓库太大时会带来很多别的问题,比如网络问题、硬件限制、git操作会变慢,所以很多开源项目仅用 Monorepo 来管理一个项目相关的包,比如核心功能和官方组件,比如著名的 Babel。

为什么要使用 Monorepo

  • 让每个包只做一件事情,而且把它做好。

    项目大了之后难以维护,所以进行拆分是必然的结果,借助包管理器可以更好地复用代码,避免出现一个问题要在不同的地方都修复一遍。

  • 支持包之间的依赖关系,无需发布。

    包之间存在依赖关系时,不需要发布内测版进行调试,而是直接在 Monorepo 中建立依赖的 link 即可。

  • 整合开发流程,批量操作。

    通过统一的规范,可以批量地对仓库中所有的包进行统一管理,统一编译和发布,及时更新依赖版本,避免出现漏更新、版本不匹配的情况。

    此外,可以建立统一的工具链,减少拆分时每个项目的模板代码。

  • 方便重构代码。

    因为所有相关的代码都在一个仓库中,所以修改 API 变得非常方便,在 TypeScript 的帮助下更加可以确保接口变更时所有的类型都是匹配的,不论是人工操作还是代码感知的方式在 Monorepo 中都会更加方便。

  • 共享依赖。

    NPM 依赖有个严重的问题就是磁盘资源占用巨大,而 Monorepo 中的包通常会有很多相同的依赖,通过一些工具的辅助,可以将整个 Monorepo 中相同的依赖共享,从而大大地节约磁盘空间。

如何创建 Monorepo

Babel 开发者提供了一个强大的工具用于管理 Monorepo:lerna

lerna 是一个管理 monorepo 的工具,它提供了很多方便的命令,用于批量操作仓库中的包。

通过 lerna init 就可以创建一个 Monorepo,它的目录结构如下:

.
├── lerna.json
├── package.json
└── packages/
    ├── package-1
    ├── package-2
    └── package-3

packages 下面每个目录对应 Monorepo 中的一个包。

lerna 提供了一系列的命令可以操作这些包,比如:

  • lerna bootstrap 可以初始化所有包的依赖,包括同一个仓库中不同包之间的依赖关系。
  • lerna publish 可以批量发布所有发生变化的包。
  • lerna exec -- npm run build 可以在所有包中执行 npm run build

默认情况下, lerna 初始化后会在每个包下面都安装自己的依赖,生成对应的 node_modules,所以并不能起到依赖共享的作用,还是会占用很多空间。举个例子:

.
└── packages/
    ├── package-1/
    │   └── node_modules/
    │       ├── common1
    │       └── a
    ├── package-2/
    │   └── node_modules/
    │       ├── common1
    │       ├── common2
    │       └── b
    └── package-3/
        └── node_modules/
            ├── common2
            └── c

这时如果我们使用的是 npm 包管理工具,可以使用 lerna bootstrap --hoist 来初始化依赖,它会自动分析所有包的依赖,将相同的部分提取出来,放到仓库根目录下的 node_modules 中,从而避免重复安装。得到的结构如下:

.
├── node_modules/
│   ├── common1
│   └── common2
└── packages/
    ├── package-1/
    │   └── node_modules/
    │       └── a
    ├── package-2/
    │   └── node_modules/
    │       └── b
    └── package-3/
        └── node_modules/
            └── c

但是这会导致依赖存在在不同的 node_modules 中,导致某些使用了 node_modules 的目录结构的包出现异常。比如上面的 a 想找同目录下的 common1 时就找不到了。

Yarn Workspaces vs Lerna

Yarn 作为 NPM 的替代品,带来了很多新的特性,workspaces 就是一个很强大的功能。

简单地说,workspaces 就是 monorepo。

实际上 Yarn workspaces 是对 monorepo 底层依赖处理的一种不同的实现,而且它不需要用户感知,它的定位更加底层。

而 lerna 是一个管理 monorepo 的工具,它提供给用户直接使用的命令集。lerna 既可以基于 NPM 使用,也可以基于 yarn 使用,还可以基于 yarn workspaces 使用。

当使用 yarn workspaces 时,需要在 lerna.json 中指定:

{
  "npmClient": "yarn",
  "useWorkspaces": true
}

这时通过 lerna bootstrap 初始化依赖后,lerna 会优先把所有的依赖都安装到仓库根目录下的 node_modules,遇到版本有冲突时,才会把单个项目依赖的版本安装到包目录下的 node_modules。上面的例子会得到如下结构:

.
├── node_modules/
│   ├── common1
│   ├── common2
│   ├── a
│   ├── b
│   └── c
└── packages/
    ├── package-1
    ├── package-2
    └── package-3

通过这个方式得到的依赖的目录结构和单独安装时会比较类似,能更好地兼容对 node_modules 目录结构有依赖的包。这样还有一个好处是需要查找 node_modules 中的某个依赖时,更容易找到。

除此之外,使用 Yarn 安装依赖可能速度会快一些,其他方面并没有太大差别。

Yarn workspaces 也可以单独使用,毕竟 lerna 只是在它的基础上进行了封装,具体可以参考官方文档。

总结

Monorepo 在维护一组有关系的包时,可以对多个包同时开发和调试,不仅简化了开发、调试流程,还避免了依赖的重复安装,节约了时间和空间。这得益于 Lerna 或者 Yarn 对 node_modules 的处理以及包集中管理的模式。

延伸阅读


© 2020