3、Monorepo 基础:Monorepo 概念、pnpm workspace 搭建、npm/yarn/pnpm 对比
3.1 什么是 Monorepo?
Monorepo 这个词,拆开看就是 "Mono"(单一)+ "Repo"(仓库)。说白了,就是把多个项目的代码,都塞进同一个 Git 仓库里管理。
你可能会问:「这不乱套了吗?」
嗯,我第一次听到这个想法时,也是这个反应。但后来我在一个大型中台项目里,被多仓库的依赖地狱折磨得够呛——A 仓库改了接口,B 仓库忘了更新,C 仓库直接炸了。那时候我才意识到,Monorepo 其实是个好东西。
Monorepo 的核心价值在于:
- 代码共享:公共组件、工具函数、类型定义,不用再发 npm 包了,直接引用源码
- 原子提交:一次提交可以同时修改多个子项目,保证版本一致性
- 统一构建:一套 ESLint、TypeScript、构建工具配置,管所有项目
- 依赖管理:公共依赖提升到根目录,避免重复安装
关键认知:Monorepo 不是把代码堆在一起就完事了。它需要工具链的支持,否则你会陷入「改一个文件,全部项目都要重新构建」的泥潭。
3.2 pnpm workspace 搭建实战
我个人习惯用 pnpm 来做 Monorepo。为什么?因为它的 workspace 机制最干净,而且解决了 npm/yarn 长期以来的幽灵依赖问题。
搭建步骤其实很简单,我带你走一遍:
3.2.1 初始化项目
mkdir my-monorepo
cd my-monorepo
pnpm init
3.2.2 配置 workspace
在根目录创建 pnpm-workspace.yaml 文件:
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**'
这个配置告诉 pnpm:「所有 packages 和 apps 目录下的子目录,都是 workspace 里的独立项目。」
3.2.3 创建子项目
mkdir -p packages/shared
mkdir -p apps/web
然后在 packages/shared/package.json 里:
{
"name": "@my/shared",
"version": "1.0.0",
"main": "src/index.ts"
}
在 apps/web/package.json 里:
{
"name": "@my/web",
"version": "1.0.0",
"dependencies": {
"@my/shared": "workspace:*"
}
}
小技巧:workspace:* 这个写法,表示「引用 workspace 里的最新版本」。pnpm 会自动把它链接到本地源码,而不是去 npm registry 下载。
3.2.4 安装依赖
pnpm install
跑完这个命令,你会发现 node_modules 里没有 @my/shared 的副本,而是通过软链接指向了 packages/shared。这就是 pnpm 的硬链接机制——省磁盘空间,而且修改源码后,引用方立即生效。
3.3 npm / yarn / pnpm 对比
这三个工具我都用过,踩过不少坑。咱们直接上对比表:
| 特性 | npm | yarn | pnpm |
|---|---|---|---|
| 安装速度 | 慢(串行) | 快(并行) | 极快(硬链接复用) |
| 磁盘占用 | 高(每个项目重复下载) | 高 | 低(全局存储 + 硬链接) |
| 幽灵依赖 | 有 | 有 | 无(严格隔离) |
| Monorepo 支持 | 需配合 lerna | 需配合 yarn workspaces | 原生支持 |
| 安全性 | 一般 | 一般 | 高(依赖隔离) |
这里重点说一下「幽灵依赖」。你想想看,在 npm 或 yarn 里,你明明没安装 lodash,但代码里却能 require('lodash')——因为某个依赖的依赖被提升到了根 node_modules。这很危险,我曾经因为这个踩过一个大坑:升级了一个底层库,结果所有「偷用」了它子依赖的项目全挂了。
pnpm 的做法是:每个包只能访问自己 package.json 里声明的依赖。想用 lodash?老老实实自己装。这听起来麻烦,但长期来看,这是对项目负责。
避坑指南:我曾经在一个老项目里从 yarn 迁移到 pnpm,结果发现很多代码偷偷用了未声明的依赖。那段时间我每天都在补 package.json 里的缺失依赖……所以,如果你要迁移,建议先跑一遍 pnpm install --strict-peer-dependencies 来检查。
3.4 核心逻辑:Monorepo 的依赖管理流程
为了让你更直观地理解 Monorepo 的运作方式,我画了一张流程图:
这张图展示了 Monorepo 的核心流程:根目录的 pnpm-workspace.yaml 定义了哪些目录是子项目,然后 pnpm 会扫描这些子项目的 package.json,解析依赖关系,最后通过硬链接把公共依赖安装到全局存储中。
说白了,pnpm 就像一个聪明的图书管理员——它不会给每个读者都买一套《百科全书》,而是把书放在公共书架上,谁要看就去拿,看完放回原位。这样既省空间,又不会搞丢。
个人建议:如果你是新项目,直接上 pnpm + Monorepo。如果是老项目迁移,先评估一下依赖的混乱程度。我一般会先跑 pnpm doctor 检查项目健康状况,再决定迁移策略。