字节笔记本
2026年2月25日
pnpm Workspaces 从入门到精通
本文全面介绍 pnpm Workspaces 的使用方法,从基础概念到高级配置,帮助你掌握 monorepo 项目管理的核心技能。
一、什么是 Workspaces
pnpm workspaces 是 pnpm 内置的 monorepo 管理方案。它允许你在一个代码仓库中管理多个相互关联的包,同时共享依赖、统一构建流程,并通过软链接实现包之间的本地引用。
相比 npm/yarn workspaces,pnpm 的优势在于其独特的硬链接存储机制,能大幅节省磁盘空间,且安装速度更快。
二、初始化项目结构
一个典型的 monorepo 目录结构如下:
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── packages/
│ ├── app/
│ │ └── package.json
│ ├── ui/
│ │ └── package.json
│ └── utils/
│ └── package.json根目录 pnpm-workspace.yaml,这是 pnpm workspaces 的核心配置文件:
packages:
- 'packages/*'
- 'apps/*'
- '!**/node_modules/**'! 前缀表示排除匹配的路径,这里排除所有 node_modules 目录,避免将其中的包误识别为 workspace 成员。
根目录 package.json:
{
"name": "my-monorepo",
"private": true,
"engines": {
"node": ">=18",
"pnpm": ">=8"
}
}"private": true 是必须的,防止根包被意外发布到 npm。
三、子包的 package.json
每个子包都有独立的 package.json,包名建议使用 @scope/name 的形式:
{
"name": "@myapp/utils",
"version": "1.0.0",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
}
}四、依赖管理
安装依赖到根目录(所有包共享)
pnpm add typescript -D -w-w 或 --workspace-root 表示安装到根目录。
安装依赖到指定子包
# 方式一:使用 --filter
pnpm add react --filter @myapp/app
# 方式二:进入子包目录后直接安装
cd packages/app && pnpm add react引用 workspace 内部的包
这是 monorepo 的核心能力。让 @myapp/app 依赖 @myapp/utils:
pnpm add @myapp/utils --filter @myapp/app --workspace执行后,@myapp/app 的 package.json 会出现:
{
"dependencies": {
"@myapp/utils": "workspace:*"
}
}workspace:* 是 pnpm 特有的协议,表示始终使用本地 workspace 中的版本。发布时 pnpm 会自动将其替换为实际版本号。
workspace: 协议支持三种写法:
| 写法 | 含义 |
|---|---|
workspace:* | 使用当前本地版本,发布时替换为精确版本 |
workspace:^ | 发布时替换为 ^version |
workspace:~ | 发布时替换为 ~version |
五、--filter 过滤器详解
--filter(简写 -F)是 pnpm workspaces 最重要的命令参数,用于精确选择要操作的包。
# 指定包名
pnpm --filter @myapp/app build
# 使用通配符
pnpm --filter "@myapp/*" build
# 选择某个包及其所有依赖(包含本地依赖链)
pnpm --filter @myapp/app... build
# 选择依赖了某个包的所有包(反向依赖)
pnpm --filter ...@myapp/utils build
# 选择某个目录下的包
pnpm --filter "./packages/app" build
# 只选择有变更的包(需配合 git)
pnpm --filter "[origin/main]" build六、在所有包中执行脚本
# 并行执行所有包的 build 脚本
pnpm -r build
# 等价于
pnpm --recursive run build
# 按拓扑顺序执行(先执行被依赖的包)
pnpm -r --sort build
# 只在根目录执行
pnpm run build # 不加 -r 默认只在当前目录-r 与 --filter 可以组合使用:
pnpm -r --filter "@myapp/*" test七、.npmrc 常用配置
在根目录创建 .npmrc 文件,配置 pnpm 行为:
# 禁止幽灵依赖(强烈推荐)
shamefully-hoist=false
# 只允许访问 package.json 中声明的依赖
strict-peer-dependencies=false
# link-workspace-packages 控制本地包的链接行为
link-workspace-packages=true
# 自动安装 peer dependencies
auto-install-peers=true幽灵依赖是指在代码中使用了未在 package.json 中声明的包,这在 npm/yarn 的扁平化结构中很常见,pnpm 的默认行为会阻止这种情况,是更健康的依赖管理方式。
八、Catalog 功能(pnpm 9+)
pnpm 9 引入了 catalog 功能,用于在所有子包中统一管理依赖版本,解决版本不一致的问题。
在 pnpm-workspace.yaml 中定义:
packages:
- 'packages/*'
catalog:
react: ^18.3.0
typescript: ^5.4.0
vite: ^5.0.0在子包的 package.json 中使用:
{
"dependencies": {
"react": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vite": "catalog:"
}
}还支持具名 catalog,用于区分不同场景的依赖版本:
catalog:
react: ^18.3.0
catalogs:
react17:
react: ^17.0.0
react18:
react: ^18.3.0{
"dependencies": {
"react": "catalog:react17"
}
}九、与构建工具集成
Turborepo
Turborepo 是目前最流行的 monorepo 构建编排工具,与 pnpm workspaces 配合极佳:
pnpm add turbo -D -w根目录 turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"]
},
"lint": {}
}
}"dependsOn": ["^build"] 表示在执行本包的 build 之前,必须先完成所有依赖包的 build,这实现了正确的拓扑排序构建。
# 利用缓存并行构建所有包
turbo build
# 只构建受影响的包
turbo build --filter="[HEAD^1]"不使用 Turborepo 的原生方案
# pnpm 原生支持按拓扑顺序构建
pnpm -r --sort run build十、发布工作流
手动发布
# 进入子包目录
cd packages/utils
# 发布前 pnpm 自动将 workspace: 协议替换为实际版本
pnpm publish --access public使用 Changesets 自动化版本管理
Changesets 是 monorepo 版本管理的事实标准:
pnpm add @changesets/cli -D -w
pnpm changeset init工作流程:
# 1. 开发完成后,创建 changeset 描述变更
pnpm changeset
# 2. 更新版本号(自动消费 changeset 文件)
pnpm changeset version
# 3. 发布所有有变更的包
pnpm changeset publish十一、常见问题与最佳实践
问题一:循环依赖
包 A 依赖包 B,包 B 又依赖包 A,会导致构建失败。应当重新审视架构,将公共逻辑抽取到第三个包中。
问题二:TypeScript 路径不识别
在根目录 tsconfig.json 中配置 paths 或使用 project references:
{
"compilerOptions": {
"paths": {
"@myapp/*": ["./packages/*/src"]
}
}
}更推荐使用 TypeScript project references(references 字段),它能提供增量编译和更精确的类型检查。
问题三:只安装某个包的依赖
pnpm install --filter @myapp/app最佳实践总结:
一是始终在根目录 package.json 加上 "private": true;二是使用 workspace:* 协议引用本地包,而非具体版本号;三是将构建工具、lint 工具等开发依赖安装到根目录;四是用 catalog 统一管理跨包的公共依赖版本;五是配合 Turborepo 或 nx 实现增量构建和缓存,在大型项目中能大幅提升构建速度;六是在 CI 中使用 pnpm install --frozen-lockfile 确保依赖版本严格锁定。
十二、一个完整的示例结构
my-monorepo/
├── .npmrc
├── pnpm-workspace.yaml
├── package.json # private: true, 根目录脚本
├── tsconfig.base.json # 共享 TS 配置
├── turbo.json
├── packages/
│ ├── utils/ # @myapp/utils 纯工具函数
│ │ ├── src/index.ts
│ │ ├── tsconfig.json
│ │ └── package.json
│ └── ui/ # @myapp/ui 组件库,依赖 utils
│ ├── src/index.ts
│ ├── tsconfig.json
│ └── package.json
└── apps/
└── web/ # @myapp/web 应用,依赖 ui 和 utils
├── src/
├── tsconfig.json
└── package.json掌握以上内容,基本可以应对绝大多数 monorepo 场景。随着项目规模扩大,重点关注构建缓存策略和依赖图的合理性,这是性能优化的核心所在。