从零落地全栈 Monorepo:Turbo + npm workspaces 实战(附一个记账小应用)

从零落地全栈 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 服务,端口 3000
  • web: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" }
}

三个关键点:

  1. workspaces :npm 会把 apps/*packages/* 识别为子包,根目录一次 npm install 装好所有依赖,并自动把本地包软链进 node_modules
  2. packageManager这个字段不能省 !Turbo 靠它识别包管理器来解析 workspace,缺了会直接报错 Could not resolve workspace
  3. 脚本全部委托给 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: falsedev 是长跑进程,不能缓存、不能等它"结束"
  • --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 听起来唬人,落地其实就三件事:

  1. npm workspaces 声明包结构,一次安装、自动软链
  2. turbo.json 编排任务,^build 拓扑构建 + 缓存加速
  3. 共享包承载类型与常量,前后端编译期同步

配合 Docker Compose(MySQL + server + web 三个服务一键拉起),从开发到部署的完整链路就闭环了。这个记账应用的全部代码就是一个可以直接参考的最小实战模板,希望能帮你把 Monorepo 真正用起来。

记账小应用源码地址:github.com/cdd-1101/AI... 👋

相关推荐
灯澜忆梦19 分钟前
【基于GO的Web开发15】gin路由和路由组
前端·后端·golang·gin
Southern Wind25 分钟前
从一段文字到一部动态漫:如何打造全流程分镜创作工作台 StoryCanvas AI
前端·javascript·vue.js·人工智能
烂蜻蜓31 分钟前
Flask入门教程(七):请求与响应——Web应用的核心数据流
前端·python·flask
爱勇宝38 分钟前
从前端到全栈,我最后学会的是:先做产品
前端·后端
console.log('npc')41 分钟前
Grill-Me 技能使用教程
前端·大模型·产品·skill·需求
真空回流焊炉1 小时前
数字功率芯片真空共晶设备实操教程与要点解析
前端·人工智能
mayaairi1 小时前
JS DOM属性操作完全指南:内容、样式与自定义属性
开发语言·前端·javascript
avi91111 小时前
Threejs新版本后glb导出提示.x问题GLTFExporter,替代3dmax
开发语言·前端·javascript·threejs·glb·gltfexporter