❌现有的架构可能会遇到什么问题
分散的git仓库
随着时间的沉淀,项目数量在飞速增长,增加了项目的工程管理难度
怎么区分来源呢?
- 通过git目录划分
- 通过项目命名
新员工入职的时候需要,需要下载代码并安装所需的依赖。在这种情况下,初始设置将很复杂(可能会遇到环境不一致),在文档通常不完整情况下,可能需要寻求帮助。
代码复用
在多个项目间涉及公共组件、公共函数或公共配置(如各种config)时候,代码复用就成了问题。
解决方案:
- copy 相关代码
- 抽取公共逻辑发布npm包
开发体验
项目开发时
多个 git 仓库管理起来天然是麻烦的。对于功能类似的模块,如果拆成了多个仓库,无论对于多人协作还是独立开发,都需要打开多个仓库页面。
组件开发时
- 进入 npm 包项目,执行
npm link
- 进去 app 应用,执行
npm link package
- 调试完成后,执行
npm unlink package
工作流
在当前的工作流中,每个仓库都需要做一些重复的工程化能力配置(如 eslint/prettier/ci 等)且无法统一维护,当有工程上的升级时,需要手动更新所有仓库的配置。
Code Review、Merge Request 从各自模块仓库执行,比较割裂。
🤔️什么是 Monorepo
monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略
Monorepo 能带来什么
统一工作流
首先是 工作流的一致性
,由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况,对开发调试而言都带来了方便
降低基建成本
其次是 项目基建成本的降低
,所有项目复用一套标准的工具和规范,无需切换开发环境,如果有新的项目接入,也可以直接复用已有的基建流程,比如 CI 流程、构建和发布流程。这样只需要很少的人来维护所有项目的基建,维护成本也大大减低
提升团队协作效率
再者, 团队协作也更加容易
,一方面大家都在一个仓库开发,能够方便地共享和复用代码,方便检索项目源码,另一方面,git commit 的历史记录也支持以功能为单位进行提交,之前对于某个功能的提交,需要改好几个仓库,提交多个 commit,现在只需要提交一次,简化了 commit 记录,方便协作
Monorepo遇到的问题
权限问题
由于单仓的管理模式,使用Monorepo将无法简单的控制各个模块代码的访问限制,任何有权限访问该仓库的人员将有权限访问所有的代码工程,这可能会导致部分安全问题。
性能问题
当仓库的代码规模非常的巨大,达到GB/TB的级别,会增大开发环境的代码下载成本,以及本地硬盘的压力,执行git status也可能需要花费数秒甚至数分钟的时间。并且,当代码工程很多且活跃数量也很多的情况,会加大分支管理策略和各个代码工程版本管理的压力。
Monorepo 方案
考虑使用 turborepo 构建方案 和 pnpm 包管理方案
turborepo 构建方案
TurboRepo 的出现,正是解决 Monorepo 慢的问题
多任务并行处理
Turbo支持多个任务的并行运行,我们在对多个子包,编译打包的过程中,turbo会同时进行多个任务的处理
在传统的 monorepo 任务运行器中,就像lerna
或者yarn
自己的内置workspaces run
命令一样,每个项目的script生命周期脚本都以拓扑方式运行(这是“依赖优先”顺序的数学术语)或单独并行运行。根据 monorepo 的依赖关系图,CPU 内核可能处于空闲状态——这样就会浪费宝贵的时间和资源。
什么是拓扑 ? 拓扑 Topological Order 是一种排序 拓扑排序是依赖优先的术语, 如果 A 依赖于 B,B 依赖于 C,则拓扑顺序为 C、B、A。
比如一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity)。在整个工程中,有些子工程(活动)必须在其它有关子工程完成之后才能开始,也就是说,一个子工程的开始是以它的所有前序子工程的结束为先决条件的
为了可以了解turbo
多么强大,下图比较了turbo
vslerna
任务执行时间线:
turbo
它能够有效地安排任务类似于瀑布可以同时异步执行多个任务,而lerna
一次只能执行一项任务 所以turbo
的 性能不言而喻。
增量构建
TurboRepo 的基本原则是从不重新计算以前完成的工作, Turborepo 会记住你构建的内容并跳过已经计算过的内容,在多次构建开发时,这也就意味更少的构建耗时。
pnpm 包管理方案
pnpm(performant npm)本质上就是一个包管理器,这一点跟 npm/yarn 没有区别,但它作为杀手锏的优势在于:
- 解决幽灵依赖
- 包安装速度极快;
- 磁盘空间利用非常高效。
👻解决幽灵依赖
package.json
// project
{"name": "test","version": "1.0.0","main": "lib/index.js","dependencies": {"A": "1.0.0","B": "1.0.0","C": "1.0.0"},
}// c package
{"name": "C","version": "1.0.0","main": "lib/index.js","dependencies": {// "A": "2.0.0","D": "1.0.0"},
}
在 npm1
、npm2
中呈现出的是嵌套结构,比如下面这样:
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0└── node_modules└── D@1.0.0
这样的设计会存在什么问题:
- 依赖层级太深,会导致文件路径过长的问题,尤其在 window 系统下。
- 大量重复的包被安装,文件体积超级大。比如跟
A
同级目录下有一个 B, 两者都依赖于同一个版本的lodash
,那么 lodash 会分别在两者的 node_modules 中被安装,也就是重复安装。
从 npm3 开始,包括 yarn,都着手来通过扁平化依赖
的方式来解决这个问题
相比之前的嵌套结构
,现在的目录结构类似下面这样:
node_modules
├── A@1.0.0
├── B@1.0.0
├── C@1.0.0
└── D@1.0.0
未定义在其 package.json 文件中的D包,可以直接被引用到
pnpm
使用符号链接将项目的直接依赖项添加到模块目录的根目录中。
.pnmp 为虚拟存储目录,该目录通过 <package-name>@<version>
来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升,所以它不会存在之前提到的 幽灵依赖问题!
node_modules
├── A -> .pnmp/A@1.0.0
├── B -> .pnmp/B@1.0.0
└── C -> .pnmp/C@1.0.0node_modules└── D -> .pnmp/D@1.0.0
速度快
pnpm 安装包的速度究竟有多快?先以 React 包为例来对比一下:
可以看到,作为黄色部分的 pnpm,在绝多大数场景下,包安装的速度都是明显优于 npm/yarn,速度会比 npm/yarn 快 2-3 倍。
高效利用磁盘空间
当使用 npm 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被存放在一个统一的位置,因此:
- 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则
pnpm update
只需要添加一个 新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件。 - 所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。
🌰Monorepo 实践
对于一个新的项目,可以运行下面的命令来生成全新的代码仓库
pnpm dlx create-turbo@latest