工程化与工具链 #Monorepo#Turborepo#pnpm

从零搭建 Monorepo:Turborepo + pnpm 最佳实践

· 13 min read
Code on screen

一、为什么要用 Monorepo?

日常开发中,我们经常会遇到这类场景:同一个团队维护多个关联项目,比如前台业务站、后台管理系统、文档站点,同时拥有通用组件、工具函数、TS 配置、ESLint 规则等公共能力。

如果采用传统多仓库管理,会出现很多棘手问题:

  • 公共代码冗余:通用工具、组件需要多仓库拷贝维护,迭代不同步,极易出现版本不一致问题

  • 依赖管理混乱:各个项目依赖版本不统一,升级、降级需要逐个修改,维护成本极高

  • 工程规范割裂:TS、ESLint、Prettier、构建配置各自维护,团队代码风格难以统一

  • 协作与部署低效:功能跨项目联动时,需要多仓库提交、合并、发版,无法实现原子化迭代

而 Monorepo 的核心优势,就是将所有关联应用、公共工具包统一收纳在单个代码仓库中管理。既能实现公共能力全局复用、工程规范统一,又能保留各个应用、工具包的独立性,兼顾开发效率与项目规范性。

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

目前前端 Monorepo 方案有很多,比如 Lerna、Nx、pnpm 原生 workspace 等。结合主流工程化实践,pnpm + Turborepo 是轻量化、高性能、低学习成本的最优组合,也是大厂广泛采用的方案。

1. pnpm 核心优势

  • 极致性能:相比 npm、yarn,安装速度更快,依托硬链接机制极大节省磁盘空间,多包场景优势尤为明显

  • 严格依赖隔离:自动规避幽灵依赖问题,强制规范依赖引入,减少线上因依赖缺失、版本错乱导致的报错

  • 原生支持 Workspace:无需额外安装插件,原生支持多包联动、本地依赖关联、批量脚本执行

2. Turborepo 核心优势

  • 增量构建:只构建改动的包及其依赖项,未改动模块直接读取缓存,大幅提升构建速度

  • 并行执行:自动并行执行多包 build、lint、test 脚本,最大化利用设备性能

  • 轻量化无侵入:不绑定框架、不侵入业务代码,适配 React、Vue、Next 等所有前端项目

  • 支持远程缓存:团队协作时可共享构建缓存,统一本地、CI 构建效率

三、从零初始化 Monorepo 项目

整套流程基于原生 pnpm + Turborepo,无多余冗余依赖,步骤极简。

1. 初始化项目根目录

# 创建项目根目录
mkdir my-monorepo && cd my-monorepo

# 初始化根目录 package.json
pnpm init -y

# 全局安装 Turborepo(开发依赖)
pnpm add -D turbo

2. 配置 pnpm Workspace

在根目录创建 pnpm-workspace.yaml,声明项目工作区规则,统一管理所有应用和公共包:

# pnpm-workspace.yaml
packages:
  # 业务应用目录:所有独立项目存放此处
  - "apps/*"
  # 公共工具包目录:通用组件、配置、工具函数存放此处
  - "packages/*"

配置生效后,pnpm 会自动识别 appspackages 下的所有子包,支持跨包依赖引用、批量安装依赖、批量执行脚本。

3. 基础目录结构搭建

搭建行业通用标准目录结构,区分业务应用公共基础包,结构清晰、方便后期迭代扩展:

my-monorepo/
├── apps/                # 所有独立业务应用
│   ├── web/             # 前台 Next.js 应用
│   └── docs/            # 项目文档站点
├── packages/            # 所有公共基础包(全局复用)
│   ├── ui/              # 通用 UI 组件库
│   ├── utils/           # 通用工具函数库
│   └── config/          # 全局公共配置(TS/ESLint/Prettier)
├── pnpm-workspace.yaml  # pnpm 工作区配置
├── turbo.json           # Turborepo 构建配置
└── package.json         # 根目录脚本与依赖

四、核心配置:Turborepo 构建规则

根目录创建 turbo.json,配置流水线任务、依赖执行顺序、构建产物缓存,这是实现增量构建、高效打包的核心。以下是生产级通用配置,可根据项目场景微调:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    // 构建任务:优先执行依赖包的 build,缓存 dist 产物
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"],
      "cache": true
    },
    // 测试任务:必须等待 build 完成后执行
    "test": {
      "dependsOn": ["build"],
      "cache": true
    },
    // 代码校验任务:无前置依赖,纯静态检测
    "lint": {
      "cache": true
    },
    // 开发启动任务:不缓存、实时热更新
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

配置核心说明:

  • dependsOn: ["^build"]:执行当前包构建前,先递归构建其所有依赖的公共包

  • outputs:指定构建产物目录,Turbo 会对这些目录做增量缓存

  • cache:开启任务缓存,未改动代码直接复用缓存,大幅提速

五、完善根目录脚本配置

修改根目录 package.json,统一封装全局脚本,支持一键批量执行所有子包的开发、构建、校验任务,适配团队协作规范:

