pnpm workspace 與 monorepo 實戰教程 2026

💡 什麼是 Monorepo? Monorepo(單一倉庫)是一種項目管理策略,將多個相關項目放在同一個倉庫中管理。pnpm workspace 是實現 Monorepo 的最佳工具之一。
本文將帶你從 0 到 1 掌握:
- ✅ Monorepo 架構設計原則
- ✅ pnpm workspace 配置詳解
- ✅ 依賴共享與版本管理
- ✅ 性能優化策略
- ✅ CI/CD 集成方案
- ✅ 實際項目案例
一、為什麼選擇 pnpm workspace?
1.1 Monorepo vs Multi-repo
| 維度 | Monorepo | Multi-repo |
|---|---|---|
| 代碼共享 | ✅ 模塊間輕鬆共享 | ❌ 需要 npm 發佈 |
| 依賴管理 | ✅ 統一管理,版本一致 | ❌ 重複安裝,版本可能不一致 |
| 代碼複用 | ✅ 共享組件、工具函數 | ❌ 重複造輪子 |
| CI/CD | ✅ 增量構建,效率高 | ❌ 各自構建,重複工作 |
| 學習成本 | ✅ 一處學習,處處可用 | ❌ 多個倉庫,各自為政 |
| 倉庫大小 | ⚠️ 可能很大 | ✅ 每個倉庫較小 |
| 權限管理 | ⚠️ 權限較粗粒度 | ✅ 精細權限控制 |
1.2 pnpm vs npm/yarn
| 特性 | pnpm | npm | yarn |
|---|---|---|---|
| 磁盤空間 | ⭐⭐⭐⭐⭐(硬鏈接共享) | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 安裝速度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| workspace 支持 | ✅ 原生支持 | ⚠️ 有限支持 | ✅ 支持 |
| 安全性 | ⭐⭐⭐⭐⭐(嚴格隔離) | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 緩存機制 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 社區支持 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
1.3 pnpm 的核心優勢
# pnpm 使用硬鏈接和符號鏈接來共享依賴
# 所有版本的相同包只存儲一次
pnpm install
# 傳統 npm/yarn 每個項目獨立安裝
npm install
# 或
yarn install空間對比示例:
# 10 個項目使用相同的 100 個依賴
# npm/yarn: 10 * 100 = 1000 份依賴副本
# pnpm: 100 份依賴(通過硬鏈接共享)
# 實際空間節省可達 80-90%二、項目結構設計
2.1 推薦的 monorepo 結構
my-monorepo/
├── .git/ # Git 倉庫
├── .github/ # GitHub Actions 配置
├── .vscode/ # VS Code 配置
├── packages/ # 工作區包目錄
│ ├── app/ # 主應用(可執行)
│ │ ├── src/
│ │ ├── package.json
│ │ └── vite.config.ts
│ ├── components/ # 共享組件庫
│ │ ├── src/
│ │ └── package.json
│ ├── utils/ # 工具函數庫
│ │ ├── src/
│ │ └── package.json
│ └── ui/ # UI 組件庫
│ ├── src/
│ └── package.json
├── apps/ # 應用目錄(可選)
│ └── admin/ # 管理後臺
│ ├── src/
│ └── package.json
├── tools/ # 工具腳本
│ ├── scripts/
│ └── configs/
├── .gitignore
├── package.json # 根 package.json
├── pnpm-workspace.yaml # pnpm workspace 配置
├── tsconfig.json # 根 TypeScript 配置
├── tsconfig.base.json # TypeScript 基礎配置
└── README.md2.2 各目錄職責說明
| 目錄 | 職責 | 是否必須 |
|---|---|---|
| packages/ | 可複用的包(組件、工具、UI) | ✅ |
| apps/ | 獨立運行的應用 | ⚠️ 可選 |
| tools/ | 項目工具腳本和配置 | ⚠️ 可選 |
| pnpm-workspace.yaml | workspace 配置 | ✅ |
| tsconfig.base.json | 共享的 TypeScript 配置 | ✅ |
三、pnpm workspace 配置詳解
3.1 初始化 workspace
# 1. 創建項目目錄
mkdir my-monorepo && cd my-monorepo
# 2. 初始化 pnpm workspace
pnpm init
# 3. 創建 pnpm-workspace.yaml
touch pnpm-workspace.yaml
# 4. 創建 packages 目錄
mkdir packages apps3.2 pnpm-workspace.yaml 配置
# pnpm-workspace.yaml
packages:
# 所有包目錄
- 'packages/**'
# 應用目錄
- 'apps/**'
# 可選:排除某些目錄
- '!**/node_modules'
- '!**/.git'
# 配置 workspace 根目錄的依賴安裝
# 這些依賴會安裝在根 node_modules
# 可被所有子包共享
# 注意:建議使用 workspace 協議代替
# 啟用嚴格模式(推薦)
# strict: true
# 配置 peer dependencies 自動安裝
# auto-install-peers: true
# 配置依賴注入(高級特性)
# inject:
# - package: eslint
# version: ^8.0.03.3 根 package.json 配置
{
"name": "@my-monorepo/root",
"private": true,
"version": "1.0.0",
"description": "My Monorepo",
"scripts": {
"build": "pnpm --filter \"./packages/**\" build",
"dev": "pnpm --filter @my-monorepo/app dev",
"test": "pnpm --filter \"./packages/**\" test",
"lint": "pnpm --filter \"./packages/**\" lint",
"clean": "pnpm --filter \"./packages/**\" clean",
"publish": "pnpm --filter \"./packages/**\" publish --access public"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"prettier": "^3.0.0"
},
"pnpm": {
"overrides": {
"lodash": "^4.17.21",
"react": "^18.0.0"
}
}
}3.4 子包 package.json 配置
// packages/components/package.json
{
"name": "@my-monorepo/components",
"version": "1.0.0",
"description": "共享組件庫",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"lint": "eslint ."
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"typescript": "^5.0.0"
}
}四、依賴管理策略
4.1 workspace 協議(推薦)
// packages/app/package.json
{
"dependencies": {
"@my-monorepo/components": "workspace:^",
"@my-monorepo/utils": "workspace:^",
"@my-monorepo/ui": "workspace:1.0.0"
}
}workspace 協議版本說明:
| 版本格式 | 含義 |
|---|---|
workspace:* | 任意版本 |
workspace:^ | 兼容版本(^1.0.0) |
workspace:~ | 補丁版本(~1.0.0) |
workspace:1.0.0 | 精確版本 |
workspace:../utils | 相對路徑 |
4.2 安裝依賴
# 安裝所有依賴(根目錄執行)
pnpm install
# 為特定包安裝依賴
pnpm --filter @my-monorepo/app install
# 在特定包目錄下安裝
cd packages/app
pnpm install
# 添加新依賴到特定包
pnpm --filter @my-monorepo/app add lodash
# 添加開發依賴到根目錄
pnpm add -Dw typescript eslint
# 添加共享依賴(根目錄)
pnpm add -w react react-dom
# 從特定包移除依賴
pnpm --filter @my-monorepo/app remove lodash
# 更新所有依賴
pnpm update
# 更新特定包的依賴
pnpm --filter @my-monorepo/components update
# 檢查過時依賴
pnpm outdated
# 清理未使用的依賴
pnpm prune4.3 依賴安裝策略
# 策略 1:根目錄安裝共享依賴
pnpm add -w react react-dom typescript
# 策略 2:特定包安裝專屬依賴
pnpm --filter @my-monorepo/app add lodash
# 策略 3:使用 workspace 協議引用內部包
pnpm --filter @my-monorepo/app add @my-monorepo/components@workspace
# 策略 4:安裝所有子包的依賴
pnpm install
# 策略 5:只安裝生產依賴
pnpm install --prod
# 策略 6:使用 lockfile 精確安裝
pnpm install --frozen-lockfile4.4 依賴緩存優化
# 查看 pnpm 存儲路徑
pnpm config get store-dir
# 輸出: /Users/username/Library/pnpm/store/v3
# 設置自定義存儲路徑
pnpm config set store-dir /path/to/custom/store
# 清理緩存
pnpm store prune
# 查看緩存大小
pnpm store status
# 驗證緩存完整性
pnpm store verify
# 從緩存中刪除特定包
pnpm store rm lodash五、TypeScript 配置
5.1 根 tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"lib": ["ES2020", "DOM"],
"baseUrl": ".",
"paths": {
"@my-monorepo/*": ["packages/*/src"]
},
"types": ["node"]
},
"files": [],
"references": [
{ "path": "./packages/components" },
{ "path": "./packages/utils" },
{ "path": "./packages/ui" }
]
}5.2 子包 tsconfig.json
// packages/components/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}5.3 路徑別名配置
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@my-monorepo/components": ["packages/components/src"],
"@my-monorepo/utils": ["packages/utils/src"],
"@my-monorepo/ui": ["packages/ui/src"],
"@my-monorepo/app/*": ["packages/app/src/*"]
}
}
}使用示例:
// packages/app/src/App.tsx
import { Button } from '@my-monorepo/components';
import { formatDate } from '@my-monorepo/utils';
import { Card } from '@my-monorepo/ui';
function App() {
return (
<Card>
<Button onClick={() => console.log(formatDate(new Date()))}>
Click Me
</Button>
</Card>
);
}六、腳本管理與執行
6.1 根目錄腳本
{
"scripts": {
// 構建所有包
"build": "pnpm --filter \"./packages/**\" build",
// 構建特定包及其依賴
"build:app": "pnpm --filter @my-monorepo/app... build",
// 並行構建
"build:parallel": "pnpm --parallel --filter \"./packages/**\" build",
// 開發模式
"dev": "pnpm --filter @my-monorepo/app dev",
// 並行開發多個包
"dev:all": "pnpm --parallel --filter \"./packages/**\" dev",
// 測試
"test": "pnpm --filter \"./packages/**\" test",
// 測試特定包
"test:components": "pnpm --filter @my-monorepo/components test",
// 代碼檢查
"lint": "pnpm --filter \"./packages/**\" lint",
// 格式化
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json}\"",
// 清理構建產物
"clean": "pnpm --filter \"./packages/**\" clean",
// 發佈所有包
"publish": "pnpm --filter \"./packages/**\" publish --access public",
// 版本管理
"version:major": "pnpm --filter \"./packages/**\" version major",
"version:minor": "pnpm --filter \"./packages/**\" version minor",
"version:patch": "pnpm --filter \"./packages/**\" version patch"
}
}6.2 執行腳本的方式
# 執行根目錄腳本
pnpm run build
# 執行特定包的腳本
pnpm --filter @my-monorepo/app run dev
# 在包目錄下直接執行
cd packages/app
pnpm run dev
# 並行執行多個腳本
pnpm --parallel --filter @my-monorepo/components --filter @my-monorepo/utils run dev
# 執行依賴包的腳本(會先執行依賴構建)
pnpm --filter @my-monorepo/app... run build
# 跳過緩存執行
pnpm --filter @my-monorepo/app run build --no-cache6.3 腳本執行順序控制
# 使用 --filter 控制執行順序
pnpm --filter @my-monorepo/utils build
pnpm --filter @my-monorepo/components build
pnpm --filter @my-monorepo/ui build
pnpm --filter @my-monorepo/app build
# 使用 ... 自動解析依賴順序
pnpm --filter @my-monorepo/app... build
# 使用 workspace 協議時,pnpm 會自動處理依賴順序七、性能優化策略
7.1 緩存策略
# 啟用構建緩存(vite/webpack 等)
# vite.config.ts
export default {
build: {
cacheDir: '../../node_modules/.vite'
}
}
# 設置 pnpm 緩存大小限制
pnpm config set store-max-size 10GB
# 清理舊緩存
pnpm store prune
# 使用 faster 模式(跳過某些驗證)
pnpm install --faster
# 啟用預構建(pnpm 8+)
pnpm install --prefer-offline7.2 增量構建
# 使用 turbo 進行增量構建
npm install turbo --save-dev
# turbo.json 配置
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
# 運行 turbo 構建
npx turbo build
# 運行 turbo 測試
npx turbo test
# 強制重新構建
npx turbo build --force7.3 內存優化
# 設置 Node.js 內存限制
export NODE_OPTIONS="--max-old-space-size=4096"
# 優化 tsconfig 以減少內存使用
{
"compilerOptions": {
"skipLibCheck": true,
"noEmit": true,
"incremental": true
}
}
# 使用 swc 替代 tsc 進行更快的編譯
pnpm add -D @swc/cli @swc/core7.4 依賴分析
# 分析依賴樹
pnpm ls
# 分析特定包的依賴
pnpm ls --filter @my-monorepo/app
# 查看重複依賴
pnpm dedupe --list
# 執行依賴去重
pnpm dedupe
# 檢查未使用的依賴
pnpm install --check
# 查看依賴大小
pnpm why lodash
# 查看依賴統計
pnpm stats八、CI/CD 集成
8.1 GitHub Actions 配置
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm build
- name: Run tests
run: pnpm test
- name: Run lint
run: pnpm lint
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build and deploy
run: pnpm deploy8.2 緩存策略優化
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
node_modules
**/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache build outputs
uses: actions/cache@v4
with:
path: |
packages/**/dist
apps/**/dist
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-8.3 增量 CI 配置
- name: Run turbo build
run: npx turbo build --cache-dir=.turbo
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Upload turbo cache
uses: actions/upload-artifact@v4
with:
name: turbo-cache
path: .turbo九、常見問題與解決方案
Q1:workspace 協議找不到包?
# 檢查包名是否正確
pnpm --filter @my-monorepo/app ls
# 檢查 workspace 配置
cat pnpm-workspace.yaml
# 確保包已在 workspace 中
pnpm list --filter "@my-monorepo/*"
# 重新安裝依賴
pnpm install
# 檢查 package.json 中的 workspace 協議
# 確保格式正確:workspace:^ 或 workspace:*Q2:TypeScript 路徑別名不生效?
// 確保配置了正確的路徑別名
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@my-monorepo/*": ["packages/*/src"]
}
}
}
// 如果使用 Vite,需要配置 vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@my-monorepo': path.resolve(__dirname, './packages')
}
}
});Q3:依賴版本衝突?
# 使用 pnpm overrides 強制統一版本
{
"pnpm": {
"overrides": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"lodash": "^4.17.21"
}
}
}
# 查看依賴樹
pnpm ls --filter @my-monorepo/app
# 分析重複依賴
pnpm dedupe --list
# 執行去重
pnpm dedupeQ4:構建速度慢?
# 使用 turbo 增量構建
npx turbo build
# 啟用緩存
pnpm install --prefer-offline
# 使用 swc 替代 tsc
pnpm add -D @swc/cli
# 減少內存使用
export NODE_OPTIONS="--max-old-space-size=8192"
# 並行構建
pnpm --parallel buildQ5:如何發佈包?
# 發佈所有包
pnpm --filter "./packages/**" publish --access public
# 發佈特定包
pnpm --filter @my-monorepo/components publish --access public
# 發佈到 npm 私有倉庫
pnpm --filter @my-monorepo/components publish --registry https://npm.pkg.github.com
# 使用 np 進行版本管理
pnpm add -D np
npx np --filter @my-monorepo/components十、完整項目示例
10.1 項目初始化腳本
#!/bin/bash
# init-monorepo.sh
# 創建目錄結構
mkdir -p my-monorepo/{packages,apps,tools/scripts}
cd my-monorepo
# 初始化 pnpm
pnpm init -y
# 創建 pnpm-workspace.yaml
cat > pnpm-workspace.yaml << 'EOF'
packages:
- 'packages/**'
- 'apps/**'
EOF
# 創建根 package.json
cat > package.json << 'EOF'
{
"name": "@my-monorepo/root",
"private": true,
"version": "1.0.0",
"scripts": {
"build": "pnpm --filter \"./packages/**\" build",
"dev": "pnpm --filter @my-monorepo/app dev",
"test": "pnpm --filter \"./packages/**\" test",
"lint": "pnpm --filter \"./packages/**\" lint"
},
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"prettier": "^3.0.0"
}
}
EOF
# 創建 tsconfig.json
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@my-monorepo/*": ["packages/*/src"]
}
}
}
EOF
# 創建示例包
mkdir -p packages/components/src
cat > packages/components/package.json << 'EOF'
{
"name": "@my-monorepo/components",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run"
},
"dependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"typescript": "^5.0.0",
"vitest": "^1.0.0"
}
}
EOF
cat > packages/components/tsconfig.json << 'EOF'
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
EOF
cat > packages/components/src/index.ts << 'EOF'
export const Button = ({ children }: { children: React.ReactNode }) => {
return <button>{children}</button>;
};
EOF
echo "Monorepo 項目初始化完成!"10.2 使用腳本
# 運行初始化腳本
bash init-monorepo.sh
# 安裝依賴
pnpm install
# 構建所有包
pnpm build
# 啟動開發服務器
pnpm dev結語
pnpm workspace 是構建大規模前端項目的利器,它不僅解決了依賴管理的痛點,還提供了優秀的性能和緩存機制。通過合理的架構設計和腳本管理,你可以輕鬆管理包含數十個包的複雜項目。
推薦閱讀:
🚀 提示: 開始你的 monorepo 之旅吧!從一個小型項目開始,逐步擴展,你會發現管理多個包變得前所未有的輕鬆。
延伸阅读
免责声明
本文仅供技术交流和学习参考。涉及第三方服务的链接可能包含 sponsored 标记,请自行核实服务条款、价格和可用性,并遵守当地法律法规。