Monorepo 实战:使用 pnpm + Turborepo 管理大型项目

随着项目规模的增长,你是否在多个仓库间疲于奔命?是否厌倦了重复的依赖安装和复杂的版本管理?是时候拥抱 Monorepo 了!

为什么我们需要 Monorepo?

在传统的 MultiRepo(多仓库)架构中,每个项目都有自己的独立仓库。这种模式在小型项目中工作良好,但随着业务复杂度增加,问题逐渐暴露:

  • 代码共享困难:公共组件、工具函数需要在多个仓库间复制粘贴

  • 依赖管理复杂:不同仓库的依赖版本不一致,导致诡异 bug

  • 跨项目变更繁琐:一个需求需要修改多个仓库,提交、测试、发布流程复杂

  • 开发环境配置重复:每个项目都要配置相似的构建工具、代码检查等

Monorepo(单一仓库) 将多个相关项目放在同一个代码仓库中,让它们可以共享代码、配置和工具链。

技术选型:为什么是 pnpm + Turborepo?

pnpm:下一代包管理器

与 npm/yarn 相比,pnpm 在 Monorepo 场景下具有显著优势:

  • 磁盘空间高效:使用硬链接和符号链接,相同依赖只安装一次

  • 安装速度快:依赖安装速度通常比 npm/yarn 快 2-3 倍

  • 严格的依赖管理:避免幽灵依赖和非法依赖访问

  • 内置 Workspace 支持:原生支持 Monorepo 工作空间

Turborepo:高性能的构建系统

Turborepo 专为 Monorepo 设计,解决了构建和任务执行的痛点:

  • 增量构建:只构建发生变化的部分

  • 并行执行:最大化利用多核 CPU

  • 依赖图感知:智能识别任务依赖关系

  • 远程缓存:团队共享构建缓存,避免重复工作

实战:从零搭建 Monorepo 项目

第一步:项目初始化

bash

复制代码
mkdir my-monorepo && cd my-monorepo
pnpm init

创建 pnpm-workspace.yaml 文件,定义工作空间:

yaml

复制代码
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'

项目结构规划:

text

复制代码
my-monorepo/
├── apps/
│   ├── web-app/          # 前端应用
│   └── admin-app/        # 后台应用
├── packages/
│   ├── ui/               # UI 组件库
│   ├── utils/            # 工具函数
│   └── types/            # 类型定义
├── tools/
│   └── eslint-config/    # ESLint 配置
└── package.json

第二步:配置 Turborepo

安装 Turborepo:

bash

复制代码
pnpm add -Dw turbo

创建 turbo.json 配置文件:

json

复制代码
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", "build/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "type-check": {
      "dependsOn": ["^build"]
    }
  }
}

第三步:创建共享包

1. 共享类型包 (packages/types)

bash

复制代码
mkdir -p packages/types
cd packages/types
pnpm init

json

复制代码
// packages/types/package.json
{
  "name": "@my-project/types",
  "version": "1.0.0",
  "types": "index.ts",
  "scripts": {
    "build": "tsc --build",
    "clean": "rm -rf dist"
  }
}

typescript

复制代码
// packages/types/index.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

export interface ApiResponse<T> {
  data: T;
  success: boolean;
}

2. UI 组件库 (packages/ui)

bash

复制代码
mkdir -p packages/ui
cd packages/ui
pnpm init

json

复制代码
// packages/ui/package.json
{
  "name": "@my-project/ui",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsup src/index.tsx --format esm,cjs --dts --external react",
    "dev": "tsup src/index.tsx --format esm,cjs --dts --external react --watch"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@my-project/types": "workspace:*",
    "tsup": "^6.0.0"
  }
}

tsx

复制代码
// packages/ui/src/Button.tsx
import React from 'react';
import { ButtonHTMLAttributes } from '@my-project/types';

export const Button: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = ({
  children,
  ...props
}) => {
  return <button {...props}>{children}</button>;
};

第四步:创建应用

Web 应用 (apps/web-app)

bash

复制代码
mkdir -p apps/web-app
cd apps/web-app
pnpm init

json

复制代码
// apps/web-app/package.json
{
  "name": "@my-project/web-app",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "@my-project/ui": "workspace:*",
    "@my-project/types": "workspace:*"
  }
}

第五步:配置根目录脚本

json