{
  "name": "my-monorepo",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "test": "turbo test",
    "clean": "turbo clean && pnpm clean:node_modules",
    "clean:node_modules": "pnpm rm -rf node_modules && pnpm -r exec rm -rf node_modules"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  }
}

注意:根目录必须设置 private: true,避免仓库被误发布到 npm。

六、搭建公共基础包(核心复用能力)

Monorepo 的核心价值就是公共能力复用,我们依次搭建 configutilsui 三个通用基础包,所有业务应用可直接引用。

1. 全局配置包 packages/config

统一维护 TS、ESLint 配置,所有子项目通过 extends 复用,彻底解决配置割裂问题。

创建 packages/config/package.json

{
  "name": "@my-monorepo/config",
  "version": "1.0.0",
  "private": true,
  "main": "index.js"
}

创建通用 TS 配置 packages/config/tsconfig.base.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": ["node_modules", "dist"]
}

2. 工具函数包 packages/utils

存放全局通用工具方法,所有业务应用可直接导入使用,避免重复封装。

创建 packages/utils/package.json

{
  "name": "@my-monorepo/utils",
  "version": "1.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "build": "tsc",
    "lint": "eslint ."
  },
  "dependencies": {
    "@my-monorepo/config": "workspace:*"
  }
}

编写示例工具方法 packages/utils/src/index.ts

// 通用空值判断
export function isEmpty(val: unknown): boolean {
  if (val === null || val === undefined) return true
  if (typeof val === 'string' && val.trim() === '') return true
  if (Array.isArray(val) && val.length === 0) return true
  return false
}

// 通用防抖函数
export function debounce<T extends (...args: any[]) => any>(fn: T, delay = 300) {
  let timer: NodeJS.Timeout | null = null
  return (...args: Parameters<T>) => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() =&gt; fn(...args), delay)
  }
}

创建 TS 配置 packages/utils/tsconfig.json,复用全局配置:

{
  "extends": "@my-monorepo/config/tsconfig.base.json",
  "include": ["src/**/*"]
}

3. 通用组件包 packages/ui

用于存放全局复用 UI 组件,适配所有业务应用,统一项目视觉规范。配置方式与 utils 包一致,可按需封装按钮、弹窗、卡片等通用组件。

七、创建业务应用 & 跨包依赖引用

我们以 Next.js 应用为例,创建业务项目并引入公共包,实现跨包复用。

1. 创建业务应用

# 在 apps 目录创建 next 应用
cd apps
pnpm create next-app web --typescript

2. 业务应用引入公共包

apps/web/package.json 中引入本地公共包,workspace:* 代表同步引用本地最新版本,无需手动维护版本号:

{
  "dependencies": {
    "@my-monorepo/utils": "workspace:*",
    "@my-monorepo/ui": "workspace:*"
  },
  "devDependencies": {
    "@my-monorepo/config": "workspace:*"
  }
}

3. 业务中使用公共工具

在业务页面直接导入公共工具函数,跨包复用完全生效:

import { isEmpty, debounce } from "@my-monorepo/utils";

export default function Home() {
  const handleSearch = debounce((val: string) => {
    if (!isEmpty(val)) {
      console.log("搜索内容:", val);
    }
  }, 500);

  return (
    <input
      placeholder="请输入内容"
      onChange={(e) => handleSearch(e.target.value)}
    />
  );
}

八、增量构建与缓存实战验证

Turborepo 最核心的优势就是增量构建,我们可以简单测试验证效果:

  1. 首次执行 pnpm build:会依次构建 config、utils、ui、web 所有包,完整执行全量构建

  2. 无修改再次执行 pnpm build:所有任务直接读取缓存,秒级完成构建,无重复编译

  3. 仅修改 utils 包代码:只重新构建 utils 和依赖 utils 的 web 应用,其余包直接复用缓存

这种机制能极大节省大型项目的构建时间,CI/CD 部署效率提升尤为明显。

九、落地最佳实践与避坑总结

1. 目录规范

  • apps 只存放独立可运行的业务应用,不存放工具代码

  • packages 只存放无业务侵入的公共能力,保证通用性

  • 所有公共包统一命名 @my-monorepo/xxx,统一命名空间

2. 依赖管理规范

  • 公共依赖统一下沉到对应基础包,避免业务包重复安装

  • 本地跨包依赖统一使用 workspace:*,自动同步最新代码

  • 全局开发依赖统一安装在根目录,减少体积冗余

3. 常见踩坑点

  • 缓存失效:修改公共包后未重新构建,需执行 pnpm clean 清理缓存重试

  • 路径别名报错:所有子项目统一继承根目录 TS 配置,保证路径解析一致

  • 脚本执行失败:严格按照 Turbo 依赖顺序配置,避免前置任务未执行导致报错

十、总结

这套 pnpm + Turborepo 的 Monorepo 方案,足够轻量化、零冗余、易上手,完美适配绝大多数个人项目和中小型团队的工程化需求。

相比传统多仓库方案,它解决了代码复用混乱、工程规范割裂、构建效率低下的核心痛点;相比重型 Nx 等方案,它学习成本更低、无技术侵入、维护更简单。


Zarathustra
Zarathustra
前端工程师
返回文章