精读《Monorepo 的优势》

1. 引言

本周精读的文章是 The many Benefits of Using a Monorepo

现在介绍 Monorepo 的文章很多,可以分为如下几类:直接介绍 Lerna API 的;介绍如何从独立仓库迁移到 Lerna 的;通过举例子说明 Monorepo 重要性的。

本文属于第三种,从 Android 与 IOS 的开发故事说明了 Monorepo 的重要性。

笔者之所以选择这篇文章,不是因为其故事写的好,而是认可这种具有普适性的解决思路。毕竟 Lerna 作为 Monorepo 的实现之一也并不尽善尽美,而不同场景对 Monorepo 依赖的原因、功能也有所不同,所以希望借这篇文章,从理论上解释清楚为什么会产生 Monorepo,以及 Monorepo 可以解决哪些问题,这样在工作遇到问题时,才能想清楚自己要的是什么。

2. 概述

作者的一个项目是 PDF 服务,简称 PSPDFKit,需要同时兼顾 Android 与 IOS 平台,项目的发展经历了如下几个阶段。

初始阶段

在 2011 到 2013 年间,PSPDFKit 仅支持 IOS 平台,但最终项目需要支持 Android,因此开了一个新仓库放置 Android 代码。Android 仓库的代码不仅在 UI 上不同,同时解析 PDF 文档的核心代码也不同,这是因为 IOS 平台上使用内置 PDF 渲染引擎同时做了一些业务拓展,但使用的 OC 代码无法在 Android 使用。

最终新建了两个仓库 PSPDFKit-AndroidCore

仓库 Core 中代码依赖 Android 平台 JNI 的支持,所以并不能实现 Core 一处修改,两处都生效的愿望,而我们又希望两边功能始终兼容,且减少分支过多带来的潜在的冲突,因此花了很久才意识到应该将这两个仓库合并起来。

考虑使用 Monorepo

由于 Android 的整套流程自己控制的,因此总是可以快速修复用户提出的 BUG,然而 IOS 提供的 CGPDF 总会遇上各种问题。所以在 2014 年,我们开启了一个庞大的项目,重写 IOS 的 Core 库。有三中方式可供选择:

  1. 在 IOS 代码中引用 PSPDFKit-Android
  2. PSPDFKit-Android 提取到 Core 仓库中并分别维护。
  3. 将 IOS 与 Android 代码合并到一个仓库中。

经过讨论,最终作者的团队选择了第三种方案,因此目录结构类似如下:

- ios-platform
- android-platform
- core

特例

Web 与后台服务代码一直是一个特例,我们认为这些内容相对独立,所以没有将其代码放置到 Monorepo 中。

直到一年后,开始探索 WebAssembly 时,PSPDFKit-web 模块就出现了,因为可以利用 WebAssembly 将 Core 的代码编译并在 Web 平台使用,因此 Core 仓库与 Web 仓库的关系变得非常紧密,最终,我们将 Web、Server 也都迁移到 Monorepo 中了。

问题

Monorepo 瑕不掩瑜,但作者还是列举了一些缺陷。

由于源码在一起,仓库变更非常常见,存储空间也变得很大,甚至几 GB,CI 测试运行时间也会变长。即便如此,团队中任何人都不想回到 git submodules 多仓库的方式。

3. 精读

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

工程化的最终目的是让业务开发可以 100% 聚焦在业务逻辑上,那么这不仅仅是脚手架、框架需要从自动化、设计上解决的问题,这涉及到仓库管理的设计。

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

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

在前端开发环境中,多 Git Repo,多 Npm 则是这个理想的阻力,它们导致复用要关心版本号,调试需要 Npm Link。

另外对于多仓库的缺点,文中还有一些没有提到的因素,这里一并列举出来:

管理、调试困难

多个 git 仓库管理起来天然是麻烦的。对于功能类似的模块,如果拆成了多个仓库,无论对于多人协作还是独立开发,都需要打开多个仓库页面。

虽然 vscode 通过 Workspaces 解决多仓库管理的问题,但在多人协作的场景下,无法保证每个人的环境配置一致。

对于共用的包通过 Npm 安装,如果不能接受调试编译后的代码,或每次 npm link 一下,就没有办法调试依赖的子包。

分支管理混乱

假如一个仓库提供给 A、B 两个项目用,而 B 项目优先开发了功能 b,无法与 A 项目兼容,此时就要在这个仓库开一个 feature/b 的分支支持这个功能,并且在未来合并到主干同步到项目 A。

