字节笔记本

2026年2月25日

pnpm Workspaces 从入门到精通

本文全面介绍 pnpm Workspaces 的使用方法,从基础概念到高级配置,帮助你掌握 monorepo 项目管理的核心技能。

一、什么是 Workspaces

pnpm workspaces 是 pnpm 内置的 monorepo 管理方案。它允许你在一个代码仓库中管理多个相互关联的包,同时共享依赖、统一构建流程,并通过软链接实现包之间的本地引用。

相比 npm/yarn workspaces,pnpm 的优势在于其独特的硬链接存储机制,能大幅节省磁盘空间,且安装速度更快。


二、初始化项目结构

一个典型的 monorepo 目录结构如下:

text
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── packages/
│   ├── app/
│   │   └── package.json
│   ├── ui/
│   │   └── package.json
│   └── utils/
│       └── package.json

根目录 pnpm-workspace.yaml,这是 pnpm workspaces 的核心配置文件:

yaml
packages:
  - 'packages/*'
  - 'apps/*'
  - '!**/node_modules/**'

! 前缀表示排除匹配的路径,这里排除所有 node_modules 目录,避免将其中的包误识别为 workspace 成员。

根目录 package.json

json
{
  "name": "my-monorepo",
  "private": true,
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  }
}

"private": true 是必须的,防止根包被意外发布到 npm。


三、子包的 package.json

每个子包都有独立的 package.json,包名建议使用 @scope/name 的形式:

json
{
  "name": "@myapp/utils",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  }
}

四、依赖管理

安装依赖到根目录(所有包共享)

bash
pnpm add typescript -D -w

-w--workspace-root 表示安装到根目录。

安装依赖到指定子包

bash
# 方式一:使用 --filter
pnpm add react --filter @myapp/app

# 方式二:进入子包目录后直接安装
cd packages/app && pnpm add react

引用 workspace 内部的包

这是 monorepo 的核心能力。让 @myapp/app 依赖 @myapp/utils

bash
pnpm add @myapp/utils --filter @myapp/app --workspace

执行后,@myapp/apppackage.json 会出现:

json
{
  "dependencies": {
    "@myapp/utils": "workspace:*"
  }
}

workspace:* 是 pnpm 特有的协议,表示始终使用本地 workspace 中的版本。发布时 pnpm 会自动将其替换为实际版本号。

workspace: 协议支持三种写法:

写法含义
workspace:*使用当前本地版本,发布时替换为精确版本
workspace:^发布时替换为 ^version
workspace:~发布时替换为 ~version

五、--filter 过滤器详解

--filter(简写 -F)是 pnpm workspaces 最重要的命令参数,用于精确选择要操作的包。

bash
# 指定包名
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

六、在所有包中执行脚本

bash
# 并行执行所有包的 build 脚本
pnpm -r build

# 等价于
pnpm --recursive run build

# 按拓扑顺序执行(先执行被依赖的包)
pnpm -r --sort build

# 只在根目录执行
pnpm run build  # 不加 -r 默认只在当前目录

-r--filter 可以组合使用:

bash
pnpm -r --filter "@myapp/*" test

七、.npmrc 常用配置

在根目录创建 .npmrc 文件,配置 pnpm 行为:

ini
# 禁止幽灵依赖(强烈推荐)
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 中定义:

yaml
packages:
  - 'packages/*'

catalog:
  react: ^18.3.0
  typescript: ^5.4.0
  vite: ^5.0.0

在子包的 package.json 中使用:

json
{
  "dependencies": {
    "react": "catalog:"
  },
  "devDependencies": {
    "typescript": "catalog:",
    "vite": "catalog:"
  }
}

还支持具名 catalog,用于区分不同场景的依赖版本:

yaml
catalog:
  react: ^18.3.0

catalogs:
  react17:
    react: ^17.0.0
  react18:
    react: ^18.3.0
json
{
  "dependencies": {
    "react": "catalog:react17"
  }
}

九、与构建工具集成

Turborepo

Turborepo 是目前最流行的 monorepo 构建编排工具,与 pnpm workspaces 配合极佳:

bash
pnpm add turbo -D -w

根目录 turbo.json

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,这实现了正确的拓扑排序构建。

bash
# 利用缓存并行构建所有包
turbo build

# 只构建受影响的包
turbo build --filter="[HEAD^1]"

不使用 Turborepo 的原生方案

bash
# pnpm 原生支持按拓扑顺序构建
pnpm -r --sort run build

十、发布工作流

手动发布

bash
# 进入子包目录
cd packages/utils

# 发布前 pnpm 自动将 workspace: 协议替换为实际版本
pnpm publish --access public

使用 Changesets 自动化版本管理

Changesets 是 monorepo 版本管理的事实标准:

bash
pnpm add @changesets/cli -D -w
pnpm changeset init

工作流程:

bash
# 1. 开发完成后,创建 changeset 描述变更
pnpm changeset

# 2. 更新版本号(自动消费 changeset 文件)
pnpm changeset version

# 3. 发布所有有变更的包
pnpm changeset publish

十一、常见问题与最佳实践

问题一:循环依赖

包 A 依赖包 B,包 B 又依赖包 A,会导致构建失败。应当重新审视架构,将公共逻辑抽取到第三个包中。

问题二:TypeScript 路径不识别

在根目录 tsconfig.json 中配置 paths 或使用 project references:

json
{
  "compilerOptions": {
    "paths": {
      "@myapp/*": ["./packages/*/src"]
    }
  }
}

更推荐使用 TypeScript project references(references 字段),它能提供增量编译和更精确的类型检查。

问题三:只安装某个包的依赖

bash
pnpm install --filter @myapp/app

最佳实践总结:

一是始终在根目录 package.json 加上 "private": true;二是使用 workspace:* 协议引用本地包,而非具体版本号;三是将构建工具、lint 工具等开发依赖安装到根目录;四是用 catalog 统一管理跨包的公共依赖版本;五是配合 Turborepo 或 nx 实现增量构建和缓存,在大型项目中能大幅提升构建速度;六是在 CI 中使用 pnpm install --frozen-lockfile 确保依赖版本严格锁定。


十二、一个完整的示例结构

text
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 场景。随着项目规模扩大,重点关注构建缓存策略和依赖图的合理性,这是性能优化的核心所在。

分享: