Turborepo是Vercel开源的高性能Monorepo构建系统,它最大的特点是能感知任务之间的依赖关系,把可以并行执行的脚本同时跑起来,并且通过本地与远程缓存跳过重复的构建步骤。对于一个维护着多个React项目的团队来说,把所有应用和共享库收进一个仓库统一管理,意味着公共组件只写一遍、TypeScript类型全局共享、CI流水线只跑真正变化的包。下面介绍从零搭建Turborepo并迁移现有React应用的具体步骤。

一、为什么React项目适合迁移到Monorepo
当团队只有一两个React应用时,多仓库管理问题不大。但随着业务增长,往往会出现这样的情况:三个应用各自维护一份按钮组件、一份请求封装、一份主题配置,每次改一个公共逻辑要同步改三个仓库、发三次包、升三次版本。这种重复劳动不仅浪费时间,还容易产生行为不一致的bug,比如A应用的日期组件修复了时区问题,B应用的却忘了同步。
Monorepo的核心思路是把这些重复的东西抽成独立的包,放在同一个仓库里,通过workspace协议互相引用。假设你的目录结构中有一个ui包存放公共组件,有一个utils包存放工具函数,那么所有应用都能直接引用最新代码,不需要走发包流程。Turborepo在此基础上解决了Monorepo最大的痛点:构建速度。传统Monorepo用lerna或者yarn workspace串行执行脚本,十个应用跑一遍lint加build可能要十几分钟,而Turborepo会分析包之间的依赖图,把没有依赖关系的任务并行跑,并且对输入没变化的任务直接返回缓存结果,实际耗时往往能缩短到原来的几分之一。
另外一点容易被忽视的好处是代码审查的完整性。一次需求如果同时涉及应用代码和公共组件修改,在多仓库模式下需要开多个PR分别评审,评审人很难看到整体改动。Monorepo下一个PR就能展示所有关联改动,重构公共组件时也能通过全局搜索确认影响范围,这在维护老项目时价值非常大。
二、搭建Monorepo基础结构
Turborepo本身不管理依赖,它只负责调度任务,依赖管理交给pnpm的workspace能力。先在根目录初始化工程:
# 全局安装工具 npm install -g pnpm turbo # 创建项目根目录并初始化 mkdir react-monorepo && cd react-monorepo pnpm init # 安装核心依赖到根目录 pnpm add -Dw turbo typescript
接着创建pnpm-workspace.yaml文件,告诉pnpm哪些目录是工作区包:
packages: - "apps/*" - "packages/*"
然后规划目录结构。一个典型的布局是:根目录放配置文件,apps目录放可独立部署的应用(比如主站、管理后台、文档站),packages目录放共享代码(UI组件库、工具函数、ESLint配置、TypeScript配置)。每个子包都要有自己的package.json,其中name字段是全局唯一的,其他包引用它就靠这个名字。建议在根package.json里写上packageManager字段和workspaces相关脚本,方便团队成员和CI环境统一使用正确的包管理器版本。
迁移现有React应用时,直接把原项目整个目录复制到apps/web下,删掉它自带的node_modules和锁文件,然后从根目录统一安装依赖。这一步最容易出问题的是React版本冲突:如果不同应用使用不同版本的react,pnpm会分别为它们安装各自版本,公共组件库就必须声明对react的peerDependencies而不是dependencies,否则容易出现两个React实例导致的hooks报错。
三、配置turbo.json任务管道
在根目录创建turbo.json,这是Turborepo的核心配置文件,它定义了任务之间的依赖关系和缓存策略:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": []
}
}
}
这里的^build语法表示先构建当前包依赖的所有上游包,比如apps/web依赖packages/ui,那么执行turbo run build时会先构建ui再构建web。理解这个符号非常关键,很多新手把^build写成build,前者是构建依赖包,后者是同包的其他任务依赖,写错了构建顺序就会混乱。outputs字段告诉Turborepo缓存哪些产物,不配置的话缓存里没有构建结果,命中缓存等于白命中。
dev任务要特别注意:开发服务器是常驻进程,必须设置cache为false并标记persistent,否则Turborepo会等待它退出导致命令卡住。配置完成后,在根package.json里加几个快捷脚本,比如"dev": "turbo run dev"、"build": "turbo run build",日常操作就都在根目录完成了。
四、抽离公共包与本地引用
迁移的关键动作是把重复代码抽出来。以UI组件库为例,在packages/ui下创建组件并配置导出。package.json里需要声明入口字段,同时把react放到peer依赖中:
{
"name": "@myorg/ui",
"version": "0.1.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup index.ts --format cjs,esm --dts"
},
"peerDependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0"
}
}
应用侧引用本地包时,使用workspace协议:pnpm add @myorg/ui --workspace。这样apps/web的package.json里会出现"@myorg/ui": "workspace:*",pnpm会通过软链接直接指向本地目录,改组件代码立即生效,不需要发包。在tsup或tsc构建出dist之前,也可以临时把入口指到源码文件来加快开发时的调试,一些团队会配置条件导出,开发环境走源码、生产环境走构建产物。
类型共享建议单独做一个packages/tsconfig包,把base.json、react.json等预设配置放进去,各子包通过extends字段继承,保证编译选项全仓库一致。ESLint和Prettier同理,抽成packages/eslint-config后,各应用只要写一行"extends": "@myorg/eslint-config"即可,再也不用担心十个仓库十种风格。
五、构建缓存与CI加速
Turborepo的缓存基于输入哈希:任务的所有输入文件、环境变量、依赖包构建产物拼接成一个哈希值,哈希相同则直接恢复缓存的日志和产物。第一次跑turbo run build会比较慢,第二次开始命中缓存的任务几乎是秒级完成。可以做个简单实验:在十个包的Monorepo里跑两遍build,第二遍的输出会大量出现>> Cached: Repeating previous build标记,整体耗时断崖式下降。
想要在CI上也享受缓存,需要开启远程缓存。执行npx turbo login和npx turbo link把仓库关联到Vercel账号后,本地构建产物会上传到云端,CI机器拉取后直接命中。如果不方便使用Vercel服务,也可以自建缓存服务器,通过环境变量TURBO_API、TURBO_TOKEN指向自己的接口。在GitHub Actions中,记得缓存.turbo目录和pnpm store,并利用turbo run build --filter=...[origin/main]只构建受影响的包,进一步压缩流水线时间。
迁移完成后的收益是渐进式体现的:依赖统一了,公共代码收敛了,本地一条命令跑起全部开发服务,CI时长随缓存命中率稳步下降。建议迁移时先小步走,把一个应用和一个组件库搬进来跑通全流程,再逐步把其余项目迁入,每一步都验证构建和部署链路,避免一次性大搬家带来的风险。