实践 lerna monorepo

本文最后更新于:2021年6月7日 凌晨

历史

  • 上古时期,前端没有工程化的概念可言,复用代码也不过是将某些 css、js 代码片段保存到笔记,需要时复制到项目中,仅此而已。参考:55 个提高你 CSS 开发效率的必备片段,或是 jquery 库
  • 莽荒时代,前端出现了 nodejs 和 npm,于是一大批通用代码被发布到了 npm 平台,可以在项目中简单配置即可使用通用的库,任何人都可以简单的将代码发布到 npm。参考:lodash
  • 现代,由于前端项目的复杂度逐渐上升,所以出现了 monorepo 工具以更简单的复用代码。例如层出不穷的 monorepo 支持工具 lerna@microsoft/rushyarn 2pnpmnpm 7

自去年 10 月开始,吾辈使用 lerna 重构个人和公司的项目,以应对愈加复杂的前端项目。

为什么需要 monorepo?

借用一下 lerna 官网的简介:

将大型代码仓库分割成多个独立版本化的 软件包(package)对于代码共享来说非常有用。但是,如果某些更改 跨越了多个代码仓库的话将变得很 麻烦 并且难以跟踪,并且, 跨越多个代码仓库的测试将迅速变得非常复杂。

为了解决这些(以及许多其它)问题,某些项目会将 代码仓库分割成多个软件包(package),并将每个软件包存放到独立的代码仓库中。但是,例如 Babel、 React、Angular、Ember、Meteor、Jest 等项目以及许多其他项目则是在 一个代码仓库中包含了多个软件包(package)并进行开发。

你可能会认为除了大型开源项目之外,monorepo 对于小型项目和生产环境的业务项目没有太多价值。但这是错的,前者我在微型工具库 liuli-util 上进行了实践,确定了它对于维护和使用确实有帮助。而后者,甚至出现了专门为业务项目的 monorepo 工具 @microsoft/rush,微软在 rushstack 项目中大规模使用了它。

为什么选择 lerna?

那么,有了这么多 monorepo 工具,为什么我们选择 lerna?

其实,除了 lerna 与 @microsoft/rush 之外,其它竞争对手都是包管理器,仅仅只是提供了 workspace 的工作空间,并未提供更高级功能。

lerna 和 @microsoft/rush 的 npm 趋势对比参考: https://www.npmtrends.com/[email protected]/rush

下图是一个对比

对比项 lerna @microsoft/rush
star 26,824 2,392
周下载 1,155,241 100,386
使用者 知名开源项目 微软系产品

就吾辈的实际使用体验而言,相比于 lerna,rush 默认包含了更多的东西,而非通过组合一系列可选的工具支持,这增长了相当的门槛。

下面是吾辈对其的一些认知过程

  • rush.js 是真的感觉很【专业】,限定了很多很多东西
  • https://rushjs.io/pages/maintainer/setup_policies/
  • 像是这里,通过 allowedPackagesPolicy 的方式对 team 中所有开发人员都可以直接引入新的 npm 包做出了限制
  • 唉,rush 比 lerna 复杂多了,做了很多很多的预定义的事情,这就意味着,它对项目维护者(而非开发者)的要求更高
  • 和 ide 没完全集成真痛苦.JPG
  • 吾辈总算明白这些配置为什么是【推荐配置】而不是【默认配置】了,引发的错误太多了(毕竟 npm 包很多并不规范)
  • rush monorepo 的一个问题是,某些包总喜欢强制指定依赖包的特定版本(例如 react-scripts),而 rush 总是“聪明”的仅安装最新的,导致添加的项目莫名其妙的炸掉
  • 吾辈的锅,它在最后给了方法 https://rushjs.io/pages/advanced/installation_variants/
  • 但一整个进阶主题都是在处理这个问题。。。
  • 算了,吾辈放弃了,rush + pnpm 感觉上维护配置成本太高了,滚回 lerna + yarn 了

