monorepo 技术探索

技术 11月 10, 2020

引言

笔者最早接触 monorepo 的概念是在去年年中的时候,当时组内有需求写一个组件库,于是在设计之初有人提议使用 Lerna 来管理项目,便有了第一次与 monorepo 接触的契机,但后来发现没什么必要,便没有使用,但在其他应用有用到,并且在此之后,由于开源社区使用 monorepo 的项目越来越多,如 vue3.0 使用的 yarn workspaces、slidev 使用的 pnpm workspaces,笔者对这项技术的好奇心越来越高,并且客观事实证明 monorepo 对复杂项目的管理有很多的帮助,本文主要目的是为了建立一些基本认知

如有错误,还请不吝斧正。

一致性

为了方便理解并防止分歧,有必要对本文内一些可能混淆的概念进行强调并释义。
请注意,释义仅对本文负责,并不一定适用于本文以外的内容,请注意甄别。

名词表

名词名称 释义
项目 project,抽象概念,在本文中可以将一个 package.json 对应的一个工程视为本文中的“项目”,或者也可以理解为一个单独的库。
根项目 负责统一管理项目的项目,公共配置一般在根项目配置
子项目 被管理的项目
存储库 repository,由你的版本管理工具(git,SVN)所限定的一个工程整体。
包管理工具 package manager,如 npm、yarn、pnpm、jspm 这类的依赖管理工具。

假设

为了方便理解,使得读者情感带入,我们需要先假设场景。
假设你现在是公司几个基础项目的维护者,其中有几个项目是这样的,

  • 一个基于 webpack 封装的打包工具
  • 一个基于该打包工具的快速生成项目的 CLI 工具
  • 几个内部使用的 webpack 插件

你可能会相对频繁面临如下几个问题:

  • 对三个项目的公共依赖需要进行统一的升级
  • 其中一个项目更新,另外几个项目连锁反应也需要更新

那么该如何更好地解决呢?
monorepo 并不是银弹,但在适合的场景它可以解决不少的问题。

monorepo 介绍

What

monorepo (“mono” 的意思是 “单一”,“repo” 是 “repository” 的缩写)是一种软件开发策略,具体表现就是多个项目存储在一个存储库中。这种开发策略其实已经存在了许久,在前端流行之前便已经在存在许久,只不过在近几年才被明确定义。同时也属于版本管理控制的概念之一。
它与传统的一个项目一个存储库的策略背道而驰,其目的往往是为了统一管理项目的公共部分内容、拉近项目之间的依赖关系、缩短工作流。

Why

归功于前端技术的发展,前端应用的复杂度也随之上升,传统软件开发领域的策略也开始在前端流行,monorepo 便是其中之一。其目的归根结底是为了更好管理项目,提升效率,当然有利有弊,需要开发者谨慎选择。

什么场景会需要 monorepo?

Be water, my friend.

其实很难说清楚什么场景需要 monorepo,笔者更想把这个问题变成“什么场景会需要将多个项目存储在一个存储库里?”来解答:

  1. 项目有较强的关联性
  2. 项目中存在不少重复的内容
  3. 你想拉近项目之间的距离,而不是让他们之间的关系遥不可及。

基本上来说,当你在开发工作中,总是需要 **来回切换项目来完成“”件事 **的时候可能就需要 monorepo 来解决一些问题。

monorepo 就一定需要专门的工具(库)才能实现吗?

答案当然是否定的,严格意义上说,只要将多个项目放在一个存储库里就算 monorepo。
但是现在主流的 monorepo 所承担的责任并不只是存储的问题,还可以承担比如依赖管理,增量构建等一系列工程化的功能,已经成为工程化技术中非常有价值的一块领域,所以有时你为了实现某个特殊的功能不得不借助社区的力量,或者站在大佬的肩膀上。

利弊

益处

  • 公共事务统一处理:比如 eslint 配置、tsconfig 配置等。
  • 及时的依赖关系:通常项目和项目之间需要通过 node_modules 来联系,但是在不少 monorepo 解决方案中,你可以直接本地及时响应有依赖的变动。

弊端

  • 项目结构复杂:如果不注意保持项目结构整洁,很容易会导致项目结构十分复杂。
  • 学习成本较高:不管是基于很重复杂程度的解决方案都会带来一定的心智成本,耦合程度越高,心智成本越高。

社区方案

不同的场景,有着不同的需求,所以下面介绍的方案并不一定适用所有场景。

Workspaces

首先介绍的是主流包管理工具的 workspaces 特性,借用 Workspaces in Yarn 里对 workspaces 的介绍:“workspaces 是一种可以将多个由 package.json 为单位的项目统一在一个也以 package.json 为单位的项目里面,他们的依赖可以被统一管理。”

包管理工具利用其对于依赖管理得天独厚的优势,可以直接无痛的对项目依赖作出干涉,从而实现了依赖层面的统一管理。

值得注意的是,包管理工具的 workspaces 特性已经成为其他更上层的 monorepo 工具的基础设施之一。
所以 workspaces 不仅是 monorepo 的轻量解决方案之一,同时也是目前其他解决方案的基础,也是至关重要的环节之一,workspaces 自身的质量决定了上层建筑的好使程度。

