Monorepo 架构下的前端工程化实践:pnpm + Turborepo 从入门到落地

一、为什么需要 Monorepo?

当团队项目发展到一定阶段,你会遇到这些问题:

  • 公共逻辑复制粘贴:工具函数在 5 个项目里各有一份,改 bug 要改 5 次
  • 版本管理混乱:A 项目依赖 UI 库 1.2.0,B 项目依赖 1.3.0,C 项目直接本地 copy 了一份
  • 联调成本高昂:改一个公共组件,要 npm link 到各个项目测试
  • CI/CD 重复配置:每个仓库配一套流水线,维护 nightmare

Monorepo 的核心价值:在一个仓库中管理多个相关项目,代码共享、依赖统一、原子提交。

二、Monorepo 方案对比

方案 工具链 优点 缺点 适用规模
Lerna yarn/npm 生态成熟 性能差、维护慢 中小型
Nx 专用 CLI 功能强大 学习曲线陡 大型
Turborepo pnpm/npm 极速、云缓存 相对较新 中大型
Rush 微软出品 企业级 配置复杂 超大型

本文选择 pnpm + Turborepo,理由:速度快、配置简单、社区活跃。

三、项目初始化与架构设计

3.1 目录结构设计

复制代码
my-monorepo/
├── apps/                    # 应用层
│   ├── web-admin/          # 管理后台
│   ├── web-portal/         # 用户端
│   └── mobile-h5/          # H5 页面
├── packages/               # 共享包
│   ├── ui/                 # 组件库
│   ├── utils/              # 工具函数
│   ├── hooks/              # 共享 hooks
│   ├── eslint-config/      # 共享 ESLint 配置
│   └── ts-config/          # 共享 TS 配置
├── turbo.json              # Turborepo 配置
├── pnpm-workspace.yaml     # pnpm 工作区
└── package.json

3.2 pnpm 工作区配置

复制代码
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

# 依赖提升规则(避免重复安装)
shared-workspace-lockfile: true
link-workspace-packages: true

// package.json (根目录)
{
  "name": "my-monorepo",
  "private": true,
  "packageManager": "pnpm@8.15.0",
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev --parallel",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean && rm -rf node_modules",
    "changeset": "changeset",
    "version-packages": "changeset version",
    "release": "turbo run build --filter=docs^... && changeset publish"
  },
  "devDependencies": {
    "@changesets/cli": "^2.27.1",
    "turbo": "^1.12.0"
  }
}

3.3 Turborepo 管道配置

复制代码
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "clean": {
      "cache": false
    }
  }
}

关键概念解释:

  • ^build:依赖项需要先构建(拓扑排序执行)
  • outputs:缓存这些目录,下次直接复用
  • cache: false:不缓存,实时执行

四、共享包开发实战

4.1 创建 UI 组件库

复制代码
// packages/ui/src/components/Button.tsx
import React from 'react';

