在 pnpm workspace 中,正确地为每个子包安装依赖是 Monorepo 工程化的基础。本文详细介绍 4 种安装方式、依赖类型区分、验证方法和最佳实践。
概念梳理文章地址:深入理解 Monorepo:概念梳理新技术的的诞生一定是有它的技术背景的。新技术一定是适合公司、团队的技术吗?我看这倒未 - 掘金
工程落地文章地址:深入理解 Monorepo:工程落地在上一篇中对 Monorepo 的基本概念进行了讲解,在本篇文章中,将会结合项目代码 - 掘金
前言
当你从传统的单仓单包项目迁移到 Monorepo 后,第一个遇到的问题往往是:
"我想给
apps/web装一个axios,应该在根目录装还是在子目录装?命令怎么写?"
在 Monorepo 中,依赖安装不再是简单的 pnpm install xxx。你需要明确:
- 这个依赖是给哪个包装的?
- 是运行时依赖还是开发依赖?
- 是第三方包还是 Monorepo 内部的本地包?
- 是所有包共享的工程工具,还是某个包专属的业务依赖?
本文以 pnpm workspace 为例,系统讲解 Monorepo 中子包依赖的安装方法。
前置知识:pnpm workspace 的依赖结构
在开始之前,先理解 pnpm workspace 的核心特点:
bash
my-monorepo/
├── node_modules/ # 根目录依赖(全局工程工具)
│ ├── .pnpm/ # pnpm 的真实存储(硬链接到全局 store)
│ ├── eslint -> .pnpm/... # 符号链接
│ └── turbo -> .pnpm/...
├── apps/
│ └── web/
│ ├── node_modules/ # 子包专属依赖
│ │ ├── axios -> ../../node_modules/.pnpm/...
│ │ └── @my-monorepo/
│ │ ├── ui -> ../../../packages/ui # 软链接到本地包
│ │ └── utils -> ../../../packages/utils # 软链接到本地包
│ └── package.json
├── packages/
│ ├── ui/
│ │ └── package.json
│ └── utils/
│ └── package.json
├── package.json # 根目录配置
└── pnpm-workspace.yaml # workspace 声明
关键特点:
- 根目录的
node_modules存放所有包共享的工程工具(eslint、turbo 等) - 子包的
node_modules存放该包专属的依赖,通过符号链接指向根目录的.pnpm存储 - 本地包依赖 通过软链接直接指向源码目录,修改即时生效,无需发布
pnpm-lock.yaml在根目录,统一管理所有包的依赖版本锁定
方法一:根目录 + --filter(最推荐)
在根目录 执行命令,通过 --filter 参数指定要给哪个包装依赖。这是最推荐的方式,不需要切换目录,也不容易装错位置。
基本语法
bash
pnpm --filter <包名> add <依赖名> [参数]
实际示例
bash
# 给 @my-monorepo/web 安装运行时依赖 axios
pnpm --filter @my-monorepo/web add axios
# 给 @my-monorepo/web 安装开发依赖 vitest
pnpm --filter @my-monorepo/web add -D vitest
# 给 @my-monorepo/ui 安装 peer 依赖 vue
pnpm --filter @my-monorepo/ui add --save-peer vue
# 同时安装多个依赖
pnpm --filter @my-monorepo/web add axios vue-router pinia
--filter 的多种匹配方式
--filter 非常灵活,支持以下匹配方式:
bash
# 1. 按包名精确匹配(最常用)
pnpm --filter @my-monorepo/web add axios
# 2. 按目录路径匹配
pnpm --filter ./apps/web add axios
# 3. 通配符匹配(给所有 packages 下的包装)
pnpm --filter "./packages/*" add lodash-es
# 4. 匹配某个包的所有依赖(...前缀)
# 例如:@my-monorepo/web 依赖了 ui 和 utils,这个命令会给 web、ui、utils 都装
pnpm --filter ...@my-monorepo/web add -D eslint
# 5. 取反匹配(排除某个包)
pnpm --filter "!@my-monorepo/utils" add -D prettier
执行结果
执行后,依赖会被写入对应子包 的 package.json,而不是根目录的:
json
// apps/web/package.json(自动更新)
{
"name": "@my-monorepo/web",
"dependencies": {
"axios": "^1.6.0"
}
}
同时,根目录的 pnpm-lock.yaml 会自动更新,apps/web/node_modules/axios 会自动建立符号链接。
方法二:进入子包目录直接安装
cd 到子包目录后,直接执行 pnpm add,pnpm 会自动识别当前目录属于哪个 workspace 包,并把依赖写入该包的 package.json。
基本语法
bash
cd <子包目录>
pnpm add <依赖名> [参数]
实际示例
bash
# 进入 web 应用目录
cd apps/web
# 安装运行时依赖 → 写入 apps/web/package.json 的 dependencies
pnpm add axios
# 安装开发依赖 → 写入 devDependencies
pnpm add -D vitest
# 安装 peer 依赖 → 写入 peerDependencies
pnpm add --save-peer vue
效果
和方法一完全一样,依赖会被写入当前目录的 package.json。区别只是需要先 cd 进子目录。
提示 :如果你不确定当前目录属于哪个包,可以执行
pnpm list查看,或者看当前目录的package.json中的name字段。
方法三:安装本地包(workspace 内部依赖)
当一个子包需要依赖另一个本地子包 时(例如 apps/web 需要使用 packages/ui 的组件),必须使用 workspace:* 协议,而不是写具体版本号。
什么是 workspace:*
workspace:* 是 pnpm workspace 的特殊协议,表示"这个依赖来自 Monorepo 内部,使用本地最新源码"。pnpm 解析时会:
- 在 workspace 中查找同名的包
- 在使用方的
node_modules中创建软链接,直接指向本地源码目录 - 修改本地包的源码后,使用方即时生效,无需发布或重新安装
方式 A:用命令安装
bash
# web 依赖本地的 ui 包
pnpm --filter @my-monorepo/web add @my-monorepo/ui@workspace:*
# web 同时依赖 ui 和 utils
pnpm --filter @my-monorepo/web add @my-monorepo/ui@workspace:* @my-monorepo/utils@workspace:*
方式 B:手动编辑 package.json(更常用)
直接编辑子包的 package.json,在 dependencies 中添加本地包引用:
json
// apps/web/package.json
{
"name": "@my-monorepo/web",
"dependencies": {
"vue": "^3.3.0",
"@my-monorepo/ui": "workspace:*",
"@my-monorepo/utils": "workspace:*"
}
}
然后在根目录执行一次安装,让 pnpm 建立软链接:
bash
pnpm install
安装后的软链接结构
执行 pnpm install 后,apps/web/node_modules/@my-monorepo/ 下会创建软链接:
bash
apps/web/node_modules/@my-monorepo/
├── ui -> ../../../packages/ui # 软链接,指向本地源码
└── utils -> ../../../packages/utils # 软链接,指向本地源码
你可以用以下命令验证:
bash
ls -la apps/web/node_modules/@my-monorepo/
为什么不能写具体版本号
json
// ❌ 错误:写具体版本号
{
"dependencies": {
"@my-monorepo/ui": "1.0.0"
}
}
如果写具体版本号,pnpm 会去 npm registry 查找 @my-monorepo/ui@1.0.0,而不是使用本地包。如果这个包没有发布到 npm,就会安装失败。
json
// ✅ 正确:使用 workspace:* 协议
{
"dependencies": {
"@my-monorepo/ui": "workspace:*"
}
}
workspace:* 告诉 pnpm:"这个依赖来自本地 workspace,不要去 npm 找。"
方法四:安装到根目录(全局共享)
工程治理类工具(eslint、prettier、turbo、typescript、syncpack 等)应该装在根目录,所有子包共享,不要每个包都装一遍。
基本语法
bash
pnpm add -Dw <依赖名>
-D=--save-dev,写入devDependencies-w=--workspace-root,安装到根目录
实际示例
bash
# 安装全局工程工具
pnpm add -Dw eslint prettier turbo typescript
# 安装依赖检查工具
pnpm add -Dw syncpack dependency-cruiser
# 安装 git hooks 工具
pnpm add -Dw husky lint-staged
执行结果
依赖会被写入根目录 的 package.json:
json
// 根目录 package.json
{
"name": "my-monorepo",
"private": true,
"devDependencies": {
"eslint": "^8.50.0",
"prettier": "^3.0.0",
"turbo": "^1.10.0",
"typescript": "^5.0.0"
}
}
这些工具会安装在根目录的 node_modules 中,所有子包都可以访问到。
为什么工程工具要装在根目录
| 原因 | 说明 |
|---|---|
| 避免重复安装 | 每个包都装一遍 eslint,浪费磁盘空间,且版本可能不一致 |
| 版本统一 | 所有包使用同一个版本的 eslint,lint 行为一致 |
| 全局配置 | eslint、prettier 的配置文件通常在根目录,工具也应该在根目录 |
| CI 简化 | CI 中只需要在根目录执行 pnpm install,所有工具就都可用了 |
依赖类型详解
在安装依赖时,需要明确依赖的类型,不同类型写入 package.json 的不同字段,对运行时和构建时的影响也不同。
| 类型 | 命令参数 | 写入字段 | 说明 | 示例 |
|---|---|---|---|---|
| 运行时依赖 | pnpm add xxx |
dependencies |
生产环境运行时需要的包 | vue、axios、vue-router、express |
| 开发依赖 | pnpm add -D xxx |
devDependencies |
构建、测试、开发时需要的包 | vite、vitest、eslint 插件、typescript |
| 对等依赖 | pnpm add --save-peer xxx |
peerDependencies |
可发布库要求使用方自己安装的包 | vue、react(组件库的 peer) |
| 可选依赖 | pnpm add -O xxx |
optionalDependencies |
可选的、安装失败不影响主流程的包 | 某些平台特定的原生模块 |
| 本地 workspace 包 | pnpm add xxx@workspace:* |
dependencies |
Monorepo 内部的本地包 | @my-monorepo/ui@workspace:* |
运行时依赖 vs 开发依赖
bash
# ✅ 运行时依赖:代码中 import 了,生产环境需要
pnpm --filter @my-monorepo/web add axios
# ✅ 开发依赖:只在构建/测试时用,生产环境不需要
pnpm --filter @my-monorepo/web add -D vitest
判断标准:在 src/ 目录的代码中 import 了的包,就是运行时依赖;只在配置文件、构建脚本、测试文件中用的,就是开发依赖。
对等依赖(peerDependencies)
对等依赖主要用于可发布的库 。例如你开发了一个 Vue 组件库 @my-monorepo/ui,使用这个组件库的项目必须自己安装 Vue,组件库不应该把 Vue 打包进去(否则会导致项目中有多个 Vue 实例)。
json
// packages/ui/package.json
{
"name": "@my-monorepo/ui",
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"vue": "^3.3.0"
}
}
peerDependencies:声明"使用方需要自己安装 vue@^3.0.0"devDependencies:组件库自己开发时需要安装 vue 用于测试和构建
安装后验证
安装依赖后,建议进行以下验证,确保安装正确。
1. 查看某个包的完整依赖树
bash
pnpm --filter @my-monorepo/web list
输出示例:
less
@my-monorepo/web@0.1.0
├── axios@1.6.0
├── vue@3.3.4
├── vue-router@4.2.4
├── @my-monorepo/ui -> link:../../packages/ui
└── @my-monorepo/utils -> link:../../packages/utils
注意 -> link: 表示这是本地 workspace 包的软链接。
2. 检查本地包软链接
bash
ls -la apps/web/node_modules/@my-monorepo/
输出示例:
bash
ui -> ../../../packages/ui
utils -> ../../../packages/utils
如果看到 -> 指向本地目录,说明软链接建立成功。
3. 查看某个具体依赖是否安装
bash
pnpm --filter @my-monorepo/web list axios
4. 验证构建是否正常
bash
# 构建单个包
pnpm --filter @my-monorepo/web run build
# 或构建所有包
pnpm build
如果构建成功,说明依赖安装正确,代码可以正常引用。
常见问题与最佳实践
问题 1:可以用 npm 或 yarn 安装吗?
不建议。 如果项目使用 pnpm workspace,就应该统一使用 pnpm。混用 npm/yarn 会导致:
- lockfile 冲突(
pnpm-lock.yamlvspackage-lock.jsonvsyarn.lock) workspace:*协议 npm 不支持,会安装失败node_modules结构不同,可能导致依赖解析错误
如果确实要切换包管理器,需要先删除旧的 lockfile 和 node_modules,再用新的包管理器安装。
问题 2:安装本地包时写了具体版本号会怎样?
json
// ❌ 错误
{
"dependencies": {
"@my-monorepo/ui": "1.0.0"
}
}
pnpm 会去 npm registry 查找 @my-monorepo/ui@1.0.0,如果这个包没有发布,就会报 404 Not Found。即使发布了,也会使用 npm 上的版本,而不是本地最新源码,修改本地代码不会即时生效。
正确做法 :始终使用 workspace:* 协议。
问题 3:每个包都需要装一遍 eslint 吗?
不需要。 eslint、prettier、turbo、typescript 等工程工具统一装在根目录:
bash
pnpm add -Dw eslint prettier turbo
子包的 devDependencies 中不需要重复声明。在根目录执行 pnpm lint 时,eslint 会从根目录的 node_modules 解析。
问题 4:如何删除某个包的依赖?
bash
# 删除 @my-monorepo/web 的 axios 依赖
pnpm --filter @my-monorepo/web remove axios
不要手动删除 node_modules 中的文件,应该用 pnpm remove 命令,它会同时更新 package.json 和 pnpm-lock.yaml。
问题 5:安装后软链接没生效怎么办?
执行 pnpm install 重新建立链接:
bash
pnpm install
如果还是不行,可以尝试清理后重新安装:
bash
# 删除所有 node_modules(谨慎操作)
rm -rf node_modules apps/*/node_modules packages/*/node_modules
# 重新安装
pnpm install
问题 6:如何给所有包批量安装同一个依赖?
bash
# 给所有 workspace 包装 lodash-es
pnpm -r add lodash-es
# 给所有 packages 下的包装
pnpm -r --filter "./packages/*" add lodash-es
-r = --recursive,递归所有 workspace 包。
最佳实践总结
| 场景 | 推荐做法 | 命令示例 |
|---|---|---|
| 给某个包装第三方依赖 | 根目录 + --filter |
pnpm --filter @my-monorepo/web add axios |
| 给某个包装开发依赖 | 根目录 + --filter + -D |
pnpm --filter @my-monorepo/web add -D vitest |
| 安装本地 workspace 包 | 手动编辑 + workspace:* |
"@my-monorepo/ui": "workspace:*" |
| 安装全局工程工具 | 根目录 + -Dw |
pnpm add -Dw eslint prettier turbo |
| 删除某个包的依赖 | pnpm --filter <包> remove |
pnpm --filter @my-monorepo/web remove axios |
| 批量给所有包装 | -r(递归) |
pnpm -r add lodash-es |
| 重新建立软链接 | 根目录执行 | pnpm install |
一句话记忆
业务依赖装在子包,工程工具装在根目录,本地包用
workspace:*,安装统一用pnpm --filter。
总结
在 pnpm workspace Monorepo 中,依赖安装有 4 种方式:
- 根目录 +
--filter:最推荐,不用切换目录,精确控制给哪个包装 - 进入子包目录直接装 :直观,但需要
cd,效果和方法一相同 - 安装本地包 :使用
workspace:*协议,通过软链接指向本地源码,修改即时生效 - 安装到根目录:工程治理工具统一装在根目录,所有包共享,避免重复和版本不一致
正确区分依赖类型(运行时/开发/对等/可选),安装后验证软链接和构建,遵循"业务依赖装子包、工程工具装根目录"的原则,就能在 Monorepo 中高效地管理依赖。
希望这篇文章能帮助你在 Monorepo 项目中正确地为各个子包安装依赖。如果有任何问题,欢迎在评论区交流。