复制代码
// package.json (根目录)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev --parallel",
    "lint": "turbo run lint",
    "type-check": "turbo run type-check",
    "test": "turbo run test",
    "clean": "turbo run clean"
  },
  "devDependencies": {
    "turbo": "^1.10.0"
  }
}

高级特性与最佳实践

1. 依赖管理技巧

使用 pnpm 的过滤命令操作特定包:

bash

复制代码
# 为所有包添加依赖
pnpm add -r lodash

# 为特定包添加依赖
pnpm add vite --filter @my-project/web-app

# 使用工作空间协议,确保总是使用本地版本
"dependencies": {
  "@my-project/ui": "workspace:*"
}

2. 缓存配置优化

配置 Turborepo 远程缓存,实现团队构建共享:

bash

复制代码
# 在 CI/CD 环境中
turbo run build --api="http://my-turborepo.com" --token="$TURBO_TOKEN"

3. 环境变量管理

创建共享环境配置:

typescript

复制代码
// packages/config/src/env.ts
export const getEnvVars = () => ({
  apiUrl: process.env.API_URL || 'http://localhost:3000',
  environment: process.env.NODE_ENV || 'development',
});

4. CI/CD 流水线优化

yaml

复制代码
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      
      - run: pnpm install
      - run: pnpm type-check
      - run: pnpm lint
      - run: pnpm test
      - run: pnpm build
      
      # 缓存构建输出
      - uses: actions/cache@v3
        with:
          path: |
            apps/*/dist
            packages/*/dist
          key: ${{ runner.os }}-build-${{ hashFiles('**/package.json', '**/tsconfig.json') }}

性能对比:传统方式 vs pnpm + Turborepo

场景 传统 MultiRepo pnpm + Turborepo 提升
初始安装 每个项目独立安装 依赖共享,硬链接 减少 60% 磁盘空间
构建时间 全量构建 增量构建 + 缓存 减少 70% 构建时间
开发启动 分别启动 并行启动 减少 50% 启动时间
跨项目变更 手动同步 自动依赖追踪 减少 80% 协调成本

常见问题与解决方案

Q: 如何处理不同项目的不同 Node.js 版本?

A: 使用 .nvmrc 文件 + volta 或 nvm 管理:

bash

复制代码
# 在每个项目根目录创建 .nvmrc
echo "18" > apps/web-app/.nvmrc
echo "16" > apps/legacy-app/.nvmrc

Q: 如何管理私有包发布?

A: 使用 changesets 进行版本管理和发布:

bash

复制代码
pnpm add -Dw @changesets/cli
pnpm changeset init

总结

通过 pnpm + Turborepo 的组合,我们成功构建了一个高效、可扩展的 Monorepo 架构:

  • 🚀 极速体验:依赖安装和构建速度大幅提升

  • 🔗 代码共享:轻松实现组件和工具函数的复用

  • 🧩 模块化架构:清晰的包边界和依赖关系

  • ⚡ 开发效率:热重载、并行执行提升开发体验

  • 🛠 统一工具链:一致的代码质量和构建流程

Monorepo 不是银弹,但在中大型项目中,它能够显著提升团队的协作效率和开发体验。现在就开始重构你的项目架构,享受现代化开发工作流带来的便利吧!

相关推荐
ByteCraze39 分钟前
如何处理大模型幻觉问题?
前端·人工智能·深度学习·机器学习·node.js
fruge39 分钟前
技术面试复盘:高频算法题的前端实现思路(防抖、节流、深拷贝等)
前端·算法·面试
Mike_jia42 分钟前
LoggiFly:开源Docker日志监控神器,实时洞察容器健康的全栈方案
前端
风语者日志44 分钟前
CTFSHOW菜狗杯—WEB签到
前端·web安全·ctf·小白入门
27669582921 小时前
最新 _rand 分析
前端·javascript·数据库·node·rand·231滑块·_rand分析
一 乐1 小时前
宠物医院预约|宠物医院|基于SprinBoot+vue的宠物医院预约管理系统源码+数据库+文档)
java·前端·数据库·vue.js·后端·springboot
v***5651 小时前
分布式WEB应用中会话管理的变迁之路
前端·分布式
x***38161 小时前
Go-Gin Web 框架完整教程
前端·golang·gin
晓得迷路了1 小时前
栗子前端技术周刊第 108 期 - npm 沙虫攻击 2.0、Ant Design 6.0、Playwright 1.57...
前端·javascript·css