PNPM Workspace 实践指南
介绍
Workspace(工作区)是 pnpm 提供的 monorepo 解决方案,允许你在一个代码仓库中管理多个相关的包。相比 Lerna、Yarn Workspaces 等方案,pnpm workspace 具有以下优势:
- 磁盘空间高效:通过内容寻址存储,相同的依赖只会在磁盘上存储一次
 - 安装速度快:利用硬链接和符号链接机制,大幅提升安装速度
 - 严格的依赖管理:避免幽灵依赖(phantom dependencies)问题
 - 原生支持:无需额外工具,pnpm 内置支持 workspace
 
典型使用场景:
假设你需要同时维护 Web 应用、浏览器插件、移动端应用,它们需要共享 UI 组件、工具函数、类型定义等。使用 Workspace 可以:
my-product/
├── packages/
│   ├── ui/          # 共享 UI 组件
│   ├── utils/       # 工具函数
│   └── types/       # 类型定义
└── apps/
    ├── web/         # Web 应用
    ├── extension/   # 浏览器插件
    └── mobile/      # 移动端所有应用都可以直接引用共享包:import { Button } from '@my-product/ui',一次修改,处处生效!
快速开始
1. 安装
首先确保你已经安装了 pnpm:
# 使用 npm 安装
npm install -g pnpm
# 或使用 Homebrew(macOS)
brew install pnpm
# 验证安装
pnpm --version2. 创建配置
在项目根目录创建 pnpm-workspace.yaml 文件:
packages:
  # 所有在 packages/ 目录下的包
  - 'packages/*'
  # 所有在 apps/ 目录下的应用
  - 'apps/*'
  # 也可以排除某些目录
  # - '!**/test/**'3. 创建项目结构
# 创建目录结构
mkdir -p packages/shared packages/utils
mkdir -p apps/web apps/admin
# 在根目录初始化
pnpm init4. 创建子包
手动创建
在每个子包目录中创建 package.json:
# packages/shared/package.json
{
  "name": "@myproject/shared",
  "version": "1.0.0",
  "main": "index.js"
}
# packages/utils/package.json
{
  "name": "@myproject/utils",
  "version": "1.0.0",
  "main": "index.js"
}通过脚手架创建
如果你使用脚手架(如 Vite、Create React App、Next.js 等)创建子包,流程如下:
# 进入应用目录
cd apps
# 使用 pnpm 创建项目(以 Vite 为例)
pnpm create vite web --template react-ts
# 或使用 Next.js
pnpm create next-app admin
# 修改生成的 package.json,添加 scope
cd web关键步骤:修改 apps/web/package.json,添加统一的包名前缀:
{
  "name": "@myproject/web",  // 添加 scope,保持命名规范
  "version": "1.0.0",
  "dependencies": {
    "@myproject/shared": "workspace:*"  // 引用 workspace 内部包
  }
}注意事项:
- 确保子包的 
package.json中的name字段符合 workspace 命名规范 - 安装完成后运行 
pnpm install更新依赖链接 
5. 安装依赖
# 为整个 workspace 安装依赖
pnpm install
# 为特定包添加依赖
pnpm add lodash --filter @myproject/shared
# 添加 workspace 内部依赖
pnpm add @myproject/shared --filter @myproject/utils --workspace6. 常用命令
安装完成后,以下是日常开发中常用的命令:
# 在所有包中运行脚本
pnpm -r run build
# 在特定包中运行脚本
pnpm --filter @myproject/web dev
# 在所有包中安装依赖
pnpm install -r
# 添加依赖到根目录(通常用于开发工具)
pnpm add -w typescript -D
# 并行运行多个包的脚本
pnpm -r --parallel run dev
# 查看 workspace 中所有包
pnpm list -r --depth 0常见问题
为什么没有一键初始化
Note
许多开发者在接触 pnpm workspace 时会疑惑:为什么不像 create-react-app 或 npm init 那样提供一个一键初始化命令?
原因分析:
PNPM Workspace 并非一个具体的项目模板,而是一个灵活的项目组织方式。不同团队和项目的 monorepo 结构差异很大:
- 目录结构不同:有的团队使用 
packages/,有的使用apps/和libs/,还有的使用services/、clients/等 - 包的类型不同:可能包含前端应用、后端服务、共享库、CLI 工具等
 - 构建工具多样:可能使用 Vite、Webpack、Rollup、Turbopack 等不同工具
 - 技术栈各异:React、Vue、Angular、Node.js、TypeScript 等组合方式众多
 
