Develop

pnpm Workspace + Turborepo:现代前端 Monorepo 工程化实战

✎ -- 字 🕐 -- 分钟
字号

单仓库多包管理(Monorepo)是大型前端工程的标配。相比 npm/yarn,pnpm 的硬链接机制让依赖安装速度提升 2-3 倍,配合 Turborepo 的智能缓存,可以实现真正的增量构建。本文从零搭建一套完整方案。

一、为什么选 pnpm + Turborepo?

📦
pnpm 核心优势
  • 硬链接存储:同一版本的包全局只存一份,节省磁盘空间
  • 严格隔离:包只能访问自己声明的依赖,消除幽灵依赖
  • 原生 workspace:内置多包管理,无需额外插件
Turborepo 核心优势
  • 远程缓存:CI 共享构建缓存,重复构建接近零耗时
  • 并行执行:自动分析任务依赖图,最大化并行度
  • 增量构建:只重建受影响的包

二、项目结构设计

my-monorepo/
├── apps/
│   ├── web/          # Next.js 前端应用
│   └── admin/        # Vite + React 管理后台
├── packages/
│   ├── ui/           # 共享 UI 组件库
│   ├── utils/        # 工具函数库
│   └── config/       # 共享配置(ESLint、TypeScript 等)
├── package.json
├── pnpm-workspace.yaml
└── turbo.json

三、初始化项目

Step 1:安装 pnpm

corepack enable
corepack prepare pnpm@latest --activate
pnpm --version  # 9.x

Step 2:创建目录

mkdir my-monorepo && cd my-monorepo
mkdir -p apps/web apps/admin packages/ui packages/utils packages/config

Step 3:pnpm-workspace.yaml

packages:
  - apps/*
  - packages/*

Step 4:根 package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo build",
    "dev":   "turbo dev",
    "lint":  "turbo lint",
    "test":  "turbo test",
    "clean": "turbo clean"
  },
  "devDependencies": { "turbo": "^2.0.0" },
  "engines": { "node": ">=18", "pnpm": ">=9" }
}

Step 5:turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "package.json", "tsconfig.json"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev":   { "cache": false, "persistent": true },
    "lint":  { "dependsOn": ["^build"] },
    "test":  { "dependsOn": ["^build"] },
    "clean": { "cache": false }
  }
}

四、共享 UI 组件库

// packages/ui/package.json
{
  "name": "@myapp/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs --dts",
    "dev": "tsup src/index.ts --format esm,cjs --dts --watch"
  },
  "peerDependencies": { "react": "^18.0.0" },
  "devDependencies": { "tsup": "^8.0.0", "typescript": "^5.0.0" }
}
// packages/ui/src/components/Button.tsx
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
}

export function Button({ variant = 'primary', size = 'md', children, onClick }: ButtonProps) {
  const base = 'inline-flex items-center rounded font-medium transition-all';
  const variants = {
    primary:   'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    ghost:     'border border-gray-300 hover:bg-gray-50',
  };
  const sizes = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2', lg: 'px-6 py-3 text-lg' };
  return <button className={`${base} ${variants[variant]} ${sizes[size]}`} onClick={onClick}>{children}</button>;
}

五、共享配置包

// packages/config/eslint-base.js
module.exports = {
  extends: ['eslint:recommended'],
  rules: { 'no-console': 'warn', 'no-unused-vars': 'error' },
  env: { node: true, es2022: true },
};
// packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "strict": true, "skipLibCheck": true, "esModuleInterop": true,
    "module": "ESNext", "moduleResolution": "bundler", "target": "ES2022",
    "declaration": true, "declarationMap": true, "sourceMap": true
  }
}

六、在应用里引用共享包

// apps/web/package.json(关键部分)
{
  "dependencies": {
    "@myapp/ui": "workspace:*",
    "@myapp/utils": "workspace:*",
    "next": "^14.0.0",
    "react": "^18.0.0"
  }
}
// apps/web/src/app/page.tsx
import { Button } from '@myapp/ui';
import { formatDate } from '@myapp/utils';

export default function Home() {
  return (
    <div>
      <p>今天是 {formatDate(new Date())}</p>
      <Button variant="primary">开始使用</Button>
    </div>
  );
}

七、常用命令速查

pnpm install                          # 安装所有依赖
pnpm dev                              # 启动所有应用(并行)
pnpm --filter @myapp/web dev          # 只启动某个包
pnpm --filter @myapp/web add axios    # 为某个包添加依赖
pnpm add -Dw typescript               # 为根 workspace 添加开发依赖

八、构建加速实测

# 首次构建
pnpm build
# Tasks: 3 successful | Cached: 0 | Time: 18.2s

# 未修改,再次构建
pnpm build
# Tasks: 3 successful | Cached: 3 cached | Time: 312ms  <-- 全命中!

# 只改了 ui 包
pnpm build
# Tasks: 3 successful | Cached: 1 cached | Time: 8.1s

九、远程缓存(CI 共享)

npx turbo login
npx turbo link
# 之后 CI 自动共享缓存,团队成员构建也能命中

十、版本管理与发布

pnpm add -Dw @changesets/cli
pnpm changeset init
pnpm changeset        # 记录变更(交互式)
pnpm changeset version  # 升版本号 + 生成 CHANGELOG.md
pnpm publish -r --filter @myapp/ui --filter @myapp/utils

常见问题

幽灵依赖报错

# Cannot find module 'lodash'(未显式安装)
pnpm --filter @myapp/web add lodash

workspace 链接未更新

pnpm --filter @myapp/ui build  # 重建 ui 包

turbo 缓存失效

pnpm build --force  # 忽略缓存强制重建

核心要点:workspace:* 协议引用内部包、dependsOn: ["^build"] 声明构建依赖、Changesets 管理版本——这三点是这套方案的精髓。