精读《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-Android
与 Core
。
仓库 Core 中代码依赖 Android 平台 JNI 的支持,所以并不能实现 Core 一处修改,两处都生效的愿望,而我们又希望两边功能始终兼容,且减少分支过多带来的潜在的冲突,因此花了很久才意识到应该将这两个仓库合并起来。
考虑使用 Monorepo
由于 Android 的整套流程自己控制的,因此总是可以快速修复用户提出的 BUG,然而 IOS 提供的 CGPDF 总会遇上各种问题。所以在 2014 年,我们开启了一个庞大的项目,重写 IOS 的 Core 库。有三中方式可供选择:
- 在 IOS 代码中引用
PSPDFKit-Android
。 - 将
PSPDFKit-Android
提取到Core
仓库中并分别维护。 - 将 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.json
的 paths
与 webpack
的 alias
共同实现虚拟模块路径的效果。
再结合 Lerna 根据联动发布功能,使每个子模块都可以独立发布。
4. 总结
Lerna 是业界知名度最高的 Monorepo 管理工具,功能完整。但由于通用性要求非常高,需要支持任意项目间 Monorepo 的组合,因此在 packages
文件夹下的配置文件还是与独立仓库保持一致,这样在 TS 环境下会造成配置截断的问题。同时包之间的引用也通过更通用的 symlink 完成,这导致了还是要在子模块目录存在 node_modules
文件夹,而且效果依赖项目初始化命令。
如果加一些限定条件,比如基于 Webpack + Typescript 环境的 Monorepo,可以换一套思路,利用这些工具自身运行时功能,减少更多模版代码或配置文件,进一步提升 Monorepo 的效果。
对于别名映射,对 symlink 与 alias 进行对比:
- symlink: 更通用,适合任何构建器。但需要初始化,且在每个关联模块下新增
node_modules
文件夹。 - alias: 限定构建器。但不需要初始化,不新增文件夹,甚至可以运行时动态修改别名配置。
可见如果限定了构建器,别名映射可以做得更轻量,且无需初始化。
今天的问题是,你的项目需要使用 Monorepo 吗?你对 Monorepo 有其他要求吗?
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
special Sponsors
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)
精读《Monorepo 的优势》的更多相关文章
- 精读《V8 引擎 Lazy Parsing》
1. 引言 本周精读的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎为了优化性能,做了怎样的尝试吧! 这篇文章介绍的优化技术叫 preparser,是通过跳过不必要函数编译的方式优化性 ...
- 深入浏览器工作原理和JS引擎(V8引擎为例)
浏览器工作原理和JS引擎 1.浏览器工作原理 在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?以及JavaScript代码在浏览器中是如何被执行的? 大概流程可观察以下图: 首先,用户在浏览器搜 ...
- [翻译] V8引擎的解析
原文:Parsing in V8 explained 本文档介绍了 V8 引擎是如何解析 JavaScript 源代码的,以及我们将改进它的计划. 动机 我们有个解析器和一个更快的预解析器(~2x), ...
- 一文搞懂V8引擎的垃圾回收
引言 作为目前最流行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在浏览器和Nodejs这两大宿主环境中,也是因为背后有强大的V ...
- Chrome V8引擎系列随笔 (1):Math.Random()函数概览
先让大家来看一幅图,这幅图是V8引擎4.7版本和4.9版本Math.Random()函数的值的分布图,我可以这么理解 .从下图中,也许你会认为这是个二维码?其实这幅图告诉我们一个道理,第二张图的点的分 ...
- (译)V8引擎介绍
V8是什么? V8是谷歌在德国研发中心开发的一个JavaScript引擎.开源并且用C++实现.可以用于运行于客户端和服务端的Javascript程序. V8设计的初衷是为了提高浏览器上JavaScr ...
- 浅谈Chrome V8引擎中的垃圾回收机制
垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...
- V8引擎嵌入指南
如果已读过V8编程入门那你已经熟悉了如句柄(handle).作用域(scope)和上下文(context)之类的关键概念,以及如何将V8引擎作为一个独立的虚拟机来使用.本文将进一步讨论这些概念,并介绍 ...
- 浅谈V8引擎中的垃圾回收机制
最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...
- 深入出不来nodejs源码-V8引擎初探
原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...
随机推荐
- 通信接口是webservice快还是scoket快解决方案
通信接口是webservice快还是scoket快webservice和scoket都可以做为通信接口,一个走HTTP访问,一个走TCP协议访问 问1:通讯速度是webservice快还是scoket ...
- Bootstrap 中文文档教程
Bootstrap 中文文档教程 Bootstrap 中文文档教程 全局样式和grid布局—Bootstrap中文使用指南 全局样式1.要求html5文档类型 Bootstrap使用的css属性和ht ...
- layui静态表格固定td宽度,table固定td宽度
正在做一个项目,要求数据表的列是不固定的,有可能是有10列,有可能是20列,第一列宽度要固定,然后我怎么设置都没有用, 这个问题困扰了我三天,后来终于百度到了, 这个博客: https://www.c ...
- 更改pip源地址为阿里云
1.在用户名目录创建pip目录,在pip目录下创建pip.ini. 2.pip.ini中输入: [global] index-url = http://mirrors.aliyun.com/pypi/ ...
- Cloud Computing——Everything as a Service
service 分类 有Iaas, Paas, SaaS HDFS 总结☞: HDFS应付不了的场景 无法低时延 小文件存储存在空间利用率问题 文件不可修改 三副本有什么作用 防止单机故障,提高可用性 ...
- WebView:是应用程序打开web网页的UI控件后端
public class WebViewActivity extends Activity { private WebView webView; @Override protected void on ...
- vue基于element-ui的三级CheckBox复选框
最近vue项目需要用到三级CheckBox复选框,需要实现全选反选不确定三种状态.但是element-ui table只支持多选行,并不能支持三级及以上的多选,所以写了这篇技术博文供以后学习使用. 效 ...
- CentOS关闭系统不必要的端口
注:以下所有操作均在CentOS 7.2 x86_64位系统下完成. 1)首先查看当前系统开放的端口号: # netstat -tlnup Active Internet connections (o ...
- 第五周学习总结&实验报告(三)
第五周学习总结&实验报告(三) 这一周又学习了新的知识点--继承. 一.继承的基本概念是: *定义一个类,在接下来所定义的类里面如果定义的属性与第一个类里面所拥有的属性一样,那么我们在此就不需 ...
- pdf.js浏览中文pdf乱码的问题解决
由于项目中需要支持移动设备在线浏览pdf,苹果还好,天生支持,但是安卓中就不行了,需要第三方组件的支持. 这里就找到了pdf.js,由于pdf数据太多,开始的时候没法一一测试,所以随便测试打开了几篇没 ...