关于前端:开源项目都在用-monorepo但是你知道居然有那么多坑么

4次阅读

共计 4443 个字符,预计需要花费 12 分钟才能阅读完成。

前言

明天文章的话题是 monorepo。在进入注释之前,笔者先来概括下什么是 monorepo 以及本文会从哪几个点来聊聊 monorepo。

monorepo 简略来说就是将多个我的项目整合到了一个仓库里来治理,很多开源库都采纳了这种代码治理形式,比方 Vue 3.0:

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

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

  • 比照一下几种代码治理形式的不同处
  • 这些代码治理形式各自有什么优缺点,为什么咱们会抉择 monorepo
  • 抉择 monorepo 会给咱们带来哪些挑战
  • 市面上风行的工具链,比方 lerna 是如何帮忙咱们解决问题的

两种代码治理的形式及优缺点

目前风行的就两种代码治理形式,别离为:

  • multi repo
  • mono repo

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

开发

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。

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

monorepo 带来了什么问题

装置依赖

各个包之间都存在各自的依赖,有些依赖可能是多个包都须要的,咱们必定是心愿雷同的依赖能晋升到 root 目录下装置,其它的依赖装哪都行。

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

之后当咱们装置依赖的时候,yarn 会尽量把依赖拍精装在根目录下,存在版本不同状况的时候会把应用最多的版本装置在根目录下,其它的就装在各自目录里。

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

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

因为 node 寻找包的时候都是从最近目录开始寻找的,此时在开发的过程中可能就会呈现多个 React 实例的问题,相熟 React 开发的读者必定晓得这就会报错了。

遇到这种状况的时候,咱们就得用 resolutions 去解决问题,当然也能够通过阻止 yarn 晋升独特依赖来解决(更麻烦了)。笔者曾经不止一次遇到过这种问题,多是装置 依赖的依赖 造成的多版本问题。

link

在 multi repo 中各种 link 曾经够头疼了,我可不想在 mono repo 中持续 link 了。

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

然而实际上这样的形式还会带来一个坑。因为各个 package 都能拜访到拍平在根目录中的依赖了,因而此时其实咱们无需在 package.json 中申明 dependencies 就能应用他人的依赖了。这种状况很可能会造成咱们最终忘了加上 dependencies,一旦部署上线我的项目就运行不起来了。

以上两块次要聊了依赖以及 link 层面的问题,这部分咱们能够间接通过 yarn 解决,尽管又引入了别的问题。

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

构建

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

首先因为所有包都存在一个仓库中了,如果每次执行 CI 的时候把所有包都构建一遍,那么一旦代码质变多,每次构建可能都要花上不少的工夫。

这时候必定有读者会想到增量构建,每次只构建批改了代码的 package,这个的确可能解决问题,外围代码也很简略:

git diff --name-only {git tag / commit sha} --{package path}

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

然而单纯这样的做法是不够的,因为在 mono repo 中咱们还会遇到多个 package 之间有依赖的场景

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

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

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

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

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

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

单测

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

这个需要一般来说单测工具都会提供,比方 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

执行以上代码就根本解决了部署会遇到的问题,当然如果本人去实现这套内容会不便自定义一些。

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

工具链带来的益处及害处

从上文中读者们应该也能够发现这些 monorepo 的工具链帮忙咱们解决了很多问题,以至于把这些问题都暗藏了起来,导致了很多开发者可能都不理解应用 monorepo 到底会带来哪些问题。

另外这些工具链也并不是完满的,应用它们当前其实又会带来一些别的问题。

比如说咱们用 yarn workspaces 解决了 link 以及装置依赖的问题,然而又带来了版本间的抵触以及非法拜访依赖的问题,解决这些问题咱们可能又得引入新的包管理器,比方 pnpm 来解决。

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

总结

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

正文完
 0