第六章:大型项目管理术——Monorepo 架构与 PNPM Workspaces 实战
第六章:大型项目管理术——Monorepo 架构与 PNPM Workspaces 实战
Prorise第六章:大型项目管理术——Monorepo 架构与 PNPM Workspaces 实战
摘要: 当我们的项目从一个独立的“小作坊”成长为由多个包(如组件库、主应用、文档站)构成的“联合企业”时,传统的 Multi-repo(多仓库)管理模式将面临代码复用困难、依赖管理混乱等巨大挑战。本章,我们将学习解决这一问题的现代前端架构模式——Monorepo(单体仓库)。我们将深入对比它与 Multi-repo 的优劣,并以一个真实的设计系统为案例,手把手教你使用 PNPM Workspaces 搭建、管理和维护你自己的第一个 Monorepo 项目。
6.1. 什么是 Monorepo?它与传统的 Multi-repo 有何不同?
在软件开发中,代码仓库的管理方式主要有两种:
- Multi-repo (多仓库): 这是最传统、最常见的方式。每个项目、每个库都有自己独立的 Git 仓库。例如,
webapp
在一个仓库,ui-components
在另一个仓库。 - Monorepo (单体仓库): 将所有相关的项目、库都放在同一个 Git 仓库中进行管理。例如,
webapp
和ui-components
都在同一个仓库的不同子目录中。
这两种模式在工作流、代码共享、版本控制等方面有着天壤之别。
对比维度 | Multi-repo (多仓库) | Monorepo (单体仓库) |
---|---|---|
代码库管理 | 每个包一个独立的 Git 仓库。 | 所有包共享同一个 Git 仓库。 |
代码共享 | 困难。共享代码需发布为 npm 包,本地调试需 npm link ,流程繁琐。 | 极其容易。可以直接在仓库内部互相引用,无需发布,修改即时生效。 |
依赖管理 | 分散。每个包有自己的 node_modules ,易造成依赖版本不一致和冗余。 | 集中。可将公共依赖提升到根目录,统一管理,保证版本一致。 |
版本控制 | 分散。每个包独立打版本号。 | 可统一版本,也可独立版本,管理方式更灵活。 |
原子化提交 | 不可能。一个跨多包的功能修改,需要向多个仓库提交 commits。 | 核心优势。可以一次 commit 完成跨多包的修改,保持逻辑的原子性。 |
构建与 CI/CD | 简单。每个仓库配置独立的 CI 流程。 | 较复杂。需要工具支持来识别变更范围,实现按需构建和测试。 |
适用场景 | 互相完全独立的项目。 | 互相之间有代码共享、有强依赖关系的多个项目。 |
6.2. Monorepo 的优势:以“设计系统”为例
为了更具体地感受 Monorepo 的威力,让我们构思一个真实的企业级场景:Acme 公司要构建自己的设计系统。
这个系统至少包含三个包:
@acme/ui-components
: 核心的 UI 组件库 (React/Vue)。@acme/docs
: 用于展示和测试组件的文档站 (VitePress/Storybook)。@acme/webapp
: 使用这套组件库的官方主应用。
在 Multi-repo 模式下,工作流会是这样:
- 修改了
@acme/ui-components
里的一个按钮组件。 - 为了在文档站里看效果,需要先将组件库发布一个
beta
版本到 npm。 @acme/docs
项目更新依赖,安装这个beta
版本,然后启动调试。- 调试通过后,再将组件库发布一个正式版。
@acme/webapp
和@acme/docs
再更新到正式版。这个流程因为涉及多次发布和安装,效率极其低下。
而在 Monorepo 模式下,工作流将变得无比丝滑:
- 直接在 Monorepo 中修改
@acme/ui-components
的按钮组件代码。 - 由于
@acme/docs
是直接通过源码引用组件库的,它的开发服务器会 立即热更新,实时看到修改效果。 - 调试完成后,进行 一次原子化提交,这个提交同时包含了组件库的修改和(可能有的)文档站的适配修改。
Monorepo 的核心优势在于,通过将所有相关代码集中管理,极大地 降低了跨包协作的成本,让多包开发如同在一个项目中修改不同模块一样简单。
6.3. 实战入门:使用 PNPM Workspaces 搭建你的第一个 Monorepo
PNPM 内置了对 Monorepo(在 PNPM 中称为 Workspaces/工作区)的顶级支持。现在,让我们亲手搭建上面提到的 Acme 设计系统。
第一步:创建 Monorepo 根目录
1 | # 创建并进入项目根目录 |
第二步:定义工作区 (Workspace)
创建 pnpm-workspace.yaml
文件,这是声明此项目为 Monorepo 的“开关”。
文件路径: acme-design-system/pnpm-workspace.yaml
1 | packages: |
第三步:创建子项目
1 | # 创建用于存放所有子项目的目录 |
此时,你的项目结构应该是这样:
1 | # acme-design-system/ |
第四步:安装公共依赖
我们的三个子项目很可能都依赖 react
和 typescript
。我们可以把这些公共依赖安装在 Monorepo 的 根目录,让所有子项目共享。
使用 -w
或 --workspace-root
标志来告诉 PNPM 在根目录安装。
1 | # 在 Monorepo 根目录执行 |
执行后,react
等依赖会被安装到根目录的 node_modules
中,并被自动“提升”(hoist),所有子项目都可以直接引用到它们,无需重复安装。
6.4. 依赖管理与内部包的互相引用
这是 Monorepo 最神奇的地方:如何让 @acme/webapp
依赖本地的 @acme/ui-components
?
第一步:修改子项目名称
为了能互相引用,我们需要先给子项目 package.json
里的 name
字段赋予规范的、带作用域的名称。
packages/ui-components/package.json
->"name": "@acme/ui-components"
packages/docs/package.json
->"name": "@acme/docs"
packages/webapp/package.json
->"name": "@acme/webapp"
第二步:使用 workspace:*
协议
现在,我们编辑 webapp
的 package.json
,让它依赖 ui-components
:
文件路径: packages/webapp/package.json
1 | { |
workspace:*
这个特殊的协议告诉 PNPM:“我依赖的 @acme/ui-components
不是 NPM 仓库里的某个版本,而是 当前工作区中的那个本地包”。
第三步:运行 pnpm install
在 Monorepo 根目录 运行:
1 | pnpm install |
PNPM 会识别到 workspace:*
协议,然后它不会去下载任何东西,而是会在 packages/webapp/node_modules
目录下,创建一个指向 packages/ui-components
目录的 符号链接。
第四步:在子项目中运行脚本
如果你想运行 webapp
的 dev
脚本,可以使用 -F
或 --filter
标志:
1 | # 假设 webapp 的 package.json 中有 "dev": "vite" |
这个命令会精准地只运行指定子项目的脚本,是 Monorepo 开发中的日常操作。
6.5. 本章核心速查总结
分类 | 关键项 | 核心描述 |
---|---|---|
核心概念 | Monorepo | 在单一 Git 仓库中管理多个相关联的包,以提升代码复用和协作效率。 |
核心概念 | Multi-repo | 每个包拥有独立的 Git 仓库,是传统的、分散式的管理方式。 |
PNPM 配置 | pnpm-workspace.yaml | (关键) 用于声明一个项目是 PNPM Monorepo 并定义工作区范围的文件。 |
PNPM 协议 | workspace:* | (核心) 在 package.json 中使用,用于声明一个依赖是工作区内的本地包。 |
核心命令 | pnpm add <pkg> -w | 在 Monorepo 根目录 安装一个所有包共享的依赖。 |
核心命令 | pnpm --filter <pkg_name> | (常用) 在 Monorepo 中,精准地对指定子项目执行命令(如 dev , build )。 |
6.6. 高频面试题与陷阱
你们为什么选择使用 Monorepo 架构?它主要解决了你们团队的什么问题?
我们选择 Monorepo 主要是为了解决跨项目代码复用的问题。比如我们的主应用、文档站都需要使用同一个内部组件库。在 Monorepo 中,组件库可以直接被其他项目以源码形式引用,任何修改都能即时生效,无需发布 npm 包,极大地提升了开发联调的效率。此外,它还带来了依赖集中管理和原子化提交的好处,保证了所有相关项目技术栈和版本的一致性。
听起来不错。那在 PNPM Workspaces 中,如果我想让 webapp
依赖工作区里的 ui-components
,我应该怎么在 package.json
中声明?pnpm install
时会发生什么?
我会在 webapp
的 package.json
的 dependencies
中,将 @acme/ui-components
的版本号指定为 workspace:*
。当在 Monorepo 根目录运行 pnpm install
时,PNPM 会解析这个协议,它不会去 npm 仓库下载,而是在 webapp
的 node_modules
目录下创建一个指向 packages/ui-components
源码目录的符号链接。这样就实现了本地包之间的高效引用。
好的。现在 Monorepo 里有几十个包,我只想运行其中一个,比如 @acme/docs
的 build
脚本,应该用什么命令?
我会使用 --filter
标志。在 Monorepo 的根目录执行 pnpm --filter @acme/docs build
。这个命令可以让 PNPM 精准地定位到 @acme/docs
这个包,并只在它的上下文中执行 build
脚本,避免了进入特定目录的麻烦,也方便在 CI/CD 流程中进行自动化操作。