本文首发于 vivo互联网技术
作者:孔垂亮

对于维护过多个package的同学来说,都会遇到一个选择题,这些package是放在一个仓库里维护还是放在多个仓库里单独维护,本文通过一个示例讲述了如何基于Lerna管理多个package,并和其它工具整合,打造高效、完美的工作流,最终形成一个最佳实践

背景

最近在工作中接触到一个项目,这个项目是维护一套 CLI,发到 npm 上供开发者使用。先看一张图:

基于 Lerna 管理 packages 的 Monorepo 项目最佳实践

项目仓库中的根目录上就三个子模块的文件夹,分别对应三个 package,在熟悉了构建和发布流程后,有点傻了。工作流程如图中所示:

  1. 使用webpack、babel和uglifyjs把 pkg-a 的 src 编译到 dist

  2. 使用webpack、babel和uglifyjs把 pkg-b 的 src 编译到 dist

  3. 使用webpack、babel和uglifyjs把 pkg-main 的 src 编译到 dist

  4. 最后使用拷贝文件的方式,把pkg-main、pkg-a、pkg-b中编译后的文件组装到 pkg-npm 中,最终用于发布到 npm 上去。

痛点

  1. 不好调试。因为最终的包是通过文件拷贝的方式组装到一起的,并且都是压缩过的,无法组建一个自上到下的调试流程(实际工作中只能加log,然后重新把包编译组装一遍看效果)

  2. 包的依赖关系不清晰。pkg-a、pkg-b索性没有版本管理,更像是源码级别的,但逻辑又比较独立。pkg-main中的package.json最终会拷贝到 pkg-npm 中,但又依赖pkg-a、pkg-b中的某些包,所以要把pkg-a、pkg-b中的依赖合并到pkg-main中。pkg-main和pkg-npm的package.json耦合在一起,导致一些本来是工程的开发依赖也会发布到 npm 上去,变成pkg-npm 的依赖包。

  3. 依赖的包冗余。可以看到,pkg-a、pkg-b、pkg-main要分别编译,都依赖了babel、webpack等,要分别 cd 到各个目录安装依赖。

  4. 发布需要手动修改版本号。 因为最终只发布了一个包,但实际逻辑要求这个包即要全局安装又要本地安装,业务没有拆开,导致要安装两遍。耦合一起,即便使用 npm link 也会导致调试困难,

  5. 发版没有 CHANGELOG.md。 因为pkg-a、pkg-b都没有真正管理版本,所以也没有完善的CHANGELOG来记录自上个版本发布已来的变动。

整个项目像是一个没有被管理起来的 Monorepo。那什么又是 Monorepo 呢?

Monorepo vs Multirepo

Monorepo 的全称是 monolithic repository,即单体式仓库,与之对应的是 Multirepo(multiple repository),这里的“单”和“多”是指每个仓库中所管理的模块数量。

Multirepo 是比较传统的做法,即每一个 package 都单独用一个仓库来进行管理。例如:Rollup, ...

Monorep 是把所有相关的 package 都放在一个仓库里进行管理,每个 package 独立发布。例如:React, Angular, Babel, Jest, Umijs, Vue ...

一图胜千言:

基于 Lerna 管理 packages 的 Monorepo 项目最佳实践

当然到底哪一种管理方式更好,仁者见仁,智者见智。前者允许多元化发展(各项目可以有自己的构建工具、依赖管理策略、单元测试方法),后者希望集中管理,减少项目间的差异带来的沟通成本。

虽然拆分子仓库、拆分子 npm 包是进行项目隔离的天然方案,但当仓库内容出现关联时,没有任何一种调试方式比源码放在一起更高效。

结合我们项目的实际场景和业务需要,天然的 MonoRepo ! 因为工程化的最终目的是让业务开发可以 100% 聚焦在业务逻辑上,那么这不仅仅是脚手架、框架需要从自动化、设计上解决的问题,这涉及到仓库管理的设计。

一个理想的开发环境可以抽象成这样:

“只关心业务代码,可以直接跨业务复用而不关心复用方式,调试时所有代码都在源码中。”

在前端开发环境中,多 Git Repo,多 npm 则是这个理想的阻力,它们导致复用要关心版本号,调试需要 npm link。而这些是 MonoRepo 最大的优势。

上图中提到的利用相关工具就是今天的主角 Lerna ! Lerna是业界知名度最高的 Monorepo 管理工具,功能完整。