pnpm Workspace 最佳实践与进阶技巧
James Park | 2026-09-02T01:08:15 | JavaScript, DevOps
全面讲解 pnpm Workspace 的配置和使用,包括严格依赖管理、内容寻址存储、workspace 协议、发布策略和常见问题解决。
# pnpm Workspace 最佳实践与进阶技巧 ## 为什么选 pnpm npm 和 yarn 的 node_modules 是平铺的(hoisting),这导致了幽灵依赖问题 — 你的代码可以引用未在 package.json 中声明的包。pnpm 用硬链接 + 符号链接创建严格的 node_modules 结构,彻底解决这个问题。 此外,pnpm 的内容寻址存储(content-addressable store)意味着同一个包版本全局只存一份。 ## 初始化 Workspace ```yaml # pnpm-workspace.yaml packages: - "apps/*" - "packages/*" - "tools/*" ``` ```json // package.json (root) { "name": "my-monorepo", "private": true, "scripts": { "dev": "turbo dev", "build": "turbo build", "lint": "turbo lint", "clean": "turbo clean && rm -rf node_modules" }, "devDependencies": { "turbo": "^2.0.0" }, "packageManager": "pnpm@9.7.0" } ``` ## workspace 协议 ```json // apps/web/package.json { "dependencies": { // workspace:* - 使用 workspace 中的最新版本 "@repo/ui": "workspace:*", // workspace:^ - 发布时转为 ^x.y.z "@repo/utils": "workspace:^", // 外部依赖正常写版本号 "react": "^18.3.0" } } ``` 发布时 `workspace:*` 会被替换为实际版本号。 ## .npmrc 配置 ```ini # .npmrc # 严格模式:禁止幽灵依赖 strict-peer-dependencies=true # 自动安装 peer dependencies auto-install-peers=true # 提升到根目录的包(尽量少用) public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier* # 不提升的包 shamefully-hoist=false # 注册源 registry=https://registry.npmmirror.com ``` ## 依赖管理命令 ```bash # 给特定包添加依赖 pnpm add axios --filter @repo/api-client # 给所有包添加开发依赖 pnpm add -Dw typescript # 给根目录添加开发依赖 pnpm add -Dw prettier # 给匹配的包添加 pnpm add jest --filter "./packages/*" # 删除依赖 pnpm remove lodash --filter @repo/utils # 更新所有包的某个依赖 pnpm update react --recursive ``` ## 脚本执行 ```bash # 在特定包中执行脚本 pnpm --filter @repo/web dev # 在所有包中执行(如果存在该脚本) pnpm -r run build # 按拓扑顺序执行(先构建依赖) pnpm -r --workspace-concurrency=4 run build # 只在有变更的包中执行 pnpm -r --filter="...[origin/main]" run test # 在根目录执行 pnpm -w run format ``` ## 版本管理和发布 ### 使用 Changesets ```bash pnpm add -Dw @changesets/cli pnpm changeset init ``` ```json // .changeset/config.json { "$schema": "https://unpkg.com/@changesets/config/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [["@repo/ui", "@repo/utils"]], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch" } ``` 工作流: ```bash # 记录变更 pnpm changeset # -> 选择变更的包 # -> 选择版本类型 (major/minor/patch) # -> 输入变更描述 # 应用版本变更 pnpm changeset version # 发布到 npm pnpm changeset publish ``` ## 内容寻址存储 理解 pnpm 的存储机制: ```bash # 查看 store 位置 pnpm store path # ~/.local/share/pnpm/store/v3 # 查看 store 状态 pnpm store status # 清理未被引用的包 pnpm store prune ``` node_modules 结构: ``` node_modules/ ├── .pnpm/ # 虚拟存储(硬链接到全局 store) │ ├── react@18.3.1/ │ │ └── node_modules/ │ │ └── react/ # 硬链接到 store │ └── ... ├── react -> .pnpm/react@18.3.1/node_modules/react # 符号链接 └── @repo/ └── ui -> ../../packages/ui # workspace 符号链接 ``` ## CI 缓存策略 ```yaml # GitHub Actions - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 9 - name: Get pnpm store directory shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Cache pnpm store uses: actions/cache@v4 with: path: ${{ env.STORE_PATH }} key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: pnpm-store- - run: pnpm install --frozen-lockfile ``` ## 常见问题 ### 问题 1: 某些工具不支持符号链接 ```ini # .npmrc # 取消某些包的符号链接隔离 node-linker=hoisted # 最后手段,不推荐 ``` ### 问题 2: Peer dependency 警告 ```bash # 方案 1: 自动安装 auto-install-peers=true # 方案 2: 允许特定版本不匹配 peerDependencyRules: allowedVersions: "react": "18" ``` ### 问题 3: Monorepo 中的 TypeScript 路径 ```json // tsconfig.json (root) { "compilerOptions": { "paths": { "@repo/ui/*": ["./packages/ui/src/*"], "@repo/utils/*": ["./packages/utils/src/*"] } } } ``` ## 性能对比 | 操作 | npm | yarn | pnpm | |------|-----|------|------| | 冷安装 | 45s | 38s | 18s | | 有缓存安装 | 22s | 15s | 5s | | 磁盘占用 | 200MB | 180MB | 80MB | (数据为典型中型项目参考值) ## 总结 pnpm 的严格依赖管理和高效存储机制使它成为 Monorepo 包管理的首选。配合 workspace 协议和 Changesets,可以建立完善的多包管理和发布流程。性能优势在 CI 环境中尤为明显。