从零搭建 Monorepo:Turborepo + pnpm 最佳实践
一、为什么要用 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 会自动识别 apps、packages 下的所有子包,支持跨包依赖引用、批量安装依赖、批量执行脚本。
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 的核心价值就是公共能力复用,我们依次搭建 config、utils、ui 三个通用基础包,所有业务应用可直接引用。
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(() => 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 最核心的优势就是增量构建,我们可以简单测试验证效果:
首次执行
pnpm build:会依次构建 config、utils、ui、web 所有包,完整执行全量构建无修改再次执行
pnpm build:所有任务直接读取缓存,秒级完成构建,无重复编译仅修改 utils 包代码:只重新构建 utils 和依赖 utils 的 web 应用,其余包直接复用缓存
这种机制能极大节省大型项目的构建时间,CI/CD 部署效率提升尤为明显。
九、落地最佳实践与避坑总结
1. 目录规范
apps只存放独立可运行的业务应用,不存放工具代码packages只存放无业务侵入的公共能力,保证通用性所有公共包统一命名
@my-monorepo/xxx,统一命名空间
2. 依赖管理规范
公共依赖统一下沉到对应基础包,避免业务包重复安装
本地跨包依赖统一使用
workspace:*,自动同步最新代码全局开发依赖统一安装在根目录,减少体积冗余
3. 常见踩坑点
缓存失效:修改公共包后未重新构建,需执行
pnpm clean清理缓存重试路径别名报错:所有子项目统一继承根目录 TS 配置,保证路径解析一致
脚本执行失败:严格按照 Turbo 依赖顺序配置,避免前置任务未执行导致报错
十、总结
这套 pnpm + Turborepo 的 Monorepo 方案,足够轻量化、零冗余、易上手,完美适配绝大多数个人项目和中小型团队的工程化需求。
相比传统多仓库方案,它解决了代码复用混乱、工程规范割裂、构建效率低下的核心痛点;相比重型 Nx 等方案,它学习成本更低、无技术侵入、维护更简单。