export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  children,
  onClick
}) => {
  const baseStyles = 'rounded font-medium transition-colors';
  const variants = {
    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
    secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
    danger: 'bg-red-500 hover:bg-red-600 text-white'
  };
  const sizes = {
    sm: 'px-3 py-1 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  };

  return (
    <button
      className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

// packages/ui/src/components/Button.tsx
import React from 'react';

export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  children,
  onClick
}) => {
  const baseStyles = 'rounded font-medium transition-colors';
  const variants = {
    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
    secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
    danger: 'bg-red-500 hover:bg-red-600 text-white'
  };
  const sizes = {
    sm: 'px-3 py-1 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  };

  return (
    <button
      className={`${baseStyles} ${variants[variant]} ${sizes[size]}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

// packages/ui/src/index.ts
export { Button } from './components/Button';
export { Input } from './components/Input';
export { Modal } from './components/Modal';
// ...

4.2 共享配置包

复制代码
// packages/eslint-config/index.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier'
  ],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  settings: {
    react: {
      version: 'detect'
    }
  },
  rules: {
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off'
  }
};

// packages/eslint-config/package.json
{
  "name": "@myrepo/eslint-config",
  "version": "0.0.1",
  "main": "index.js",
  "files": ["*.js"],
  "dependencies": {
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-react": "^7.33.0",
    "eslint-plugin-react-hooks": "^4.6.0"
  }
}

// apps/web-admin/.eslintrc.js
{
  "extends": ["@myrepo/eslint-config"]
}

五、应用层开发

5.1 管理后台项目

复制代码
// apps/web-admin/package.json
{
  "name": "@myrepo/web-admin",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3000",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@myrepo/ui": "workspace:*",
    "@myrepo/utils": "workspace:*",
    "@myrepo/hooks": "workspace:*",
    "next": "14.1.0",
    "react": "^18.2.0"
  },
  "devDependencies": {
    "@myrepo/tsconfig": "workspace:*",
    "@myrepo/eslint-config": "workspace:*"
  }
}

// apps/web-admin/src/pages/index.tsx
import { Button } from '@myrepo/ui';
import { formatDate } from '@myrepo/utils';
import { useAuth } from '@myrepo/hooks';

export default function Dashboard() {
  const { user } = useAuth();
  
  return (
    <div>
      <h1>管理后台</h1>
      <p>当前时间:{formatDate(new Date())}</p>
      <p>欢迎,{user?.name}</p>
      <Button variant="primary" onClick={() => console.log('clicked')}>
        操作按钮
      </Button>
    </div>
  );
}

5.2 本地开发热更新

复制代码
# 根目录执行,同时启动所有应用的 dev 模式
pnpm dev

# 或者只启动特定应用
pnpm --filter @myrepo/web-admin dev

# 添加依赖到特定包
pnpm --filter @myrepo/ui add lodash

# 添加依赖到根目录(共享 devDependencies)
pnpm add -D typescript -w

六、版本管理与发布

6.1 Changeset 工作流

复制代码
# 安装
pnpm add -D @changesets/cli -w

# 初始化
pnpm changeset init

# 开发完功能后,添加变更集
pnpm changeset

# 选择要发布的包
# 填写变更描述(支持 minor/major/patch)

# 版本号自动更新 + 生成 CHANGELOG
pnpm version-packages

# 构建并发布到 npm
pnpm release

// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@myrepo/web-admin", "@myrepo/web-portal"]
}

七、CI/CD 集成

复制代码
# .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
        with:
          fetch-depth: 2  # Turborepo 需要 git 历史
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Setup Turborepo cache
        uses: dtinth/setup-github-actions-caching-for-turbo@v1
      
      - name: Lint
        run: pnpm lint
      
      - name: Test
        run: pnpm test
      
      - name: Build
        run: pnpm build

  # 只构建变更的应用
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      
      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - name: Install
        run: pnpm install
      
      - name: Build affected
        run: |
          # 只构建受影响的项目
          npx turbo run build --filter=[HEAD^1]

八、性能优化:Turborepo 远程缓存

复制代码
# 登录 Vercel 远程缓存
npx turbo login

# 链接到远程缓存
npx turbo link

# 后续构建会自动上传/下载缓存
# 团队成员共享构建结果,大幅提升 CI 速度

九、常见问题与解决方案

问题 1:循环依赖

复制代码
# 检测循环依赖
pnpm m ls --json | grep -A5 "cycles"

问题 2:类型定义不同步

复制代码
// packages/ui/tsconfig.json
{
  "extends": "@myrepo/tsconfig/base.json",
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true  // 生成源映射,跳转更准确
  }
}

问题 3:Husky 在 Monorepo 中的配置

复制代码
// package.json (根目录)
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

十、总结

Monorepo 不是银弹,但在以下场景收益巨大:

  • 多个项目共享组件库/工具函数
  • 需要原子提交保证多包一致性
  • 团队规模 10+ 人,需要统一工程规范

迁移建议: 小团队先用 pnpm workspace,需要任务编排时引入 Turborepo,稳定后再上 Changeset 版本管理。

🚀 真实收益数据: 某 30 人前端团队迁移 Monorepo 后,组件库发布频率从每月 1 次提升到每周 3 次,重复代码减少 60%,新人 onboarding 时间从 3 天缩短到 2 小时。

相关推荐
mCell6 小时前
当代码不再为人而写:Claude Code 零注释背后的 Harness 逻辑
架构·ai编程·claude
jump_jump7 小时前
用 3100 个数字造一台计算机
性能优化·架构·typescript
徐小夕9 小时前
我用 AI 撸了个开源"万能预览器":浏览器直接打开 Office、CAD 和 3D 模型
前端·vue.js·github
小码哥_常10 小时前
Flutter Android 延迟加载代码指南:提升应用性能的关键
前端
这是个栗子10 小时前
TypeScript(三)
前端·javascript·typescript·react
kvo7f2JTy10 小时前
基于机器学习算法的web入侵检测系统设计与实现
前端·算法·机器学习
北风toto10 小时前
前端CSS样式详细笔记
前端·css·笔记
nanfeiyan10 小时前
git commit
前端
KaneLogger11 小时前
如何把AI方面的先发优势转化为结构优势
人工智能·程序员·架构