PNPM Workspace 实践指南

PNPM Workspace 实践指南

本文由 AI 协助创作
2025-26-10
PNPM Workspace 是高效的 monorepo 解决方案,支持在单仓库中管理多个包,实现 Web、插件、移动端等多端代码共享。文章讲解从安装配置到日常使用的完整流程,解答依赖提升、协议版本、循环依赖、热更新等常见问题。

介绍

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 --version

2. 创建配置

在项目根目录创建 pnpm-workspace.yaml 文件:

packages:
  # 所有在 packages/ 目录下的包
  - 'packages/*'
  # 所有在 apps/ 目录下的应用
  - 'apps/*'
  # 也可以排除某些目录
  # - '!**/test/**'

3. 创建项目结构

# 创建目录结构
mkdir -p packages/shared packages/utils
mkdir -p apps/web apps/admin

# 在根目录初始化
pnpm init

4. 创建子包

手动创建

在每个子包目录中创建 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 --workspace

6. 常用命令

安装完成后,以下是日常开发中常用的命令:

# 在所有包中运行脚本
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-appnpm 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 包

最佳实践

  1. 包命名规范:使用 scope(如 @myproject/)统一管理内部包

    {
      "name": "@myproject/ui",
      "dependencies": {
        "@myproject/utils": "workspace:*"
      }
    }
  2. 合理划分包

    • packages/ - 可复用的共享库(UI 组件、工具函数、配置等)
    • apps/ - 实际的应用(Web、Admin、Mobile 等)
  3. 共享配置:将 ESLint、TypeScript、Prettier 等配置放在根目录

    ├── tsconfig.json         # 基础配置
    ├── packages/
    │   └── ui/
    │       └── tsconfig.json # 继承根配置
  4. 构建顺序:使用 pnpm -r run build 时,pnpm 会自动按依赖关系排序

  5. 选择性执行:使用 --filter 参数只对需要的包执行操作

    # 只构建 web 应用及其依赖
    pnpm --filter @myproject/web build
    
    # 只在 packages 目录下的包运行测试
    pnpm --filter "./packages/**" test
  6. CI/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

快速开始

  1. 创建新的 monorepo 项目
pnpm dlx shadcn@canary init

选择 Next.js (Monorepo) 选项,CLI 会创建包含 webui 两个工作区的项目,并配置好 Turborepo。

  1. 添加组件

在你的应用路径中运行 add 命令:

cd apps/web
pnpm dlx shadcn@canary add button

CLI 会自动:

  • 将 button 组件安装到 packages/ui
  • 更新 apps/web 中组件的导入路径
  1. 导入组件
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"
  }
}

关键要求

  1. 每个工作区必须有 components.json:告诉 CLI 如何安装和导入
  2. 正确定义别名:确保导入路径正确映射到 workspace 包
  3. 保持配置一致:所有 components.json 中的 styleiconLibrarybaseColor 必须相同
  4. Tailwind CSS v4:使用 v4 时,在 components.json 中将 tailwind.config 留空

最佳实践总结

从 Shadcn UI 的实践中,我们可以学到:

  1. 清晰的职责划分

    • packages/ui - 存放可复用的 UI 组件、hooks、工具函数
    • apps/* - 存放具体的应用代码和业务逻辑
  2. 统一的别名配置:通过 components.json 统一管理路径别名,让导入更清晰

  3. 工具链支持:提供 CLI 工具自动处理组件安装和路径映射,提升开发效率

  4. 样式管理集中化:将全局样式放在 packages/ui 中,所有应用共享

总结

PNPM Workspace 是高效的 monorepo 解决方案,通过以下特性显著提升开发效率:

  • 依赖管理优化:磁盘空间高效、安装速度快、避免幽灵依赖
  • 代码共享便捷:多端应用可轻松共享 UI 组件、工具函数、类型定义
  • 开发体验友好:原生支持、自动依赖排序、灵活的过滤机制

快速检查清单

✅ 安装 pnpm 并创建 pnpm-workspace.yaml
✅ 使用 workspace:* 协议管理内部依赖
✅ 合理划分 packages 和 apps 目录
✅ 配置共享的 TypeScript、ESLint 等工具
✅ 使用 changesets 管理版本

虽然初期配置需要一些时间,但它带来的开发体验提升是非常值得的。从小型项目开始,逐步熟悉 workspace,你会发现 monorepo 开发可以如此高效!

希望这篇指南能帮助你顺利上手!

参考

最后更新于