深入理解 Monorepo:子包安装依赖

在 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 声明

关键特点:

  1. 根目录的 node_modules 存放所有包共享的工程工具(eslint、turbo 等)
  2. 子包的 node_modules 存放该包专属的依赖,通过符号链接指向根目录的 .pnpm 存储
  3. 本地包依赖 通过软链接直接指向源码目录,修改即时生效,无需发布
  4. 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 解析时会:

  1. 在 workspace 中查找同名的包
  2. 在使用方的 node_modules 中创建软链接,直接指向本地源码目录
  3. 修改本地包的源码后,使用方即时生效,无需发布或重新安装

方式 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.yaml vs package-lock.json vs yarn.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.jsonpnpm-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 种方式:

  1. 根目录 + --filter:最推荐,不用切换目录,精确控制给哪个包装
  2. 进入子包目录直接装 :直观,但需要 cd,效果和方法一相同
  3. 安装本地包 :使用 workspace:* 协议,通过软链接指向本地源码,修改即时生效
  4. 安装到根目录:工程治理工具统一装在根目录,所有包共享,避免重复和版本不一致

正确区分依赖类型(运行时/开发/对等/可选),安装后验证软链接和构建,遵循"业务依赖装子包、工程工具装根目录"的原则,就能在 Monorepo 中高效地管理依赖。

希望这篇文章能帮助你在 Monorepo 项目中正确地为各个子包安装依赖。如果有任何问题,欢迎在评论区交流。

相关推荐
OpenTiny社区2 小时前
码力全开,智启前端新生态|OpenTiny 登陆华为全联接大会2026
前端·github
妙码生花2 小时前
利用AI从零学Go并完成实战项目,完工总结:目录结构
前端·后端·go
妙码生花2 小时前
利用AI从零学Go并完成实战项目,完工总结:商业级开源产品定位和核心特性介绍
前端·后端·go
Captaincc2 小时前
掘金AI用量统计v0.1.0大更新-支持桌面宠物自定义和订阅额度卡片
前端·后端
默_笙2 小时前
🚲 从写信到打电话:WebSocket 双工通信与跨域方案的"通信进化史"
前端·javascript
掘金酱3 小时前
社区排行榜现已上线
前端·人工智能
zzjyr3 小时前
AI 问答的流式响应是怎么实现的?从 fetch 到 SSE 逐字解析
前端·人工智能·ai编程
ikoala3 小时前
DeepSeek 官方仓库惊现 DeepSeek Harness 桌面端!
前端·javascript·后端
liangshanbo12153 小时前
React useEffect 与 useLayoutEffect 面试题整理
前端·javascript·react.js