开源项目都在用 monorepo,可是你知道竟然有那么多坑么?

前言

文章首发自笔者的 我的网站,而且阅读起来也更加清晰。node

今天文章的话题是 monorepo。在进入正文以前,笔者先来归纳下什么是 monorepo 以及本文会从哪几个点来聊聊 monorepo。ios

monorepo 简单来讲就是将多个项目整合到了一个仓库里来管理,不少开源库都采用了这种代码管理方式,好比 Vue 3.0:git

从上图咱们能够看到 packages 文件夹下存在一堆文件夹,这每一个文件夹都对应一个 npm 包,咱们把这一些 npm 包都管理在一个仓库下了。github

了解 monorepo 的读者确定听过 lerna,想必也看过很多 lerna 配置相关的文章。本文不会来聊 lerna 该怎么怎么配置,而是主要来聊聊当咱们使用 monorepo 后会引入哪些问题?lerna 这些工具链解决了什么问题以及是如何解决的,总的来讲将会从如下几点来聊聊 monorepo:shell

  • 对比一下几种代码管理方式的不一样处
  • 这些代码管理方式各自有什么优缺点,为何咱们会选择 monorepo
  • 选择 monorepo 会给咱们带来哪些挑战
  • 市面上流行的工具链,好比 lerna 是如何帮助咱们解决问题的

两种代码管理的方式及优缺点

目前流行的就两种代码管理方式,分别为:npm

  • multi repo
  • mono repo

j55G0G

接下来聊聊它们各自的优缺点。编程

开发

mono repojson

✅ 只需在一个仓库中开发,编码会至关方便。axios

✅ 代码复用高,方便进行代码重构。bash

❌ 项目若是变的很庞大,那么 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。

接下来笔者就来聊聊 monorepo 在不使用工具链的状况下会存在哪些问题,以及市面上的工具链是如何解决问题的。

monorepo 带来了什么问题

安装依赖

各个包之间都存在各自的依赖,有些依赖多是多个包都须要的,咱们确定是但愿相同的依赖能提高到 root 目录下安装,其它的依赖装哪都行。

此时咱们能够经过 yarn 来解决问题(npm 7 以前不行),须要在 package.json 中加上 workspaces 字段代表多包目录,一般为 packages

以后当咱们安装依赖的时候,yarn 会尽可能把依赖拍平装在根目录下,存在版本不一样状况的时候会把使用最多的版本安装在根目录下,其它的就装在各自目录里。

|   ├── node_modules
|   |   ├── axios@0.21.1
├── packages
|   ├── pkg1
|   |   ├── package.json -> 依赖了 axios 0.21.1
|   ├── pkg2
|   |   ├── package.json -> 依赖了 axios 0.21.1
|   ├── pkg3
|   |   ├── node_modules
|   |   |   ├── axios@0.21.0
|   |   ├── package.json -> 依赖了 axios 0.21.0
复制代码

这种看似正确的作法,可能又会带来更恶心的问题。

好比说多个 package 都依赖了 React,可是它们版本并不都相同。此时 node_modules 里可能就会存在这种状况:根目录下存在这个 React 的一个版本,包的目录中又存在另外一个依赖的版本。

guYtrn

由于 node 寻找包的时候都是从最近目录开始寻找的,此时在开发的过程当中可能就会出现多个 React 实例的问题,熟悉 React 开发的读者确定知道这就会报错了。

遇到这种状况的时候,咱们就得用 resolutions 去解决问题,固然也能够经过阻止 yarn 提高共同依赖来解决(更麻烦了)。笔者已经不止一次遇到过这种问题,可能是安装依赖的依赖形成的多版本问题。这种依赖的依赖术语称之为「幽灵依赖」。

link

在 multi repo 中各类 link 已经够头疼了,我可不想在 mono repo 中继续 link 了。

此时 yarn 又拯救了咱们,在安装依赖的时候会帮助咱们将各个 package 软链到根目录中,这样每一个 package 就能找到另外的 package 以及依赖了。

可是实际上这样的方式还会带来一个坑。由于各个 package 都能访问到拍平在根目录中的依赖了,所以此时其实咱们无需在 package.json 中声明 dependencies 就能使用别人的依赖了。这种状况极可能会形成咱们最终忘了加上 dependencies,一旦部署上线项目就运行不起来了。

以上两块主要聊了依赖以及 link 层面的问题,这部分咱们能够虽然能够经过 yarn 解决,可是又引入了别的问题。

zQRUpt

接下来聊聊 mono repo 在 CI 中会遇到的挑战,包括了构建、单测、部署环节。

构建

构建是咱们会遇到的第一个问题。这时候可能有些读者就会迷惑了,构建不就是跑个 build 么,能有个啥问题。哎,接下来我就跟你聊聊这些问题。

首先由于全部包都存在一个仓库中了,若是每次执行 CI 的时候把全部包都构建一遍,那么一旦代码量变多,每次构建可能都要花上很多的时间。

这时候确定有读者会想到增量构建,每次只构建修改了代码的 package,这个确实可以解决问题,核心代码也很简单:

git diff --name-only {git tag / commit sha} --{package path}
复制代码

上述命令的功能是寻找从上次的 git tag 或者初次的 commit 信息中查找某个包是否存在文件变动,而后咱们拿到这些信息只针对变动的包作构建就行。可是注意这个命令的前提是在部署的时候打上 tag,不然就找不到上次部署的节点了。