接下来,简单罗列一下各个包管理工具的 workspaces 特性和注意点(主观观点:从上至下,由差到好)。

Demo 项目地址:https://github.com/yingpengsha/workspace-research (里面有更详细的笔记哦)

包管理工具 准备工作 依赖统一管理 添加本地子项目为依赖 注意点
yarn2 配置 workspaces 字段 ✅ 支持 ⚠️ 支持,但使用的是 yarn2 的 workspace 协议 功能很多,基本可以和大型的 monorepo 解决方案媲美。
但 yarn2 自身可用性和稳定性尚待考验,请谨慎选择。
yarn 配置 workspaces 字段 ⚠️ 默认不支持(需要关闭 nohoist ⚠️ 支持,但第一次添加依赖需要指定版本号 不支持子项目为依赖
yarn 的 bug 太多了,官方不怎么维护。
如果没有太多需求需要实现,可以一试。
npm 配置 workspaces 字段 ⚠️ 支持(子项目需要通过专门的指令) ✅ 支持 起步较晚,需要很新版本的 npm
但是功能稳定且实用,值得一试
pnpm 添加 pnpm-workspace.yaml 文件 ✅ 支持 ⚠️ 支持,但使用的是 pnpm 的 workspace 协议 好使 👍

小结

将 workspaces 作为 monorepo 的解决方案在大部分的场景其实已经够用,并且心智成本也不高,只需要统一包管理工具即可,所以笔者极力推荐读者先尝试只用workspaces 做为解决方案,如果不行也可以试试自己用脚本拓展一下,再不行再使用其他工具。

接下来介绍的都是一些成本稍高,但覆盖链路更长的社区解决方案。

Lerna

大名鼎鼎的 Lerna,成熟且被广泛使用的 monorepo 解决方案,并且可以结合 yarn workspaces 来进行管理,如此一来,它不仅支持依赖的共享和本地的互相依赖,它还支持更智能的版本控制及更方便的发布操作。

抛去和 workspaces 重合的依赖管理部分,我们简单过一下它其他值得关注的功能:

更方便的项目管理

$ lerna import <pathToRepo> --dist=<targetPackageName> # 将已经存在的项目添加到子项目中
$ lerna create <packageName> # 快速添加子项目

通过 git 检查项目变动

$ lerna changed # 检查自上次发布以来有哪些包有更新
$ lerna diff # 查看自上次发布以来的所有包或者指定包的 git diff 变化

可以按照拓扑顺序执行 build 指令

$ lerna run --stream --sort build

更加智能的发布脚本

$ lerna publish # 发布自上次发布以来有更新的包,包含了lerna version的工作。
$ lerna publish form-git # 显示发布当前提交中标记的包,类似于先独立执行lerna version后,再执行此命令进行发布。
$ lerna publish from-package # 显示发布npm registry中不存在的最新版本的包。

小结

Lerna 在各种项目验证过后,证明了它是一个可以扛得起长链路和复杂项目 monorepo 解决方案,如果你的项目需要较长的工具链去完成更多的工作则可以试试 Lerna。
但由于 Lerna 往往与 yarn workspaces 一起使用的问题,yarn 的一些毛病可能会传染到 Lerna 身上:

  1. 首先与 yarn workspaces 结合会导致命令太多,如 yarn <command>yarn workspace <command>lerna <command>
  2. 幻影依赖,这实际上是因为 yarn 或者 npm 说自身导致的,因为其将依赖在 node_modules 铺平的策略实际上与 nodejs 自身模块查找策略之间产生了一个灰色地带,从而导致你可以在代码中使用在 package.json 并没有显式的安装依赖,从而导致不一致的错误。
  3. yarn.lock 冲突问题
  4. ...yarn 的众多 bug

Rush

Rush 是由微软开发的 monorepo 解决方案,它可以结合 pnpm 来使用,所以解决了很多 yarn 和 npm 的问题,同时具备着更强大的链路设施。

丰富的管理策略

setup plicies
rush.schema.json

统一的指令

rush 一把梭即可

增量构建

$ rush build # 按照变动情况增量构建,当然会将其依赖的变动也重新构建一边
$ rush rebuild # 完整干净的重新构建一遍

开放接口,自定义实现更复杂的功能

The "rush-lib" API

小结

Rush 的功能很多,自定义能力也很强,笔者没有太过深入的了解全部功能,但毫无以为是,对于超大型的 monorepo 项目 rush 因为其丰富的功能和 API 完全可以胜任。

总结

本文主要介绍笔者对于 monorepo 的思考,各个包管理工具的 Workspaces 功能以及 Lerna 和 Rush。
Workspaces 其实属于前端的 monorepo 解决方案中不可取代的一环,所以你想使用 monorepo 你需要好好的去研究一下各个包管理工具和其 workspaces 的优缺点。
在前文 monorepo 的场景笔者之前只考虑了什么样的项目需要 monorepo 这种策略,但实际上还有一种角度是直接一个公司单位的代码都用 monorepo 来管理,典型的应该就是 Google,所以 monorepo 的概念和一些设定其实在不同场景下的定义已经没这么具体了。

如果你还想继续深入的研究,推荐阅读以下本文的参考文献。
Be water, my friend.

参考

Pengsha Ying

逝者如斯,故不舍昼夜