PNPM 的设计哲学:
PNPM 采用了"最小化、可组合"的设计理念:
- 只提供核心的包管理能力
 - 让开发者根据实际需求灵活组织项目
 - 通过简单的 
pnpm-workspace.yaml配置即可启用 
替代方案:
虽然没有官方的初始化命令,但你可以:
# 最简单的初始化(3 步搞定)
echo "packages:\n  - 'packages/*'" > pnpm-workspace.yaml
mkdir packages
pnpm init建议:
对于首次使用 pnpm workspace 的开发者,手动初始化反而是一个很好的学习过程,能帮助你更好地理解 workspace 的工作原理。
依赖提升
Note
PNPM 默认不会提升依赖,这可能导致某些工具(如 ESLint、Prettier)找不到配置文件或插件。
解决方案:在根目录的 .npmrc 文件中配置:
# 提升所有依赖到根目录 node_modules
shamefully-hoist=true
# 或者只提升特定的包
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*协议版本
Note
使用 workspace:* 协议引用内部包时,发布到 npm 需要特别注意。
{
  "dependencies": {
    "@myproject/shared": "workspace:*"
  }
}最佳实践:
- 开发时使用 
workspace:*或workspace:^ - 发布前 pnpm 会自动将其替换为实际版本号
 - 使用 
pnpm publish -r进行批量发布 
循环依赖
Note
Workspace 中容易出现包之间的循环依赖,导致构建失败。
检测方法:
# 使用 madge 检测循环依赖
npx madge --circular --extensions ts,tsx ./packages解决方案:
- 重新设计包结构,提取共享代码到独立包
 - 使用依赖注入或事件机制解耦
 - 将循环依赖的功能合并到同一个包
 
TypeScript 路径
Note
在 monorepo 中使用 TypeScript 时,需要正确配置路径映射。
在根目录的 tsconfig.json 中:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@myproject/*": ["packages/*/src"]
    }
  }
}同时在每个子包的 tsconfig.json 中继承根配置:
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}热更新
Note
当修改 workspace 中的依赖包时,可能需要手动重启开发服务器。
解决方案:
- 使用构建工具的 watch 模式:
pnpm -r --parallel run dev - 配置 Vite/Webpack 的 
optimizeDeps.exclude排除 workspace 包 
最佳实践
包命名规范:使用 scope(如
@myproject/)统一管理内部包{ "name": "@myproject/ui", "dependencies": { "@myproject/utils": "workspace:*" } }合理划分包:
packages/- 可复用的共享库(UI 组件、工具函数、配置等)apps/- 实际的应用(Web、Admin、Mobile 等)
共享配置:将 ESLint、TypeScript、Prettier 等配置放在根目录
├── tsconfig.json # 基础配置 ├── packages/ │ └── ui/ │ └── tsconfig.json # 继承根配置构建顺序:使用
pnpm -r run build时,pnpm 会自动按依赖关系排序选择性执行:使用
--filter参数只对需要的包执行操作# 只构建 web 应用及其依赖 pnpm --filter @myproject/web build # 只在 packages 目录下的包运行测试 pnpm --filter "./packages/**" testCI/CD 优化:
# .github/workflows/ci.yml - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build all packages run: pnpm -r run build - name: Run tests run: pnpm -r run test
配合 Shadcn UI 使用
Shadcn UI 是一个流行的 UI 组件库,提供了出色的 monorepo 支持。从其官方文档可以学到很多关于在 monorepo 中组织 UI 组件的最佳实践。
核心概念
Shadcn UI 的 CLI 现在能够理解 monorepo 结构,并自动将组件、依赖和注册表依赖安装到正确的路径,同时处理导入路径。
文件结构
apps
└── web         # 你的应用
    ├── app
    │   └── page.tsx
    ├── components
    │   └── login-form.tsx
    ├── components.json
    └── package.json
