t3code:从重复搭架子到开箱即用,全栈项目脚手架的实战解析

发布时间:2026/10/28 15:29:08
t3code:从重复搭架子到开箱即用,全栈项目脚手架的实战解析 t3code 是我自己维护了大半年的一套全栈项目脚手架名字里的三个 t 我借用了 TypeScript、Tailwind、tRPC 的常见组合叫法但实际内容比“某个技术栈模板”要宽不少。简单说它解决的是我一直很烦的问题每开一个新项目路由要重新配、请求层要重新写、lint 规则要重新聊、提交规范要重新说服队友一两天就这么耗在“搭架子”上。现在这些事基本都固化进 t3code一句命令就能拉出带完整规范的项目骨架。这篇文章直接把思路和盘托出为什么当初要造这个轮子模板内部是怎么设计的实际用起来到底顺不顺手以及这半年里我踩过的几个典型坑。如果你也是那种手里同时维护好几个项目、还得反复初始化仓库的前端或者全栈开发者又或者你小团队里总有人为“代码风格不统一”吵架那这篇应该能给你不少可落地的参考。1. 为什么会有 t3code从“重复搭架子”到“开箱即用”1.1 我最初的问题每个项目都要重新走一遍流程两年前我接手一个小团队的技术规划当时组里三个人手里同时滚着四五个项目。表面上看大家都在正常推进业务但有个问题一直让我很不舒服每个项目的初始化方式都“稍微有点不一样”。有人直接用 Vite 默认模板有人从旧项目里 copy 了一份带路由的目录还有人喜欢自己搭一套带services、stores、views的目录结构。依赖管理也是有人用 npm有人用 pnpmlockfile 一换就是一次大改动。更别提 lint 规则了——A 项目里能过的代码到了 B 项目直接红一片。这种差异一开始看着只是“不统一”但半年后问题就来了。团队里任何一个人一旦需要去改另一个人的项目光看目录结构就要花半天。代码评审的时候讨论的重点经常不在业务逻辑上而是“你这里为什么用双引号”“这个类型为什么没有显式声明”。后来我实在受不了决定把“搭架子”这件事做成一个标准流程。1.2 t3code 的三个设计原则这个脚手架做到现在核心设计原则就三条所有后续功能都是围绕这三条展开的。第一条是“约定优于配置但约定必须可覆盖”。t3code 默认给出整套规范目录怎么分、请求层怎么封装、路由怎么注册、提交信息什么格式。你直接用就能跑但如果你想改也不用去源码里翻配置所有默认值都集中在一个配置文件里改一处全局生效。第二条是“模板要有业务感不能只是个 Hello World”。一般脚手架生成的模板顶多帮你搭好一个能跑起来的页面。但实际中后台项目的大部分工作量来自登录鉴权、路由守卫、请求错误处理、权限指令、分页表格这些“业务基建”。t3code 生成的代码会直接带上这些内容让你初始化完之后不至于面对一片空白。第三条是“模板本身要被持续维护”。我一开始也犯过“模板写一次就放着不管”的毛病后来发现技术栈一升级、业务模式一变旧模板反而成了负担。所以 t3code 里引入了“片段”概念模板被拆成一个个可插拔的模块核心部分升级时不会影响你已经生成的项目已经创建的项目也能通过新片段增量同步最新实践。后面我会专门讲这一块。选 CLI 而不是让用户直接git clone一个模板仓库也是基于这三点考虑。直接 copy 仓库最大的问题是仓库里的包名、项目名、作者信息全是写死的拿回来还得全局搜索替换一不留神就漏改。而且 git 历史、旧依赖、旧的提交钩子配置全都被带过来了反而成了脏数据。CLI 加模板渲染的方式则可以做到参数化生成回答几个问题渲染器把变量写进对应文件产出的项目干净且完整。2. 核心设计与技术选型t3code 到底长什么样2.1 目录结构设计与划分逻辑整个 t3code 的仓库结构是典型的 pnpm monorepo。核心目录大概长这样t3code/ ├── apps/ │ ├── web/ # 前端应用React Vite │ └── server/ # 后端 APIExpress Prisma ├── packages/ │ ├── shared/ # 前后端共享的类型与工具函数 │ └── config/ # 统一的 eslint / tsconfig / prettier 配置 ├── scripts/ │ ├── cli/ # 初始化命令行的实现 │ ├── generators/ # 页面、API、模型的代码生成器 │ └── loaders/ # 模板片段的加载与注册逻辑 ├── templates/ │ ├── base/ # 最基础的项目骨架 │ └── fragments/ # 可插拔的片段login、user-list、audit-log 等 └── package.json这个结构乍看比单个应用项目复杂但对“脚手架”这个角色来说很合适。apps里是真正的生成目标packages里放的是跨项目复用的配置和代码。比如t3code/config这个包集中管理 TypeScript 的strict选项、ESLint 的规则集以及 Prettier 的格式配置。所有从 t3code 生成出来的项目统一从packages/config继承规范而不是在各自项目里各写一份.eslintrc。为什么坚持用 monorepo因为在真实项目里一个中后台系统往往同时包含前端、API 服务、定时任务、文档站点。如果每个代码仓库都独立维护 tsconfig 和 eslint很容易出现两个服务的类型定义悄然漂移。我之前见过一个项目前端User接口里age是number后端User模型里age是string编译各自都通过联调的时候才发现问题。把共享类型抽到packages/shared之后前后端引的是同一份定义这类问题直接被消灭在编译期。2.2 技术选型背后的取舍t3code 目前默认的技术栈是 TypeScript React Vite Tailwind CSS Express Prisma Vitest配套 ESLint、Prettier、Husky、lint-staged、commitlint。表格里列一下我当初选型时的理由技术选择理由备选方案与落选原因TypeScript前后端能共享类型联调时少一堆低级问题JavaScript 初始效率高但团队一扩大就失控React Vite生态成熟Vite 冷启动和热更新足够快Next.js 很香但文件路由和 SSR 心智较重我做中后台 API 分离场景用不上Tailwind CSS原子类让样式随组件走不用再为“取类名”纠结CSS Modules 也不错但跨项目复用样式时需要额外约定Express生态最大、中间件丰富招人门槛低Fastify 性能更好但团队对这个栈不够熟悉Prismaschema 即文档迁移和类型生成一体TypeORM 配置繁琐Drizzle 当时还不够成熟Vitest跟 Vite 同源配置几乎为零Jest 生态老但 transformer 配置太多跑起来也慢这个选型可能不是每个场景的最优解但它是“大概率不会错”的组合。比如我不选 Next.js是因为 t3code 的目标场景是传统前后端分离的中后台前端要的是快速迭代和清晰的页面路由后端要的是能直接接数据库的 CRUD 接口。如果换成重 SSR 的框架部署复杂度会明显上升对一个小团队来说性价比不高。当然t3code 并没有把路堵死技术选型都集中在模板层你完全可以基于模板换成别的框架。2.3 模板与代码生成器的关系t3code 本质上不是“一份静态模板”而是“模板 生成器”的组合。静态模板能解决初始化问题但解决不了重复造页面、重复写 CRUD 的问题。我在模板里内置了几条常用命令比如生成一个新的列表页面、生成一个带 Prisma model 的接口、生成一个表单组件。打个比方静态模板相当于毛坯房加一张户型图生成器则是按图施工的施工队。你告诉它“我需要一个用户列表页”它会自动把路由配置、导航菜单、API 调用函数、页面组件、类型定义全部生成出来并且把该注册的地方都注册好。这一步大大减少了“业务基建”层面的重复劳动。不过有一点我必须强调生成器不是魔术它的价值建立在团队约定一致的基础上。如果你们团队本来就没有统一的目录规则那任何生成器产出的代码都可能跟你的现状冲突。所以 t3code 默认先把目录结构和注册机制定死遇到不合规的情况宁可报错也不要生成一个“四不像”的文件。3. 实际使用教程用 t3code 快速创建一个可用项目3.1 安装与初始化假设你现在要用 t3code 创建一个项目整个过程大概是这样的。先执行pnpm create t3code my-app如果你用的是 npm可以执行npm create t3codelatest my-app。初始化之后CLI 会问你几个问题主要是三种项目名、包管理器偏好、需要哪些功能片段。功能片段目前有登录鉴权、用户列表、操作日志、文件上传这几个。这些片段不是必选但绝大多数人至少会选登录所以我默认把auth放在第一个。回答完之后CLI 会开始渲染模板。这里有两个细节我觉得比生成结果本身更重要一并说一下。一是它会做“文件冲突检测”如果你执行命令的项目目录里已经有文件CLI 不会直接覆盖而是逐文件询问是跳过、替换还是保留两者。我见过很多脚手架直接把目录清空这个行为我始终认为太危险了。二是它会做“全量依赖安装”。生成完之后自动执行pnpm install并且跑一次pnpm lint和pnpm typecheck确保你拿到手的就是一个能通过全部检查的项目。如果初始化过程中有任何一步失败它会明确提示是在哪个环节断的不会留下一堆未知状态。3.2 生成后的项目结构与关键配置初始化完成后你会得到一个这样的目录结构my-app/ ├── apps/ │ ├── web/ │ │ ├── src/ │ │ │ ├── pages/ │ │ │ ├── components/ │ │ │ ├── layouts/ │ │ │ ├── router/ │ │ │ ├── services/ │ │ │ └── main.tsx │ └── server/ │ ├── src/ │ │ ├── routes/ │ │ ├── middlewares/ │ │ ├── prisma/ │ │ └── index.ts ├── packages/ │ ├── shared/ │ │ └── src/ │ │ ├── types/ │ │ └── utils/ │ └── config/ ├── .env.example ├── .eslintrc.cjs ├── .prettierrc ├── .gitignore ├── commitlint.config.cjs └── package.json几个关键配置文件的价值我拆开说。packages/config里的tsconfig.base.json保存了严格模式下的所有编译选项。核心是strict: true、noUncheckedIndexedAccess: true、exactOptionalPropertyTypes: true、moduleResolution: bundler。这几个选项看着不起眼实际能帮你拦下一大批运行时错误。特别是noUncheckedIndexedAccess它会让你在访问数组元素时意识到结果可能有undefined逼着你处理边界。.env.example里预置了数据库连接串、JWT 密钥、前端 API 地址这些常见的环境变量占位。在模板里环境变量不是到处process.env.SOMETHING随手取而是要求先在一个env.ts文件里定义 schema统一导出。这个习惯花不了多少时间但能让“缺个环境变量导致线上崩”的概率大幅下降。husky 和 lint-staged 是开箱即用的。提交之前自动跑一次 prettier 和 eslint只针对暂存区文件。commitlint 则限定了提交信息的格式。这些机制刚上时会有队友觉得烦但用一个月之后基本没人愿意再回到“commit message 随便写”的状态。3.3 一个完整的落地示例新增页面和接口我用一个“用户列表页面”来演示 t3code 的日常开发流程。第一步在apps/server里定义数据模型。假设用 Prisma直接在schema.prisma增加一个模型model User { id String id default(cuid()) email String unique name String? role String default(member) createdAt DateTime default(now()) }第二步运行pnpm prisma migrate dev生成迁移文件。之后用 t3code 的生成器创建 APIpnpm gen api user这个命令会读取 Prisma 模型自动生成对应的路由文件、controller 和对应的方法并且在routes/index.ts里完成注册。路由层拿到的请求会直接映射到 Prisma 的 CRUD 方法所以很短import { Router } from express; import { prisma } from ../prisma; import type { User } from t3code/shared; export const userRouter Router(); userRouter.get(/users, async (_req, res) { const users await prisma.user.findMany(); res.json(users satisfies User[]); });第三步在前端项目里生成页面pnpm gen page user-list生成器会自动创建apps/web/src/pages/user-list/index.tsx把路由配置加进router/routes.tsx把导航菜单项加进layouts/MainLayout.tsx的菜单配置里再生成一个调用接口的service文件。页面组件用 React Query 管理请求状态大致长这样import { useQuery } from tanstack/react-query; import { fetchUsers } from ../../../services/user; import type { User } from t3code/shared; export function UserListPage() { const { data, isLoading } useQuery({ queryKey: [users], queryFn: fetchUsers }); if (isLoading) { return div加载中.../div; } return ( div classNamep-6 h1 classNametext-lg font-semibold用户列表/h1 div classNamemt-4 grid gap-2 {data?.map((user: User) ( div key{user.id} classNamerounded border p-3 span{user.name}/span span classNametext-sm text-gray-500{user.email}/span /div ))} /div /div ); }整个过程大概五分钟。以前这五分钟要花在复制旧代码、改路径、查路由表、补类型上现在只需要给出“我要用户列表”这个意图剩下都是生成器的事。我刻意没有在示例里写更复杂的表格组件因为生成器只是起点真正的业务逻辑终究要自己写它的价值在于把你“必经的路”自动铺好。4. 深度自定义把 t3code 改造成你自己的脚手架4.1 配置文件与动态变量体系t3code 的模板渲染建立在一套“变量注入 条件文件”的机制上。所有变量都来自templates/base/template.config.ts这个文件。看一下简化版本export default { prompts: [ { name: projectName, message: 项目名称, default: my-app }, { name: needAuth, message: 是否需要登录鉴权, type: confirm, default: true }, ], variables: { projectName: (answers) kebabCase(answers.projectName), packageName: (answers) answers.projectName.replace(/-/g, ), }, conditionalFiles: [ { condition: (answers) answers.needAuth, source: fragments/auth, }, ], };模板文件里用% projectName %这类占位符来注入变量。举一个实际例子package.json模板会写{ name: % packageName %, scripts: { dev: pnpm run -r dev, lint: pnpm run -r lint } }这套体系成本很低但收益很实在。你可以非常方便地加入自己的变量比如公司前缀、包名缩写、内部 registry 等然后统一作用于全部模板文件。CLI 交互里收集到的答案会直接进入同一个 answers 对象后续所有渲染都基于这份 answer 展开。4.2 新增一个业务模板片段的完整步骤t3code 的“片段”机制是最值得自定义的部分。默认模板只覆盖通用场景但不同团队总有自己的一套固定玩法比如移动端项目要套壳 H5后台项目要有审批流。这时候你可以自己写片段。写一个片段分四步。第一步在templates/fragments/下创建一个目录比如叫audit-log里面放你要生成的文件用占位符标注变量。第二步在片段配置文件里声明这个片段能干什么。一个最简单的片段配置长这样export default { name: audit-log, description: 操作日志模块, prompts: [ { name: logTableName, message: 日志表名, default: audit_log }, ], files: [ { source: server/route.ts.tpl, target: apps/server/src/routes/audit-log.ts }, { source: web/page.tsx.tpl, target: apps/web/src/pages/audit-log/index.tsx }, ], };第三步在scripts/loaders/fragment.ts里注册这个片段。注册动作只做一件事把新的片段配置合并进已有的片段列表。这里有个细节要注意文件名要尽量避免和已有片段冲突否则加载的时候会直接报错。冲突比覆盖更安全因为覆盖会静默地丢掉旧代码。第四步运行pnpm gen fragment audit-log验证一遍。如果你没安装依赖可以先pnpm install正常的话生成器会提示新增了几个文件、注册了几条路由。这套机制的思路来自我一直在用的一个原则任何你做过三次以上的重复工作都应该变成可复用的模板代码。哪怕是团队内部特有的“分页表格带筛选条件”这类组件也值得沉淀成片段然后在下一个项目里直接复用。4.3 升级模板时如何兼顾已有的项目这是所有脚手架类项目最尴尬的问题模板升级了那已经用老模板生成的项目怎么办强行重新生成整个模板肯定不现实因为业务代码早就写进去了。t3code 的解决方案有三个层级按推荐程度从高到低排。第一层级把核心能力放进packages/并通过 workspace 依赖升级。比如请求封装、鉴权逻辑、Env 解析这一类基础代码它们以包的形式被项目引用。包升级时只需在项目里把版本号抬一下然后看 changelog 决定要不要改调用方。这种方式的侵入性最低也是我最推荐的同步方式。第二层级用 codemod 脚本做定向替换。比如模板早期把所有路由都放在一个routes.ts里后来你想改成按目录自动加载就不适合手动改每个项目。我写过几个 codemod 脚本遍历项目文件做 AST 级转换跑完自动提交。AST 转换比字符串替换可靠得多建议所有人都学一点 jscodeshift 的基础用法。第三层级直接放弃同步。如果某个项目已经进入稳定维护期业务逻辑基本不再变动那就没必要强行追新模板。模板升级应该服务于“还在活跃开发的项目”而不是让每个老项目都跟着折腾。5. 常见问题与踩坑记录这些坑我替你先踩过了5.1 问题速查表症状常见原因解决办法pnpm install报 workspace 协议错误pnpm 版本过旧不认识workspace:*用 corepack 启用固定版本比如corepack enable pnpm --versionTypeScript 报找不到t3code/shared模块共享包没有先构建给 shared 包加build: tsup并在根目录执行pnpm buildESLint 报一堆 “file ignored because no valid ignore”生成目录缺少.eslintignore检查模板渲染后.eslintignore是否存在不存在就补一份提交代码时 husky 钩子没触发husky v9 不再自动安装钩子检查prepare脚本里是否包含huskyPrisma client 类型跟 schema 不同步schema 改了没重新生成 client执行pnpm prisma generate必要时加postinstall脚本页面生成之后菜单没有出现新入口布局组件里的菜单是静态配置检查布局是否注册了动态菜单读取逻辑或生成器是否写了配置文件模板渲染生成的文件里出现奇怪的%文本模板文件被误当作 ejs 渲染在需要保留的字面量处使用转义或用%%包裹5.2 三个最棘手的坑第一个坑是模板变量与业务代码语法冲突。我最初用 ejs 作为模板引擎占位符是% %但 React 和 Vue 模板里大量使用{{ }}。这个问题一开始没暴露直到我在一个模板文件里写了const style { color: {{ color }} }才发现渲染结果完全不对。排查了很久最终结论是ejs 默认把%开头的内容当作代码Vue 的插值写法“看起来”没问题但某些组件库的文档模板里真会出现%字符。解决方法是统一转义所有模板文件里需要保留的%一律写成%%渲染后再还原。这在生成器里是一条硬编码规则。第二个坑是路径分隔符的系统差异。生成器在扫描片段文件时我第一次写的是本地遍历用的path.join在 Windows 上会得出apps\\web\\src这样的路径。然后渲染器把这些路径写进配置前端打包在 macOS 上就崩了。后来所有文件路径统一改用posix.join并且扫描目录交给fast-glob处理它会自动 normalize 路径分隔符。这件事让我记住一个规则凡是会写进代码生成结果的路径一律用 POSIX 风格别跟操作系统较劲。第三个坑是 pnpm 的幽灵依赖。t3code 用了 monorepo 结构pnpm 默认的符号链接布局对依赖隔离做得很好但如果你在项目里没有声明某个依赖却在代码里直接 importTypeScript 有时依然能解析到根目录node_modules里的同名包因为编辑器用的是node_modules/.pnpm或根目录桌面解析。这个问题的诡异之处在于构建能过、类型能过但换到 CI 环境可能就挂了。最终的方案是两手抓一是给 ESLint 配一条规则禁止 import 未在package.json里声明的包二是 CI 流水线里先跑一次pnpm install --frozen-lockfile再跑pnpm typecheck让“幽灵依赖”在第一步就直接报错。6. 后续扩展与个人心得6.1 我计划做的几个方向接下来我对 t3code 的规划大致有三条线。第一是把 CLI 部分从 monorepo 里拆成独立包做成真正可发布的命令行工具。现在的 CLI 还依赖仓库内的脚本上下文拆出去之后别人用起来会更干净。第二是增加更多“业务片段”的预设。目前有登录、用户列表、日志、上传接下来想做权限管理、工作流审批、数据字典这类在中后台里出现频率极高的功能。当然这些片段会比“列表页”复杂得多目前还在靠真实项目反向打磨。第三是给生成器加一个“反向扫描”能力从一个已有项目里识别哪些目录和文件符合约定生成报告。这个功能一旦做出来老项目就能更平滑地纳入 t3code 的管理体系。6.2 我个人使用 t3code 的一点体会说句实话做这套东西最大的收益不是省下一两天的初始化时间。省时间是显性的收益但更大的隐性收益在于工程规范被当作代码来管理了。以前讨论“路由应该怎么组织”“请求层要不要统一封装”是开会聊现在直接看模板和生成规则就够了有不满意的地方就改模板提 MR人人都能参与演进。规范从“口头共识”变成了“可评审的代码”这是我认为最值得推广的工作方式。还有一个对我个人影响很深的小习惯现在我每遇到第三次重复劳动就会停下来琢磨能不能把它固化进模板。这个原则听起来很简单真正坚持下来之后你会发现自己的项目工具箱里积累的都是“被验证过”的资产而不是一次次临时复制粘贴出来的碎片代码。如果你也对这种工作方式感兴趣我的建议很直接不要等模板做到完美才开始用先从一个最小可用的版本跑起来然后让它随你的项目一起慢慢长大。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询