rush 在功能、目标和文档方面更好,但现阶段而言还是 lerna 更成熟。

lerna 是什么?

简而言之,Lerna 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目。可以在一个项目中创建多个模块(基本上模块也可以认为是一个项目),并且可以在本地的模块之间互相关联。

lerna 项目的基本结构如下

生产项目

  • 根目录
    • apps: 生产项目
      • app-1
      • app-2
    • libs: 通用模块
      • lib-1
      • lib-2
    • package.json
    • lerna.json

开源库

  • 根目录
    • libs: 模块根目录
      • lib-1
      • lib-2
    • package.json
    • lerna.json

目录的名字灵感来源于 rushstack

使用 lerna 的优点

其中部分优点是 monorepo 固有的优势,但也有 lerna 独有的功能。

  • 更容易抽离公共代码: 模块之间可以互相引用并且即时生效
  • 更容易统一
    • 项目配置: tsconfig.json/prettier.json/git hooks
    • 管理和发布一系列包: lerna publish
    • 修改依赖立刻生效: lerna bootstrap
    • 依赖版本: 和默认合并不同版本的依赖
    • 文档生成和合并: fliegdoc
    • 代码风格: prettier/git hooks
    • 在一个模块运行另一个模块的命令: lerna run <cmd> --scope <pkg>
    • 打包工具和流程: 封装更适合项目的打包 cli

目前稍微大点的开源项目不是已经转为了 lerna monorepo,就是已经在转换的路上(很像最近流行的使用 typescript 重构库)。包括但不限于以下这些:

1614158368615

吾辈目前使用的笔记工具 Joplin 也在去年使用 lerna 重构了,参考:Lerna migration

其他

根据依赖图并行运行 npm 命令

1
lerna run <npm script> --include-dependencies --stream

参考: –stream–include-dependencies

git 规范

在单体项目中,只需要简单的分为 dev/master 即可开发,但在 monorepo 中,可能存在多个 apps,这种时候,简单的 dev/master 策略便不太好用了,主要原因是

  • dev 包含了所有的开发阶段的代码,所以合并的时候会合并到不希望合并的其他 apps 的修改
  • 提交记录看不出来是哪个分支的

吾辈的个人项目 joplin-utils 就面临这种问题。

下面是吾辈的一些想法,目前还正在实践中

  • 规范化分支
    • master: 生产环境分支,任何时候都应该是可部署的
    • dev: 指代正在开发环境进行测试的功能
    • feat-*: 正在开发的功能分支
    • fix-*: 修复线上 bug 的分支
  • 规范化流程
    • 分支一定是从 master 拉取
    • 分支一定是合并到 dev 测试
    • 分支一定是合并到 master 部署生产
  • 规范化提交信息
    • 基本采用 commitlint 控制提交格式,包括 类型(模块): 提交说明

模块规范

  • 目录
    • apps: 最终用户可以使用的程序或 cli
    • libs: 一些依赖项,根据需要发布到 npm
    • examples: 一些示例项目
  • scripts
    • setup: 项目初始化的一些脚本
    • dev: 开发阶段运行的脚本
    • build: 打包代码
    • docs:dev: 启动本地文档预览服务
    • docs:build: 将文档捆绑为静态文件
    • docs:deploy: 部署文档到线上

rollup 捆绑 monorepo 仍然存在错误

目前 rollup + node-resolve 插件捆绑本地依赖时仍然存在一些问题,参考:https://github.com/rollup/plugins/issues/743,目前的替代方案是 esbuild

有时候会始终无法安装正确的版本

例如在 package.json 中声明了依赖

1
2
3
{
"rollup": "^2.51.0" // 实际安装的可能是 2.50.6
}

1622999313766

总结

使用 lerna 虽然会增加一些复杂度,但带来的优点仍然是超过缺点的。

吾辈之所以相信 monorepo 会成为主流的原因是后端已经使用了这么多年的 maven/gradle,如果真的有什么问题,那不会直到现在还在使用。