Monorepo 最佳实践

4 阅读3分钟

Monorepo(单一代码库)是现代前端工程化的重要趋势,以下是完整的架构思路和最佳实践方案。

一、Monorepo 核心架构设计

1. 目录结构设计

your-project/
├── packages/                    # 所有子包
│   ├── core/                    # 核心包
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   ├── ui/                       # UI组件库
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   ├── utils/                    # 工具函数
│   │   ├── src/
│   │   └── package.json
│   ├── app1/                      # 应用1
│   │   ├── src/
│   │   ├── package.json
│   │   └── vite.config.ts
│   └── app2/                      # 应用2
│       ├── src/
│       ├── package.json
│       └── vite.config.ts
├── configs/                       # 共享配置
│   ├── eslint/
│   ├── prettier/
│   ├── typescript/
│   └── vite/
├── scripts/                        # 构建脚本
│   ├── build.js
│   ├── deploy.js
│   └── release.js
├── docs/                           # 文档
├── tests/                          # 测试
│   ├── unit/
│   └── e2e/
├── package.json
├── pnpm-workspace.yaml             # 工作空间配置
├── turbo.json                       # Turborepo配置
├── tsconfig.base.json              # TypeScript基础配置
└── .npmrc                          # npm配置

二、技术选型与工具链

1. 包管理工具

pnpm + workspace(推荐)

# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'configs/*'
  - 'apps/*'
  - 'docs'
// package.json
{
  "name": "monorepo-project",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "changeset": "changeset",
    "version": "changeset version",
    "release": "turbo run build && changeset publish"
  },
  "devDependencies": {
    "@changesets/cli": "^2.26.0",
    "turbo": "^1.10.0",
    "typescript": "^5.0.0"
  },
  "packageManager": "pnpm@8.0.0",
  "engines": {
    "node": ">=16",
    "pnpm": ">=8"
  }
}

2. 构建编排工具

Turborepo 配置

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["NODE_ENV"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "typecheck": {
      "dependsOn": ["^typecheck"],
      "outputs": []
    }
  }
}

3. TypeScript 配置

// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "composite": true,
    "incremental": true,
    "paths": {
      "@core/*": ["./packages/core/src/*"],
      "@ui/*": ["./packages/ui/src/*"],
      "@utils/*": ["./packages/utils/src/*"]
    }
  },
  "exclude": ["node_modules", "dist"]
}

三、核心包开发规范

1. 共享配置管理

ESLint 共享配置

// configs/eslint/index.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'prettier'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: 12,
    sourceType: 'module'
  },
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn'
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
}

2. 工具包开发示例

// packages/utils/src/request.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

export class HttpClient {
  private instance: AxiosInstance;

  constructor(config?: AxiosRequestConfig) {
    this.instance = axios.create({
      timeout: 10000,
      ...config
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    this.instance.interceptors.request.use(
      (config) => {
        // 添加token等
        return config;
      },
      (error) => Promise.reject(error)
    );
  }

  async get<T>(url: string, params?: any): Promise<T> {
    const response = await this.instance.get<T>(url, { params });
    return response.data;
  }

  async post<T>(url: string, data?: any): Promise<T> {
    const response = await this.instance.post<T>(url, data);
    return response.data;
  }
}

export default new HttpClient();

3. UI组件库封装

// packages/ui/src/Button/index.tsx
import React from 'react';
import classNames from 'classnames';
import './style.css';

export interface ButtonProps {
  type?: 'primary' | 'default' | 'danger';
  size?: 'large' | 'middle' | 'small';
  disabled?: boolean;
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
  children?: React.ReactNode;
}

export const Button: React.FC<ButtonProps> = ({
  type = 'default',
  size = 'middle',
  disabled = false,
  onClick,
  children
}) => {
  const classes = classNames(
    'btn',
    `btn-${type}`,
    `btn-${size}`,
    {
      'btn-disabled': disabled
    }
  );

  return (
    <button
      className={classes}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

Button.displayName = 'Button';

四、应用层开发规范

1. 应用入口配置

// packages/app1/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@core': path.resolve(__dirname, '../core/src'),
      '@ui': path.resolve(__dirname, '../ui/src'),
      '@utils': path.resolve(__dirname, '../utils/src')
    }
  },
  server: {
    port: 3001,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  build: {
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          ui: ['@ui']
        }
      }
    }
  }
});

