Monorepo架构
现代前端工程开发的趋势和痛点
趋势
现代前端工程开发的已不再是之前的单一静态页面的开发,而是随着业务场景的多样性和复杂性在不断的演进,下面是一张来字节 web infra
团队在知乎分享的前端趋势概览图
- 第一个趋势是现代前端开发不再仅限于网页,而是涉及到的平台越来越多,比如 Web 端、Node、客户端和跨平台等。
- 第二个趋势是业务场景越来越多,复杂性也越来越大,特别是近年来也涌现了很多重前端交互的应用,比如 vscode、Figma。
- 第三个趋势就是随着业务场景的和多平台开发的出现,不可避免的使得前端团队规模在不断增大。
痛点
1、代码复用困难
在维护多个项目的时候,有一些逻辑很有可能会被多次用到,比如一些基础的组件、工具函数,在 polyrepo
中,需要为这些共享的代码单独维护一个仓库,之后会发布为单独的 npm
包供各个项目引用。
这样虽然能够解决代码复用的问题,但是之后这些公共依赖的升级会非常的繁琐,比如说现在所有的项目中都使用了 shared-ui
包的 1.1.0
版本,突然某个 ui
组件想要修改一些样式,就需要走下面的流程:
- 在
shared-ui
中修改样式 - 发布一个
1.1.1
版本的包 - 所有的项目都需要将
shared-ui
更新到最新版本
如果升级完之后发现有问题,这些步骤还得重复的执行,依赖 shared-ui
的库越多,这个过程花费的时间就越多,同时这个过程也会存在一定的沟通成本。
2、重复的项目基建
在 Polyrepo
中,各个项目之间是割裂的状态,因此每个项目都需要频繁创建 git
仓库,配置 CI
、Lint
规则、构建等,而且为每个项目创建的基建后续都需要有人来维护(依赖升级)。
3、项目构建时间长
现代的前端项目开发已经离不开打包工具(webpack
、rollup
),整体开发形式为项目开发时使用模块化机制开发, 经过构建工具打包形成成品代码, 成品代码最终在不支持模块化的浏览器中执行。虽然构建打包工具为前端开发提供了便利, 但也因为在项目运行之前需要提前将代码构建成一个成品整体,这导致在本地开发时也引入了新问题:
随着项目变得越来越大,全量构建整个项目就需要花费很长的时间,在本地开发时,无论每次修改多少代码都需要重新全量构建,大大降低了整体的开发效率。
一、Monorepo 简介及其与包管理工具(npm、yarn、pnpm)之间的关系
Monorepo模式:
Monorepo
是一种项目开发与管理的策略模式,它代表"单一代码仓库"(Monolithic Repository
)。在 Monorepo
模式中,所有相关的项目和组件都被存储在一个统一的代码仓库中,而不是分散在多个独立的代码仓库中,这些项目之间还可能会有依赖关系。
包管理工具:
npm
、yarn
、pnpm
等是用来管理项目依赖、发布包、安装依赖的工具,它们都提供了对工作区(workspace
)的支持,允许在单个代码库中管理多个项目或包。这种工作区支持在单个代码库中同时开发、测试和管理多个相关的项目,而无需使用多个独立的代码仓库。
关系:
这些包管理工具与 monorepo
的关系在于它们可以为 monorepo
提供依赖安装与依赖管理的支持,借助自身对 workspace
的支持,允许在 monorepo
中的不同子项目之间共享依赖项,并提供一种管理这些共享依赖项的方式,这可以简化依赖项管理和构建过程,并提高开发效率。
二、项目开发与管理模式
项目管理模式发展到现在,先后有三种项目管理模式的历史进程:
Monolithic(单体应用)开发模式
在软件开发的早期阶段,通常采用单体应用
的开发模式。整个应用程序由一个单一的代码库、构建和部署流程
组成。这种模式简单易懂,适合小型项目。
优点:
- 易于集成和部署。所有的代码在一个仓库里面,不需要特别的集中管理和协调,也可以直接在本地部署调试。
- 易于重用。所有的代码都在一个仓库中,开发人员开发的时候比较容易发现和重用已有的代码。
- 易于规范代码。所有的代码在一个仓库当中就可以标准化依赖管理,规范化代码的风格。
缺点:
- 代码维护性变差,随着功能以及代码量的大幅增加,代码功能耦合性增强。
- 构建时间过长,任何小修改必须重新构建整个项目,这个过程往往很长。
- 稳定性差,任意一个功能出现问题,可能导致整个应用挂掉。
Multirepo(多仓多模块)开发模式
为了解决单体应用的扩展性问题,团队开始将项目拆分为多个独立的仓库
,每个仓库独立维护自己的代码和构建流程。这种模式更适合大型和复杂的项目,因为它提供了更好的隔离性和独立性,同时可以更灵活地管理多个团队的工作。
优点:
- 每一个项目都有一个独立的仓库,职责单一。
- 代码量和复杂性受控,项目由不同的团队独立维护、边界清晰。
- 单个项目也易于自治开发测试部署和扩展,不需要集中管理集中协调。 利于进行权限控制,可以针对单个仓库来分配权限,权限分配粒度比较细。
缺点:
- 代码和配置很难共享:每个仓库都需要做一些重复的工程化能力配置(如
eslint
/test
/ci
等)且无法统一维护,且不利于代码复用。 - 依赖的治理复杂:模块越来越多,涉及多模块同时改动的场景增加。如何保障底层组件升级后,其引用到的组件也能同步更新到位。这点很难做到,如果没及时升级,各工程的依赖版本不一致,往往会引发一些意想不到的问题。
- 开发人员缺乏对整个项目的整体认知:开发人员一般只关心自己的服务代码,看不到项目整体,造成缺乏对项目整体架构和业务目标整体性的理解。
- 存储和构建消耗增加:假如多个工程依赖
pkg-a
,那么每个工程下node_modules
都会重复安装
pkg-a
,对本地磁盘内存和本地启动都是个很大的挑战。而且每个模块的发布都是相对独立
的,当一次迭代修改较多模块时,总体发布时效就是每个发布流程的串联。对发布者来说是一个非常大的负担。
Monorepo (单仓多模块)开发模式
回归单体管理:Monorepo
是一种试图回归单体
管理优势的方法,但保留了多仓库开发
的某些优点。它允许在一个代码库中管理多个项目、组件或服务,提供更好的代码共享和重用性。
现代工具支持:现代的版本控制系统和工具链使得 Monorepo
开发模式更为可行,例如像 Pnpm
、Yarn
、Lerna
和 Turborepo
等工具,它们提供了更好的管理、构建和部署多个项目的能力。
优点:
- 保留 multirepo 的主要优势
- 代码复用
- 模块独立管理
- 分工明确,业务场景独立
- 代码耦合度降低
- 管理所有项目的版本控制更加容易和一致,降低了不同项目之间的版本冲突。
- 可以统一项目的构建和部署流程,降低了配置和维护多个项目所需的工作量。
缺点:
Monorepo
可能随着时间推移变得庞大和复杂,导致构建时间增长和管理困难,git clone
、pull
的成本增加。- 权限管理问题:项目粒度的权限管理较为困难,容易产生非
owner
管理者的改动风险。
注意:Monorepo
绝不是简单地将代码搬到一个仓库(即不等于 Monolith
)。
在 Monorepo
中,每个子模块仍然是独立
的,有独立的版本,可以独立发包,不受其他模块的限制,最重要的是 Monorepo
的 build
、test
都是增量
的,只有发生更改
的子模块会进行构建和测试,而不需要重新构建和测试整个代码库。这可以大大加快持续集成(CI
)的速度,提高开发效率。
与 Mulitrepo
相比,Monorepo
中的子模块可以代码共享,可以最大程度复用依赖、复用工作流、复用基础配置。
单体仓库和多仓库两种方案能同时存在,一定是各有利弊的,不要将自己锁定到一种方案上,选择最合适的才是最好的。
3、前端包管理工具与 workspace 模式
Workspace 工作区
包管理工具通过 workspace
功能来支持 Monorepo
模式。Workspace
是指在一个代码库中管理多个相关项目或模块的能力。
包管理工具通过以下方式实现 workspace
的支持:
- 代码结构组织:在
Monorepo
中,不同的项目或模块通常位于同一个代码库的不同目录中。包管理工具通过识别并管理这些目录结构,可以将它们作为独立的项目或模块进行操作。 - 共享依赖:
Monorepo
中的不同项目或模块可以共享相同的依赖项。包管理工具可以通过在根目录中维护一个共享的依赖项列表,以确保这些依赖项在所有项目或模块中都可用。 - 交叉引用:在
Monorepo
中,不同项目或模块之间可能存在相互引用的情况。包管理工具需要处理这些交叉引用,以确保正确解析和构建项目之间的依赖关系。 - 版本管理:
Monorepo
中的不同项目或模块可能具有不同的版本。包管理工具需要能够管理和跟踪这些版本,并确保正确地安装和使用适当的版本。 - 构建和测试:包管理工具需要支持在
Monorepo
中进行增量构建和测试。这意味着只有发生更改的项目或模块会重新构建和测试,而不需要重新构建和测试整个代码库。
前端目前最主流的三款包管理工具 npm7+
、yarn
、pnpm
都已经原生支持 workspace
模式,也就是说不管使用哪个包管理工具,我们都可以实现其与 monorepo
的配合,但最终依然选择 pmpm
作为包管理工具主要是由于 pnpm
很好的解决了 npm
与 yarn
遗留的历史问题
npm 与 yarn 的历史遗留问题
- 扁平化依赖算法复杂,需要消耗较多的性能,依赖串行安装还有提速空间。
- 大量文件需要重复下载,对磁盘空间的利用率不足。(虽然在同一个项目中我不会重复的安装依赖 d了,但是如果我有100个项目,100个项目都需要用到某个包,那么这个包依然会被下载100次,也就是在磁盘的不同地方写入100次)
- 扁平化依赖虽然解决了不少问题,但是随即带来了依赖非法访问的问题,项目代码在某些情况下可以在代码中使用没有被定义在 package.json中的包,这种情况就是我们常说的
幽灵依赖
。
pnpm解决幽灵依赖
那pnpm
是如何解决上述问题的?留着下期为大家揭晓吧!