可是单纯这样的作法是不够的,由于在 mono repo 中咱们还会遇到多个 package 之间有依赖的场景:

RdUElM

在这种状况下假如此时在 CI 中发现只有 A 包须要构建而且只去构建了 A 包,那么就会出现问题:在 TS 环境下确定会报错找不到 D 包的类型。

在这种存在包于包之间有依赖的场景时,咱们须要去构建一个有向无环图(DAG)来进行拓扑排序,关于这个概念有兴趣的读者能够自行查阅资料。

总之在这种场景下,咱们须要寻找出各个包之间的依赖关系,而后根据这个关系去构建。好比说 A 包依赖了 D 包,当咱们在构建 A 包以前得先去构建 D 包才成。

以上是没有工具链时可能会出现的问题。若是咱们用上 lerna 的话,内置的一些命令就能够基本帮助咱们解决问题了:

  • lerna changed 寻找代码有变更的包,接下来咱们就能够本身去进行增量构建了。
  • 经过 lerna 执行命令,自己就会去进行拓扑排序,因此包之间存在依赖时的构建问题也就被解决了。

总结一下构建时咱们会遇到的问题:

T95y1Q

单测

单测的问题其实和构建遇到的问题相似。每次把全部用例都跑一遍,可能耗时比构建还长,引入增量单测颇有必要。

这个需求通常来讲单测工具都会提供,好比 Jest 经过如下命令咱们就能实现需求了:

jest --coverage --changedSince=master
复制代码

可是这种单测方式会引来一个小问题:单测覆盖率是以「测试用例覆盖的代码 / 修改过的代码」来算的,极可能会出现覆盖率不达标的问题,虽然总体的单测覆盖率多是达标的。 常写单测的读者确定知道有时候一部分代码就是很难写单测,出现这种问题也在所不免,可是若是咱们在 CI 中配置了低于覆盖率就不能经过 CI 的话就会有点蛋疼。

固然这个问题其实仁者见仁智者见智,往好了说也是在提升每次 commit 的代码质量。

部署

部署是最重要的一环了,这里会遇到的问题也是最复杂的,固然大部分问题其实以前都解决过了,问题大体可分为:

  • 如何给单个 package 部署?
  • 单个 package 部署时有依赖关系如何解决?
  • package 部署时版本如何自动计算?

首先来看前两个问题。

第一个问题的解决办法其实和增量构建那边作法同样,经过命令找到修改过代码的 package 就行。可是光找到须要部署的 package 还不够,咱们还须要经过拓扑排序看看这个 package 有没有被别的 package 所依赖。若是被别的 package 所依赖的话,依赖方即便代码没有变更也是须要进行部署的,这就是第二个问题的解决方案。

第三个问题解决起来涉及的东西会有点多,笔者以前也给自动化部署系统写过一篇文章:连接 ,有兴趣的读者能够一读。

这里笔者就简短地聊聊解决方案。

首先咱们须要引入 commitizen 这个工具。

这个工具能够帮助咱们提交规范化的 commit 信息:

上图中最重要的就是 feat、fix 这些信息,咱们须要根据这个 type 信息来计算最终的部署版本号。

接下来在 CI 中咱们须要分析这个规范化的 commit 信息来得出 type。

其实原理很简单,仍是用到了 git command:

git log -E --format=%H=%B
复制代码

对于以上 commit,咱们能够经过执行命令得出如下结果:

固然这样分析是把当前分支的全部 commit 都分析进去了,大部分发版时候咱们只须要分析上次发版至今的全部变动,所以须要修正 command 为:

git log 上次的 commit id...HEAD -E --format=%H=%B
复制代码

最后咱们就能够经过正则来拿到 type,而后经过 semver 计算出版本号。

固然了,使用 lerna 也能帮咱们把这些问题解决的差很少了:

lerna publish --conventional-commits
复制代码

执行以上代码就基本解决了部署会遇到的问题。可是公司内部的部署系统通常都会本身去实现这部分的功能,毕竟自定义一些功能会更加方便。

总结一下部署环节中咱们可能会遇到的问题:

kGUdyE

工具链带来的好处及坏处

从上文中读者们应该也能够发现好比 lerna 这些工具链帮助咱们解决了不少问题,以致于把问题都隐藏了起来,致使了不少开发者可能都不了解使用 monorepo 到底会带来哪些问题,由于 monorepo 就是一个完美方案了。

另外这些工具链解决问题的方式也并非完美的,使用它们之后其实又会带来一些别的问题。

好比说咱们用 yarn workspaces 解决了 link 以及安装依赖的问题,可是又带来了版本间的冲突以及非法访问依赖的问题,解决这些问题咱们可能又得引入新的包管理器,好比 pnpm 来解决。

总的来讲,在编程世界里还真的没啥银弹,看似不错的工具,在帮助咱们解决了很多问题的同时必然又会引入新的问题,选择工具无非是在看当下哪一个使用起来成本更低收益更大罢了。

总结

mono repo 并非银弹,使用这个架构仍是会带来不少问题,无非市面上的工具帮助咱们解决了大部分。文章主要聊了聊在没有这些工具的时候咱们可能会遇到哪些问题,以及使用这些工具后解决了什么又带来了什么。

欢迎读者们一块儿交流探讨。

相关文章
相关标签/搜索