2. 环境变量管理

# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=App1 Development

# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=App1 Production

五、版本管理与发布

1. Changesets 配置

// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [
    ["@project/core", "@project/ui"]
  ],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@project/app1", "@project/app2"],
  "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
    "onlyUpdatePeerDependentsWhenOutOfRange": true
  }
}

2. 发布脚本

// scripts/release.js
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const version = process.argv[2];
if (!version) {
  console.error('请指定版本号');
  process.exit(1);
}

try {
  // 更新版本号
  execSync(`pnpm changeset version ${version}`, { stdio: 'inherit' });
  
  // 构建所有包
  execSync('pnpm run build', { stdio: 'inherit' });
  
  // 运行测试
  execSync('pnpm run test', { stdio: 'inherit' });
  
  // 发布到npm
  execSync('pnpm changeset publish', { stdio: 'inherit' });
  
  // 提交版本更新
  execSync('git add .', { stdio: 'inherit' });
  execSync(`git commit -m "chore: release v${version}"`, { stdio: 'inherit' });
  execSync(`git tag v${version}`, { stdio: 'inherit' });
  execSync('git push && git push --tags', { stdio: 'inherit' });
  
  console.log(`✅ 发布成功 v${version}`);
} catch (error) {
  console.error('发布失败:', error);
  process.exit(1);
}

六、CI/CD 配置

GitHub Actions 示例

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
          
      - name: Install dependencies
        run: pnpm install
        
      - name: Lint
        run: pnpm run lint
        
      - name: Type check
        run: pnpm run typecheck
        
      - name: Test
        run: pnpm run test
        
      - name: Build
        run: pnpm run build
        
      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: |
            packages/*/dist
            apps/*/dist

  deploy:
    needs: validate
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: dist
          
      - name: Deploy to server
        uses: easingthemes/ssh-deploy@main
        with:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          ARGS: "-rlgoDzvc -i --delete"
          SOURCE: "dist/"
          REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
          REMOTE_USER: ${{ secrets.REMOTE_USER }}
          TARGET: ${{ secrets.REMOTE_TARGET }}

七、最佳实践总结

1. 架构原则

  • 关注点分离:核心包、UI库、工具函数、业务应用清晰分层
  • 依赖明确:使用 pnpm 管理内部依赖,避免循环依赖
  • 构建优化:利用 Turborepo 缓存,加速构建
  • 类型共享:TypeScript 配置统一,类型定义共享

2. 开发规范

  • 版本管理:使用 Changesets 统一管理版本发布
  • 代码质量:共享 ESLint、Prettier 配置
  • 测试策略:单元测试 + E2E 测试结合
  • 文档建设:每个包必须有 README 和使用文档

3. 性能优化

  • 按需加载:UI库支持按需引入
  • 构建缓存:合理配置 Turborepo 缓存策略
  • 依赖分析:定期检查和优化依赖关系
  • 分包策略:合理的代码分割和公共依赖提取

4. 团队协作

  • 代码审查:严格的 PR 审查流程
  • 提交规范:使用 Commitlint 规范提交信息
  • 变更记录:自动生成 CHANGELOG
  • 知识共享:建立技术文档和决策记录

5. 常见问题处理

  • 循环依赖:使用 pnpm ls --depth=10 检查依赖关系
  • 构建速度:合理配置 Turborepo 缓存和并行构建
  • 版本冲突:统一核心依赖版本,使用 resolutions 或 overrides
  • 类型错误:确保所有包使用相同的 TypeScript 版本

通过以上架构设计和最佳实践,可以构建一个可维护、可扩展、高效的 Monorepo 项目,适合大型团队协作和复杂业务场景。