从零落地全栈 Monorepo:Turbo + npm workspaces 实战(附一个记账小应用)
一个仓库装下前端、后端、共享包,一条命令启动全栈,类型在前后端之间无缝流转------这篇文章用一个真实的记账应用,把 Monorepo 的搭建、配置和踩坑一次讲透。
一、为什么需要 Monorepo?
想象一个典型的全栈小项目:前端一个仓库、后端一个仓库,接口类型定义靠"口口相传"。后端改了字段,前端毫不知情,直到线上报错才发现------相信很多人都踩过这种坑。
Multirepo(多仓库)的痛点:
- 接口类型前后端各写一份,改起来两头跑,容易不同步
- 公共工具函数(日期格式化、金额处理)复制粘贴,改 A 忘 B
- 联调时要开两个仓库、两个终端,依赖版本各管各的
Monorepo(单仓库多包)的思路 :把前端、后端、共享代码放进同一个仓库的不同包里,包与包之间直接引用。一次 git clone 拿到全部代码,一次安装装好所有依赖,类型定义在前后端之间直接 import。
本文的主角就是一个采用 Monorepo 架构的个人记账应用 Money Tracker:
- 📱 移动端 H5 记账页面(记一笔、账单列表、统计图表、预算管理)
- ⚙️ RESTful 后端服务(JWT 认证、账单 CRUD、分类统计、数据导出)
- 📦 前后端共享的类型、常量、工具函数包
技术栈一览:
| 层 | 技术 |
|---|---|
| Monorepo 编排 | Turbo 2.x + npm workspaces |
| 前端 | React 18 + UmiJS 4 + Ant Design 5 + ECharts |
| 后端 | Node.js 20+ + Fastify 5 + Prisma 6 + MySQL 8 |
| 校验/认证 | Zod + @fastify/jwt + bcryptjs |
| 部署 | Docker + docker-compose |
二、工具选型:为什么是 Turbo + npm workspaces?
Monorepo 工具链常见的几个选择:
- Nx:功能强大但偏重,插件体系复杂,小项目有点"杀鸡用牛刀"
- Lerna:老牌选手,但核心能力(依赖提升、版本发布)已被包管理器原生 workspaces 覆盖
- Turborepo :Vercel 出品,只做一件事------任务编排与缓存,学习成本几乎为零
包管理器选择 npm workspaces 而不是 pnpm:npm 是 Node.js 自带的,团队任何人 clone 下来 npm install 就能跑,不需要额外全局安装工具。对于中小型项目,npm workspaces 的能力完全够用。
Turbo 负责"怎么跑任务",npm workspaces 负责"包怎么链接",两者职责清晰、互不耦合。
三、项目结构
csharp
money-tracker/
├── apps/
│ ├── server/ # 后端服务(Fastify + Prisma)
│ │ ├── prisma/
│ │ │ ├── schema.prisma
│ │ │ └── seed.ts
│ │ └── src/
│ │ ├── modules/ # user / record / category / budget / stats
│ │ ├── plugins/ # auth / cors / database / validator
│ │ └── index.ts
│ ├── web/ # 前端应用(UmiJS + AntD)
│ │ └── src/
│ │ ├── pages/ # 首页 / 记账 / 账单 / 统计 / 预算 / 我的
│ │ ├── services/ # API 请求封装
│ │ └── layouts/ # 全局布局(导航栏 + TabBar)
├── packages/
│ └── shared/ # 共享包:类型 + 常量 + 工具函数
│ └── src/
│ ├── types/ # 账单、分类、预算、用户类型
│ ├── constants/ # 默认分类等常量
│ └── utils/ # 日期、金额格式化
├── turbo.json # Turbo 任务编排
├── package.json # 根 package.json(workspaces 声明)
├── tsconfig.base.json # 共享 TS 基础配置
└── docker-compose.yml # 一键部署
三个包各司其职:
@money-tracker/shared:纯 TypeScript 库,被前后端共同依赖server:Fastify 服务,端口 3000web:UmiJS H5 应用,端口 8000,通过 proxy 把/api转发到后端
四、核心配置详解
4.1 根 package.json:声明 workspaces
json
{
"name": "money-tracker",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"dev": "turbo run dev",
"dev:web": "turbo run dev --filter=web",
"dev:server": "turbo run dev --filter=server",
"build": "turbo run build",
"db:migrate": "turbo run db:migrate --filter=server",
"db:seed": "turbo run db:seed --filter=server"
},
"devDependencies": {
"turbo": "^2.5.0",
"typescript": "^5.7.0"
},
"packageManager": "npm@11.9.0",
"engines": { "node": ">=20.0.0" }
}
三个关键点:
workspaces:npm 会把apps/*和packages/*识别为子包,根目录一次npm install装好所有依赖,并自动把本地包软链进node_modulespackageManager:这个字段不能省 !Turbo 靠它识别包管理器来解析 workspace,缺了会直接报错Could not resolve workspace- 脚本全部委托给 turbo:由 turbo 决定任务怎么跑、按什么顺序跑
4.2 turbo.json:任务编排的核心
json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".umi/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"db:migrate": { "cache": false },
"db:seed": { "cache": false }
}
}
逐条解读:
dependsOn: ["^build"]:^表示"依赖包"。执行web的 build 前,turbo 会先构建它依赖的@money-tracker/shared------按依赖拓扑排序自动执行,这就是 Monorepo 构建的精髓outputs:告诉 turbo 构建产物在哪,下次输入没变就直接命中缓存秒过persistent: true+cache: false:dev是长跑进程,不能缓存、不能等它"结束"--filter:精准只跑某个包,比如turbo run dev --filter=server只起后端
4.3 共享包:前后端类型同步的秘诀
packages/shared 的 package.json:
json
{
"name": "@money-tracker/shared",
"main": "dist/index.js",
"types": "src/index.ts",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
}
前后端的 package.json 里声明依赖即可:
json
{
"dependencies": {
"@money-tracker/shared": "*"
}
}
npm workspaces 会把 * 解析为本地包并自动软链。于是后端 service 里可以写:
ts
import { type IUser, DEFAULT_EXPENSE_CATEGORIES } from '@money-tracker/shared'
前端页面里同样可以写:
ts
import { RecordType, formatAmountWithComma, getCurrentMonth } from '@money-tracker/shared'
改一处类型,前后端同时感知、编译期报错------这就是 Monorepo 最实在的价值。比如默认记账分类(餐饮、交通、育儿......)定义在共享包里,seed 脚本、注册时自动建分类、前端展示三处共用,调整分类只改一个文件。
五、开发体验
bash
npm install # 根目录一次安装所有依赖
npm run db:seed # 初始化数据库和测试账号
npm run dev # turbo 并行拉起前端(8000) + 后端(3000)
一条 npm run dev,turbo 并行启动所有包的 dev 任务;改 shared 包代码,tsc --watch 自动重编译,前后端热更新无缝衔接。
六、什么时候该用 Monorepo?
适合的场景:
- ✅ 全栈项目,前后端需要共享类型/常量/工具
- ✅ 多个关联应用(如主站 + 管理后台 + 小程序)共用组件库
- ✅ 中小团队,希望降低跨仓库协作成本
不太适合的场景:
- ❌ 包之间完全独立、没有共享代码
- ❌ 超大型仓库(需要引入远程缓存、增量 CI 等重型基建,那是另一个话题了)
七、总结
Monorepo 听起来唬人,落地其实就三件事:
- npm workspaces 声明包结构,一次安装、自动软链
- turbo.json 编排任务,
^build拓扑构建 + 缓存加速 - 共享包承载类型与常量,前后端编译期同步
配合 Docker Compose(MySQL + server + web 三个服务一键拉起),从开发到部署的完整链路就闭环了。这个记账应用的全部代码就是一个可以直接参考的最小实战模板,希望能帮你把 Monorepo 真正用起来。
记账小应用源码地址:github.com/cdd-1101/AI... 👋