一旦需要开分支的组件变多了,且之间出来依赖关联,分支管理复杂度就会呈指数上升。

依赖关系复杂

独立仓库间组件版本号的维护需要手动操作,因为源代码不在一起,所以没有办法整体分析依赖,自动化管理版本号的依赖。

三方依赖版本可能不一致

一个独立的包拥有一套独立的开发环境,难以保证子模块的版本和主项目完全一直,就存在运行结果不一致的风险。

占用总空间大

正常情况下,一个公司的业务项目只有一个主干,多 git repo 的方式浪费了大量存储空间重复安装比如 React 等大型模块,时间久了可能会占用几十 GB 的额外空间,对于没有外接硬盘的同学来说,定期清理不用的项目下 node_modules 也是一件麻烦事。

不利于团队协作

一个大项目可能会用到数百个二方包,不同二方包的维护频率不同,权限不同,仓库位置也不同,主仓库对它们的依赖方式也不同。

一旦其中一个包进行了非正常改动,就会影响到整个项目,而我们精力有限,只盯着主仓库,往往会栽在不起眼的二方包发布上。

所以对于一个非常复杂,又具有技术挑战的大型系统在协作人员多的情况下出现问题的概率非常大,需要通过 Review 制度避免错误的发生,那么将所有相关的源码聚合在一个仓库下,是更好管理的。

理想 monorepo 的设计

参考 Lerna 的规范,以 packages 作为子模块根文件夹,笔者设计一个理想的 monorepo 结构:

.
├── packages
│      ├─ module-a
│      │    ├─ src            # 模块 a 的源码
│      │    └─ package.json   # 自动生成的,仅模块 a 的依赖
│      └─ module-b
│           ├─ src            # 模块 b 的源码
│           └─ package.json   # 自动生成的,仅模块 b 的依赖
├── tsconfig.json             # 配置文件,对整个项目生效
├── .eslintrc                 # 配置文件,对整个项目生效
├── node_modules              # 整个项目只有一个外层 node_modules
└── package.json              # 包含整个项目所有依赖

所有全局配置文件只有一个,这样不会导致 IDE 遇到子文件夹中的配置文件,导致全局配置失效或异常。node_modules 也只有一个,既保证了项目依赖的一致性,又避免了依赖被重复安装,节省空间的同时还提高了安装速度。

兄弟模块之间通过模块 package.json 定义的 name 相互引用,保证模块之间的独立性,但又不需要真正发布或安装这个模块,通过 tsconfig.jsonpathswebpackalias 共同实现虚拟模块路径的效果。

再结合 Lerna 根据联动发布功能,使每个子模块都可以独立发布。

4. 总结

Lerna 是业界知名度最高的 Monorepo 管理工具,功能完整。但由于通用性要求非常高,需要支持任意项目间 Monorepo 的组合,因此在 packages 文件夹下的配置文件还是与独立仓库保持一致,这样在 TS 环境下会造成配置截断的问题。同时包之间的引用也通过更通用的 symlink 完成,这导致了还是要在子模块目录存在 node_modules 文件夹,而且效果依赖项目初始化命令。

如果加一些限定条件,比如基于 Webpack + Typescript 环境的 Monorepo,可以换一套思路,利用这些工具自身运行时功能,减少更多模版代码或配置文件,进一步提升 Monorepo 的效果。

对于别名映射,对 symlinkalias 进行对比:

  • symlink: 更通用,适合任何构建器。但需要初始化,且在每个关联模块下新增 node_modules 文件夹。
  • alias: 限定构建器。但不需要初始化,不新增文件夹,甚至可以运行时动态修改别名配置。

可见如果限定了构建器,别名映射可以做得更轻量,且无需初始化。

今天的问题是,你的项目需要使用 Monorepo 吗?你对 Monorepo 有其他要求吗?

讨论地址是:精读《Monorepo 的优势》 · Issue #151 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

special Sponsors

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 不用你刻意的介绍,我也明白自己的角色:爱上你,是我人生的一个意外,而这样的场景,是迟早都会发生的必然…… ...
    彤莲阅读 287评论 1 1
  • 星期五的下午,许老师教给我们做书签。 我们带着彩纸、剪刀、胶棒和白纸,我们一共做了两个书签,第一个...
    黄麒臻阅读 409评论 0 0
  • 2018.12.3博客 前期准备 Windows下安装gitbash之后打开客户端 Linux或者Mac直接打开终...
    leo_luo阅读 121评论 0 0