本周精读的文章是 The many Benefits of Using a Monorepo。html
如今介绍 Monorepo 的文章不少,能够分为以下几类:直接介绍 Lerna API 的;介绍如何从独立仓库迁移到 Lerna 的;经过举例子说明 Monorepo 重要性的。前端
本文属于第三种,从 Android 与 IOS 的开发故事说明了 Monorepo 的重要性。node
笔者之因此选择这篇文章,不是由于其故事写的好,而是承认这种具备普适性的解决思路。毕竟 Lerna 做为 Monorepo 的实现之一也并不尽善尽美,而不一样场景对 Monorepo 依赖的缘由、功能也有所不一样,因此但愿借这篇文章,从理论上解释清楚为何会产生 Monorepo,以及 Monorepo 能够解决哪些问题,这样在工做遇到问题时,才能想清楚本身要的是什么。android
做者的一个项目是 PDF 服务,简称 PSPDFKit,须要同时兼顾 Android 与 IOS 平台,项目的发展经历了以下几个阶段。webpack
在 2011 到 2013 年间,PSPDFKit 仅支持 IOS 平台,但最终项目须要支持 Android,所以开了一个新仓库放置 Android 代码。Android 仓库的代码不只在 UI 上不一样,同时解析 PDF 文档的核心代码也不一样,这是由于 IOS 平台上使用内置 PDF 渲染引擎同时作了一些业务拓展,但使用的 OC 代码没法在 Android 使用。ios
最终新建了两个仓库 PSPDFKit-Android
与 Core
。git
仓库 Core 中代码依赖 Android 平台 JNI 的支持,因此并不能实现 Core 一处修改,两处都生效的愿望,而咱们又但愿两边功能始终兼容,且减小分支过多带来的潜在的冲突,所以花了好久才意识到应该将这两个仓库合并起来。github
因为 Android 的整套流程本身控制的,所以老是能够快速修复用户提出的 BUG,然而 IOS 提供的 CGPDF 总会赶上各类问题。因此在 2014 年,咱们开启了一个庞大的项目,重写 IOS 的 Core 库。有三中方式可供选择:web
PSPDFKit-Android
。PSPDFKit-Android
提取到 Core
仓库中并分别维护。通过讨论,最终做者的团队选择了第三种方案,所以目录结构相似以下:npm
- ios-platform - android-platform - core
Web 与后台服务代码一直是一个特例,咱们认为这些内容相对独立,因此没有将其代码放置到 Monorepo 中。
直到一年后,开始探索 WebAssembly 时,PSPDFKit-web 模块就出现了,由于能够利用 WebAssembly 将 Core 的代码编译并在 Web 平台使用,所以 Core 仓库与 Web 仓库的关系变得很是紧密,最终,咱们将 Web、Server 也都迁移到 Monorepo 中了。
Monorepo 瑕不掩瑜,但做者仍是列举了一些缺陷。
因为源码在一块儿,仓库变动很是常见,存储空间也变得很大,甚至几 GB,CI 测试运行时间也会变长。即使如此,团队中任何人都不想回到 git submodules 多仓库的方式。
总的来讲,虽然拆分子仓库、拆分子 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 制度避免错误的发生,那么将全部相关的源码聚合在一个仓库下,是更好管理的。
参考 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 根据联动发布功能,使每一个子模块均可以独立发布。
Lerna 是业界知名度最高的 Monorepo 管理工具,功能完整。但因为通用性要求很是高,须要支持任意项目间 Monorepo 的组合,所以在 packages
文件夹下的配置文件仍是与独立仓库保持一致,这样在 TS 环境下会形成配置截断的问题。同时包之间的引用也经过更通用的 symlink 完成,这致使了仍是要在子模块目录存在 node_modules
文件夹,并且效果依赖项目初始化命令。
若是加一些限定条件,好比基于 Webpack + Typescript 环境的 Monorepo,能够换一套思路,利用这些工具自身运行时功能,减小更多模版代码或配置文件,进一步提高 Monorepo 的效果。
对于别名映射,对 symlink 与 alias 进行对比:
node_modules
文件夹。可见若是限定了构建器,别名映射能够作得更轻量,且无需初始化。
今天的问题是,你的项目须要使用 Monorepo 吗?你对 Monorepo 有其余要求吗?
讨论地址是: 精读《Monorepo 的优点》 · Issue #151 · dt-fe/weekly
若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
<img width=200 src="https://img.alicdn.com/tfs/TB...;>
special Sponsors
版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证)