04 · 架构设计
monorepo 结构、请求链路与契约唯一事实源——一张图看懂广民工具箱怎么组织
💡 TL;DR:一个仓库(monorepo)装三个应用、三个共享包;所有接口类型只写一份(契约唯一事实源),由 CI 强制前后端零漂移。本章讲清这套骨架的每个部件和它们怎么连起来。
一、monorepo 结构
guangmin/
├── apps/
│ ├── web/ # 主站(Next.js):首页、文章 /articles、文档 /docs、管理后台
│ ├── server/ # 后端 API(NestJS):账号/积分/支付/媒体上传,OpenAPI 契约事实源
│ ├── tools-web/ # 工具箱独立站(Next.js):13 款工具的工作页
│ └── miniapp/ # 微信小程序(Taro,与网页端共用账号积分)
├── packages/
│ ├── shared/ # Zod 契约 + 常量(唯一事实源,编译为 CJS 产物)
│ ├── ui/ # 设计令牌 tokens.css + 基础组件(纯 TSX 源码交付)
│ └── config/ # 仓库级 ESLint 等工程配置
├── deploy/ # 部署脚本:deploy-agent、nginx 配置、流水线定义
└── scripts/ # quality.sh 全量门禁、各类运维脚本为什么拆三个前端而不是合一个:主站(内容+账号)与工具站(工具工作页)的用户动线不同,分开部署互不影响——工具站发新版不会让主站闪断。小程序是微信生态的独立载体,天然分开。它们共享的东西(工具目录、常量、组件、账号体系)全部下沉到 packages/,这就是「monorepo 拆分」的标准答案:按部署单元拆应用,按复用单元拆包。
二、请求链路
要点只有两个:
- nginx 按域名分发:三个前端各自独立容器,nginx 是唯一入口;
/api/*一律转发给后端(剥掉前缀转发,后端路由不带/api)。 - 同源免 CORS:每个前端都通过自己域名的
/api/访问后端(nginx 代理),浏览器看到的是同源请求,跨域问题在架构层面就不存在。
三、契约唯一事实源(本仓库的招牌设计)
前后端最容易腐烂的东西是「接口类型」:后端改了字段,前端没人记得同步,运行时才炸。本仓库的解法是让类型从代码自动流向前端,全程机器执法:
四步各有各的执法:
- Zod Schema 写一份:接口出入参在 shared 定义,前端用它做表单校验,后端用它复核请求——校验规则天然同源;
- server 代码是接口形状的唯一事实:控制器写完,
pnpm gen:openapi导出 OpenAPI(接口描述标准)快照,提交入库; - 前端类型自动生成:
pnpm gen:api从快照生成 TypeScript 类型文件,前端调用接口的类型全部来自它,手改无效; - CI 卡漂移:流水线重新生成一遍快照,
git diff有差异即红灯——「改了后端忘了同步」这类事故在合并前就被物理拦下。
新增一个字段的标准动作(一分钟版):shared 加字段 → server 用起来 → pnpm gen:openapi && pnpm gen:api → 前端直接拿到新类型。四步缺一不可,但每步都有门禁兜底。
四、灰度开关设计
每个功能面都有一个编译期常量开关(如 FEATURE_TOOLS_ENABLED、付费工具的运行时开关),集中在 shared 导出。导航、站点地图(sitemap)、接口守卫读同一个开关——「对外藏一个功能」是改一行常量重新发布的事,而不是到处删代码。这套设计的代价与收益在 05 核心实现 展开。
延伸阅读:03 技术选型 讲每层为什么这么选;06 质量门禁与部署 讲这套架构怎么安全地上生产。