1. monorepo
N1: 什么是 monorepo ?为什么会产生?
Monorepo 是一种项目管理方式,就是把多个项目放在一个仓库里面, 解决 MultiRepo 下出现的各种痛点, 提升研发效率和工程质量
Monorepo 的意思是在版本控制系统的单个代码库里包含了许多项目的代码。这些项目虽然有可能是相关的,但通常在逻辑上是独立的,并由不同的团队维护。
有些公司将所有代码存储在一个代码库中,由所有人共享,因此 Monorepos 可以非常大。例如,理论上谷歌拥有有史以来最大的代码库,每天有成百上千次提交,整个代码库超过 80 TB。其他已知运营大型单一代码库的公司还有微软、Facebook 和 Twitter。
之前传统的方式 MultiRepo 当中,每个项目都对应单独的一个代码仓库。我之前也是用这种方式开发的,是真真切切地感受到了这种方式带来的诸多弊端。
代码复用
:在维护多个项目的时候,有一些逻辑很有可能会被多次用到,比如一些基础的组件、工具函数,或者一些配置,你可能会想: 要不把代码直接 copy 过来,多省事儿!但有个问题是,如果这些代码出现 bug、或者需要做一些调整的时候,就得修改多份,维护成本越来越高。
那如何来解决这个问题呢?比较好的方式是将公共的逻辑代码抽取出来,作为一个 npm 包进行发布,一旦需要改动,只需要改动一份代码,然后 publish 就行了。
但这真的就完美解决了么?我举个例子,比如你引入了 1.1.0 版本的 A 包,某个工具函数出现问题了,你需要做这些事情:
-
去修改一个工具函数的代码
-
发布1.1.1版本的新包
-
项目中安装新版本的 A。
可能只是改了一行代码,需要走这么多流程。然而开发阶段是很难保证不出 bug 的,如果有个按钮需要改个样式,又需要把上面的流程重新走一遍......停下来想想,这些重复的步骤真的是必须的吗?我们只是想复用一下代码,为什么每次修改代码都这么复杂?
上述的问题其实是 MultiRepo普遍存在的问题,因为不同的仓库工作区的割裂,导致复用代码的成本很高,开发调试的流程繁琐,甚至在基础库频繁改动的情况下让人感到很抓狂,体验很差。
版本管理
:在 MultiRepo 的开发方式下,依赖包的版本管理有时候是一个特别玄学的问题。比如说刚开始一个工具包版本是 v1.0.0,有诸多项目都依赖于这个工具包,但在某个时刻,这个工具包发了一个 break change 版本,和原来版本的 API 完全不兼容。而事实上有些项目并没有升级这个依赖,导致一些莫名的报错。
当项目多了之后,很容易出现这种依赖更新不及时的情况。这又是一个痛点。
项目基建
:由于在 MultiRepo 当中,各个项目的工作流是割裂的,因此每个项目需要单独配置开发环境、配置 CI 流程、配置部署发布流程等等,甚至每个项目都有自己单独的一套脚手架工具。
其实,很容易发现这些项目里的很多基建的逻辑都是重复的,如果是 10 个项目,就需要维护 10 份基建的流程,逻辑重复不说,各个项目间存在构建、部署和发布的规范不能统一的情况,这样维护起来就更加麻烦了。
N2: monorepo 的作用及解决的问题?
乍一看,单一代码库和多代码库之间的选择似乎不是什么大问题,但这是一个会深刻影响到公司开发流程的决定。至于单一代码库的好处,可以列举如下:
可见性(Visibility)
:每个人都可以看到其他人的代码,这样可以带来更好的协作和跨团队贡献——不同团队的开发人员都可以修复代码中的 bug,而你甚至都不知道这个 bug 的存在。
更简单的依赖关系管理(Simpler dependency management)
:共享依赖关系很简单,因为所有模块都托管在同一个存储库中,因此都不需要包管理器。
唯一依赖源(Single source of truth)
:每个依赖只有一个版本,意味着没有版本冲突,没有依赖地狱。
一致性(Consistency)
:当你把所有代码库放在一个地方时,执行代码质量标准和统一的风格会更容易。
共享时间线(Shared timeline)
:API 或共享库的变更会立即被暴露出来,迫使不同团队提前沟通合作,每个人都得努力跟上变化。
原子提交(Atomic commits)
:原子提交使大规模重构更容易,开发人员可以在一次提交中更新多个包或项目。
隐式 CI(Implicit CI)
:因为所有代码已经统一维护在一个地方,因此可以保证持续集成。
统一的 CI/CD(Unified CI/CD)
:可以为代码库中的每个项目使用相同的 CI/CD部署流程。
统一的构建流程(Unified build process)
:代码库中的每个应用程序可以共享一致的构建流程。
Monorepo 的收益
说清楚 MultiRepo 的痛点之后,相信你也大概能理解为什么要诞生Monorepo这个技术了。现在就来细致地分析一下Monorepo到底给现代的前端工程带来了哪些收益。
a 首先是工作流的一致性,由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况。
b 其次是项目基建成本的降低,所有项目复用一套标准的工具和规范,无需切换开发环境,如果有新的项目接入,也可以直接复用已有的基建流程,比如 CI 流程、构建和发布流程。这样只需要很少的人来维护所有项目的基建,维护成本也大大减低。
c 再者,团队协作也更加容易,一方面大家都在一个仓库开发,能够方便地共享和复用代码,方便检索项目源码,另一方面,git commit 的历史记录也支持以功能为单位进行提交,之前对于某个功能的提交,需要改好几个仓库,提交多个 commit,现在只需要提交一次,简化了 commit 记录,方便协作。
Monorepo 的落地
在实际场景来落地 Monorepo,需要一套完整的工程体系来进行支撑,因为基于 Monorepo 的项目管理,绝不是仅仅代码放到一起就可以的,还需要考虑项目间依赖分析、依赖安装、构建流程、测试流程、CI 及发布流程等诸多工程环节,同时还要考虑项目规模到达一定程度后的性能问题,比如项目 构建/测试 时间过长需要进行增量 构建/测试、按需执行 CI 等等,在实现全面工程化能力的同时,也需要兼顾到性能问题。
N3: 什么场景下使用?
monorepo 简单来说就是将多个项目整合到了一个仓库里来管理,很多开源库都采用了这种代码管理方式
N4: 采用 Monorepo 解决了之前的痛点之后,产生了哪些新的问题呢?这些问题可以解决吗?
随着单一代码库的发展,我们在版本控制工具、构建系统和持续集成流水线方面达到了设计极限。这些问题可能会让一家公司走上多代码库的道路:
- 性能差(Bad performance)
:单一代码库难以扩大规模,像 git blame 这样的命令可能会不合理的花费很长时间执行,IDE 也开始变得缓慢,生产力受到影响,对每个提交测试整个 repo 变得不可行。
- 破坏主线(Broken main/master)
:主线损坏会影响到在单一代码库中工作的每个人,这既可以被看作是灾难,也可以看作是保证测试既可以保持简洁又可以跟上开发的好机会。
- 学习曲线(Learning curve)
:如果代码库包含了许多紧密耦合的项目,那么新成员的学习曲线会更陡峭。
- 大量的数据(Large volumes of data)
:单一代码库每天都要处理大量的数据和提交。
- 所有权(Ownership)
:维护文件的所有权更有挑战性,因为像 Git 或 Mercurial 这样的系统没有内置的目录权限。
- Code reviews
:通知可能会变得非常嘈杂。例如,GitHub 有有限的通知设置,不适合大量的 pull request 和 code review。
- 命令不统一
存在三种命令
yarn
yarn workspace
lerna
新人上手容易造成误解,部分命令之间功能存在重叠。
- 发布速度慢
- peerDependencies 风险
N5: monorepo 和 multirepo 对比?
- 开发
mono repo
✅ 只需在一个仓库中开发,编码会相当方便。
✅ 代码复用高,方便进行代码重构。
❌ 项目如果变的很庞大,那么 git clone、安装依赖、构建都会是一件耗时的事情。
multi repo
✅ 仓库体积小,模块划分清晰。
❌ 多仓库来回切换(编辑器及命令行),项目一多真的得晕。如果仓库之间存在依赖,还得各种 npm link。
❌ 不利于代码复用。
- 工程配置
mono repo
✅ 工程统一标准化
multi repo
❌ 各个团队可能各自有一套标准,新建一个仓库又得重新配置一遍工程及 CI / CD 等内容。
- 依赖管理
mono repo
✅ 共同依赖可以提取至 root,版本控制更加容易,依赖管理会变的方便。
multi repo
❌ 依赖重复安装,多个依赖可能在多个仓库中存在不同的版本,npm link 时不同项目的依赖可能会存在冲突问题。
- 代码管理
mono repo
❌ 代码全在一个仓库,项目一大,几个 G 的话,用 Git 管理会存在问题。
multi repo
✅ 各个团队可以控制代码权限,也几乎不会有项目太大的问题。
- 部署
这部分两者其实都存在问题。
multi repo 的话,如果各个包之间不存在依赖关系倒没事,一旦存在依赖关系的话,开发者就需要在不同的仓库按照依赖先后顺序去修改版本及进行部署。
而对于 mono repo 来说,有工具链支持的话,部署会很方便,但是没有工具链的话,存在的问题一样蛋疼,后续文章中会讲到。
看了上文中的对比,相信读者应该是能认识到 mono repo 在一些痛点上还是解决得很不错的,这也是很多开源项目采用它的原因。但是实际上当我们引入 mono repo 架构以后,又会带来一大堆新的问题,无非市面上的工具链帮我们解决了大部分问题,比如 lerna。
N6: 在 Vue 源码管理中使用 monorepo 的优点和差异?
差异
:
Vue 2.0 的源码托管在 src 之下,依据功能拆分出了 compiler (模板编译的相关代码)、core(与平台无关的通用运行时代码)、platform(平台专有代码)、server(服务端渲染相关代码)、sfc(.vue单文件解析相关代码)和 shared(共享工具代码)等目录。
Vue 3.0 源码整体是通过 monorepo 的方式维护的, 并根据功能将不同的模块拆分到 packages 目录下的不同子目录中。
优点
:
monorepo 把这些模块拆分到不同的包中,每个包都有各自的 API ,类型定义和测试。这样依赖,模块拆分的粒度更细,职责划分更明确,模块之间的依赖关系也更加明显,使得开发人员更容易阅读,理解和更改所有模块源码。提高了代码的可维护性。
另外一些包(例如 reactivity 响应式库)是独立于 Vue.js 使用的,用户可以单独依赖这个库,而不用依赖整个 Vue.js ,这就减小了引用包的体积大小,而 Vue 2.0 是做不到的.