packages
└── ui          # UI 组件和依赖安装在这里
    ├── src
    │   ├── components
    │   │   └── button.tsx
    │   ├── hooks
    │   ├── lib
    │   │   └── utils.ts
    │   └── styles
    │       └── globals.css
    ├── components.json
    └── package.json快速开始
- 创建新的 monorepo 项目:
 
pnpm dlx shadcn@canary init选择 Next.js (Monorepo) 选项,CLI 会创建包含 web 和 ui 两个工作区的项目,并配置好 Turborepo。
- 添加组件:
 
在你的应用路径中运行 add 命令:
cd apps/web
pnpm dlx shadcn@canary add buttonCLI 会自动:
- 将 button 组件安装到 
packages/ui - 更新 
apps/web中组件的导入路径 
- 导入组件:
 
import { Button } from "@workspace/ui/components/button"
import { useTheme } from "@workspace/ui/hooks/use-theme"
import { cn } from "@workspace/ui/lib/utils"配置要点
每个工作区都必须有 components.json 文件,用于告诉 CLI 如何安装组件。
apps/web/components.json:
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "../../packages/ui/src/styles/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@/components",
    "hooks": "@/hooks",
    "lib": "@/lib",
    "utils": "@workspace/ui/lib/utils",
    "ui": "@workspace/ui/components"
  }
}packages/ui/components.json:
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/styles/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@workspace/ui/components",
    "utils": "@workspace/ui/lib/utils",
    "hooks": "@workspace/ui/hooks",
    "lib": "@workspace/ui/lib",
    "ui": "@workspace/ui/components"
  }
}关键要求
- 每个工作区必须有 
components.json:告诉 CLI 如何安装和导入 - 正确定义别名:确保导入路径正确映射到 workspace 包
 - 保持配置一致:所有 
components.json中的style、iconLibrary和baseColor必须相同 - Tailwind CSS v4:使用 v4 时,在 
components.json中将tailwind.config留空 
最佳实践总结
从 Shadcn UI 的实践中,我们可以学到:
清晰的职责划分:
packages/ui- 存放可复用的 UI 组件、hooks、工具函数apps/*- 存放具体的应用代码和业务逻辑
统一的别名配置:通过
components.json统一管理路径别名,让导入更清晰工具链支持:提供 CLI 工具自动处理组件安装和路径映射,提升开发效率
样式管理集中化:将全局样式放在
packages/ui中,所有应用共享
总结
PNPM Workspace 是高效的 monorepo 解决方案,通过以下特性显著提升开发效率:
- 依赖管理优化:磁盘空间高效、安装速度快、避免幽灵依赖
 - 代码共享便捷:多端应用可轻松共享 UI 组件、工具函数、类型定义
 - 开发体验友好:原生支持、自动依赖排序、灵活的过滤机制
 
快速检查清单
✅ 安装 pnpm 并创建 pnpm-workspace.yaml
✅ 使用 workspace:* 协议管理内部依赖
✅ 合理划分 packages 和 apps 目录
✅ 配置共享的 TypeScript、ESLint 等工具
✅ 使用 changesets 管理版本
虽然初期配置需要一些时间,但它带来的开发体验提升是非常值得的。从小型项目开始,逐步熟悉 workspace,你会发现 monorepo 开发可以如此高效!
希望这篇指南能帮助你顺利上手!