一 前言
在实际工作中,经常会出现需要同时维护多个相互依赖项目的情况。比如 C 项目是一个底层的项目,被抽成了一个 NPM 包。而 A 项目和 B 项目都同时依赖 C 项目的代码。随着 A 项目的开发,可能要不时修改底层 C 项目的代码。那如何优雅的解决各项目之间互相依赖的问题?当 C 项目升级发布之后,如何更好的处理 A,B 项目的版本同步,也是一个让人头疼的问题。
针对上述的问题,下面介绍的 monorepos 可以为我们提供一种管理多项目的全新解决思路:
二 什么是 monorepos?
monorepos 是一种源码管理的方式,它在一个 Git 仓库中管理所有的相关项目。很多知名互联网公司(如 Google,Facebook,Microsoft)和开源项目(如 Babel,React,Vue,Angular,Jest)都使用这种方式管理它们的代码。
与 monorepos 相对应的一个概念是 multirepos。这种方式是为每个独立的项目都创建一个 Git 仓库,多个项目就会存在多个相互独立的 Git 仓库。(这也是我们公司之前唯一使用的方式)
三 monorepos 和 multirepos 对比
从上一节的内容可以看出,monorepos 和 multirepos 主要的不同点在于如何管理和组织一系列相关的项目。那不同的组织和管理方式究竟会带来哪些不一样的地方,我们通过分析 monorepos 和 multirepos 各自的优劣将会得到一些答案。
3.1 monorepos 优缺点
优点:
- 代码具有更高的可维护性,代码重用变的简单:所有项目相关的代码都在一个仓库里,相对于 multirepos,更容易发现和抽象出代码中共用的逻辑。
- 依赖管理变的简单:由于所有项目都是在同一仓库中,无需跨仓库引用,利用一些工具,很容易处理好不同子项目之间的依赖。
- 统一项目的工程配置和公共依赖:把多个项目用到的公共依赖(如 eslint, babel, style lint 等)和相关的工程配置(husky,commitlint)抽象到全局,这样不用每个项目都单独配置。
- 所有相关项目统一入口,统一 README 文件:免去查找相关仓库的时间,在进行项目交接时也不会遗漏相关信息。
缺点:
- 权限管理稍显麻烦:如果需要精细控制项目中每个 package 的权限,会比较麻烦。
- 构建时间会比 multirepos长:由于所有的项目都在同一个仓库中,所以构建的时间会比单独的 multirepos更长一些。
- 对新人来说,有一定的学习成本和适应过程。
3.2 multirepos 优缺点
优点:
- 按照模块划分代码仓库,每个模块体积较为合理。
- 由于不同模块是不同仓库,所以权限管理较 monorepos 更易于实现。
缺点:
- 调试相对麻烦:multirepos 一般使用
npm link
调试依赖项目的代码,如果依赖多了,手动管理 link 也是一件麻烦的事情。 - 版本管理需要手动升级依赖。
- 复用代码和相关配置比较困难:脚手架能解决一部分问题,但是如果有升级的话就比较麻烦
3.3 如何选择
不管是 monorepos 和 multireops 都有各自适用的场景。那到底选择什么样的方式,也需要根据你的实际场景来做决定。
这里有几个可以参考的指标,我们可以通过下列几个指标来问问自己,是否真的需要使用 monorepos。
- 正在维护的这一组 packages 之间是否有相关性?比如都是同一业务线下的相似项目,又或者都是一些工具类的库或插件?
- 这些 packages 之间是否相互依赖?比如有一个核心的 package, 这个 packages 被其他 package 所依赖,并且需要在核心 package 版本发生变化后,升级依赖方的版本。
- 这些 packages 之间是否有较多共同依赖?
四 monorepos 实践
创建 monorepos 业内有多种方案,这里只介绍其中比较流行的两种方案 Yarn workspaces 和 lerna.
4.1 Yarn WorkSpaces
Yarn 从 1.0 版开始支持 Workspaces(工作区)。Workspaces 能更好的统一管理有多个项目的仓库,既可在每个项目下使用独立的 package.json 管理依赖,又可便利的享受一条 yarn 命令安装或者升级所有依赖等。更重要的是可以使多个项目共享同一个 node_modules 目录,提升开发效率和降低磁盘空间占用。
关于如何使用 yarn workspaces 创建 monorepos 可参见文章。
4.2 Lerna
Lerna 是一个管理多个 npm 模块的工具,是 Babel 自己用来维护自己的 Monorepo 并开源出的一个项目。它主要解决了多个包互相依赖,且发布需要手动维护多个包的问题。
关于如何使用 Lerna 创建 monorepos 可参见文章。
4.3 如何选择
我们先简单的对比下 Yarn Workspace 和 Lerna 两种方案:
相同点:
- 都可以独立的创建 monorepos。
- packages 之间的相互依赖都可以使用 syslink 链接到本地,也可以直接安装已经发布到指定 npm 源中的版本。
- packages 的共同依赖都可以提升到根目录,避免重复安装,lerna 默认情况下不会提升,需要在 bootstrap 的时候显示指定 --hoist 参数
不同点:
- yarn workspaces 没有提供统一的版本管理与发布。workspaces 不具备本地依赖之间语义版本的自动管理,包的统一发布等。需要进入到各个包内进行手动版本更新或者发布。
从以上的对比中,我不难发现,yarn workspaces 进行 monorepos 的管理时,其方案是不完备的,比如进行 library(提供不同的 NPM 包给外界使用)类型的 monorepos 管理时只使用 yarn workspaces 是不够的。
所以在管理 library 类型的 monorepos 项目时(如果是非 library 类型的项目,用 yarn workspaces 管理就够了),推荐的方案是 yarn workspaces + lerna 组合,yarn 的 workspaces 是一个偏底层的能力,而 lerna 本身也仅是利用 npm 或 yarn 提供的能力来工作的,所以我们可以结合使用 lerna 和 yarn workspaces 为 monorepos 提供全方位的能力。(这也是 yarn 官方的使用方式)
五 项目实践
5.0 项目背景
这里就拿笔者之前开发过的一个表单项目为例,它从业务上抽象出3层,分别是表单核心渲染逻辑层,基于核心渲染逻辑的 UI 层,以及实际应用表单的应用层。其中核心层和 UI 层最终提供的是相关的 NPM 包,应用层是部署的项目(包含后台编辑和移动端渲染)。
各个模块之间的依赖关系如下:
这3层的项目之间存在互相的依赖,且都有一些可以共用的部分,且都属于表单相关的项目。结合前面的说明,该项目就非常适合用 monorepos 来管理相关的代码。
下面重点讲一些配置的关键点,其他配置的内容可翻阅参考文献中的文章进行查询。
5.1 项目组织结构
项目结构如下图:
所有相关项目统一在 packages 目录中维护。根据业务需求分为 3 个目录,core
是表单的核心层,ui-layer
是 UI 渲染层,application-layer
是应用层,包括表单配置后台和表单渲染 h5。
5.2 Yarn Workspaces 配置
我们使用 yarn workspaces 主要做两件事情
- 解决项目间互相依赖的关系
- 减少项目重复的工程化配置和通用包的重复安装
yarn workspace 的配置如下:
(1) 所有项目都放在 packages 目录下,如上图所示.
(2)在项目根目录里的 package.json 文件中,设置 workspaces 属性,属性值为之前创建的目录(当然这里的路径可以写任何你想要的),这样 packages 下所有的模块都成为了该项目的子模块。
{ "name": "xxx", "version": "1.0.0", "private": true, "workspaces": [ "packages/**" ] }
(3) 同样,在 package.json 文件中,设置 private 属性为 true,这样做是为了避免我们误操作将仓库发布。
进行完以上配置之后,只需要在根目录中执行 yarn install
后,您会发现在项目根目录中出现了 node_modules 目录,该目录拥有所有子项目共用的 npm 包。
比如多个项目中同时使用了 vue 这个包,yarn workspaces 会把 vue 这个包作为公共包放在根目录的 node_modules 下,其他子项目中则通过软链的形式链接这个包。如果是某个子项目中独有的包,则只会下载到这个子项目的 node_modules中。
同样的,yarn workspaces 也会自动处理项目间的依赖关系。比如 a 项目依赖 b 这个包,yarn workspces 会自动把本地的 b 作为 a 的依赖(需要 a/package.json 中设置的 b 版本号和本地 b 的版本号一致,如不一致则会从 npm 源中拉取)。
执行完上述命令后,就完成了一个工程里不同项目间的互相依赖和公共包的提升,即可以轻松在一个项目里调试互相依赖的不同模块,又能统一管理依赖的公共包,从而提升开发效率。
5.3 Lerna 配置
我们前面说过,项目中使用 yarn workspaces 是用来处理同个工程中依赖相关问题,而 lerna 则是专注来处理发布包的依赖问题.
lerna 可以解决如下常见的发布包依赖问题:
- 当一个子项目更新后,我们只能手动追踪依赖该项目的其他子项目,并升级其版本。
本项目中 Lerna 的配置如下:
{ "npmClient": "yarn", "useWorkspaces": true, "packages": [ "packages/*" ], "version": "independent" }
lerna 可以指定 npm 客户端,这里使用的是 yarn。同时我们也可以让 Lerna 追踪我们 workspaces 设置的目录,这样我们就依旧保留了之前 workspaces 的所有特性(子项目引用和通用包提升)。
这里再重点讲下version,它表示 lerna 的两种工作模式:Independent mode 和 Fixed/Locked mode。
lerna 的默认模式是 Fixed/Locked mode ,在这种模式下,实际上 lerna 是把工程当作一个整体来对待。每次发布 packges,都是全量发布,无论是否修改。但是在 Independent mode 下,lerna 会配合 Git,检查文件变动,只发布有改动的packge。我们这里使用的是 independent 模式, 因为我们项目里多个包的版本是保持独立的。
在需要发布相关包的时候,只需要执行 lerna publish
命令。 你就可以根据cmd中的提示,一步步的发布packges了。而实际上在执行该条命令的时候,lerna会做很多的工作,具体如下:
- Run the equivalent of
lerna updated
to determine which packages need to be published.
- If necessary, increment the
version
key inlerna.json
. - Update the
package.json
of all updated packages to their new versions. - Update all dependencies of the updated packages with the new versions, specified with a caret (^).
- Create a new git commit and tag for the new version.
- Publish updated packages to npm.
5.4 全局工程配置
前面说了, yarn workspaces 可以把通用的配置放在根目录下,之后可以在子项目中引用全局的配置,免去重复配置的麻烦。
这里就以 eslint 的配置为例,我们在根项目中写一份配置文件:
module.exports = { root: true, env: { node: true }, ... }
我们只需要在子项目中进行引用即可,不需要再重复配置。
module.exports = { extends: [ "../../../.eslintrc.js" ] }
其他的配置,如 babel, stylelint 也是一样,这里就不一一赘述。另外再说一点关于 monorepo 项目发布部署的问题,monorepo 项目发布和 multirepos 项目的发布方式还是略有不同,如果你想通过 gitlab ci/cd 进行相关发布部署的话,可以参考这篇文章。
以上就是使用 monorepos 组织代码时关键的一些配置情况,目前只用到了 yarn workspaces 和 lerna 的一些基础功能,随着对 monorepos 的不断深入使用,如果能用上一些更高阶的配置功能,之后也会持续更新到当前文档中。
六 小结
在本篇文章中,我们共同了解了什么是 monorepos 和 multirepos,以及 monorepos 和 multirepos 的优劣对比,并且一起看了如何在一个真实的项目中使用 monorepos。
monorepos 和 multirepos 各有适用的场景,如何选择也需要根据实际场景来决定。有一些参考的指标可以借鉴,如 packages 之间是否有相关性,是否有相互依赖等?
如果大家在阅读文章和实践 monorepos 方案中有任何问题,可在评论区进行讨论。
七 参考资料
- On Monorepos and the Deployment With GitLab CI/CD
- All in one:项目级 monorepo 策略最佳实践
- 使用 MonoRepo 管理前端项目
- Mono Repo vs Multi Repo: Deep Dive Into The Neverending Debate
- What is monorepo?
- Everything you should know about Monorepo
- 基于lerna和yarn workspace的monorepo工作流
- NodeJS:Lerna —— Monorepo 的最佳实践
- 使用Monorepo管理前端微前端项目