1. 项目从哪来,t3code 到底解决什么问题
先直接说结论:t3code 是我最近在维护的一个开源小工具,定位是给 T3 Stack 全栈项目用的代码生成脚手架。如果你还不熟悉 T3 Stack,简单讲就是 Next.js + TypeScript + tRPC + Prisma + Tailwind 这一套组合,近两年在中小型全栈项目里很火,尤其是在独立开发者和创业团队中间,因为它把前后端的类型安全和开发效率拉到了一个非常舒服的位置。
我自己第一次接触 T3 Stack 是在做一个内部管理系统的时候,需要同时维护多个页面、多个数据模型,每次新建一个模块都要重复同一套动作:写 Prisma model、迁移数据库、定义 tRPC router、在页面上接 hook、写表单校验……这些工作本身不难,但极度机械化,而且特别容易出低级错误,比如某个字段名在 model 里叫userId,到了 tRPC 的输入类型里写成了userID,类型检查是能过,但跑到运行时就是一堆诡异的 bug。当时我就在想,这种重复劳动能不能靠一个命令行工具一次性解决掉,于是 t3code 的雏形就出来了。
这个项目适合谁来用?首先是已经在用 T3 Stack 做项目的开发者,它能把“从数据模型到业务页面”的链路压缩到几分钟;其次是刚接触 T3 Stack 的新手,通过生成的代码模板,可以直观看到 Prisma、tRPC、React Query 之间是怎么串联的,比我当年对着文档一点点拼要高效得多;最后是那些手里同时维护好几个类似结构项目的团队,t3code 生成的代码还支持自定义模板,相当于把团队里“约定俗成的写法”沉淀成规范,新成员上手直接照着一套标准来。
t3code 目前支持的核心功能包括:一键初始化模块、根据 Prisma model 自动生成 tRPC router、自动生成页面骨架和 CRUD 表单、以及基于自定义 Handlebars 模板的代码渲染机制。后面我会具体展开每个功能怎么用、内部是怎么实现的,以及我在开发过程中踩过的坑。
2. 为什么选 T3 技术栈,以及代码生成器该怎么做设计
2.1 T3 Stack 的核心优势没那么玄乎,就是“类型安全闭环”
很多人听到 T3 Stack 第一反应是“又是一套新技术组合”,其实它更像是一套已经被验证过的打法和最佳实践。T3 的核心思路是让类型安全贯穿从数据库到前端组件的全链路:Prisma 里定义的 model 类型可以直接被 tRPC 继承,tRPC 的 router 输出类型会被自动推导出来,再通过 tRPC 的 React Query 集成,前端调用接口时拿到的返回值都是带完整类型提示的。
这套闭环的价值在写小项目时感受不明显,一旦项目到了几十个 model、上百个接口的时候,你就知道“改了一个字段类型,从数据库到前端全部报错给你看”有多爽了。对比传统 REST + 手写 API 类型的方式,省掉的不仅是重复定义接口类型的时间,更重要的是减少了运行时才发现类型不匹配的概率。t3code 之所以选择围绕这套技术栈来做,就是因为这套链路适合做代码生成——它本身的模式高度统一,模型定义、路由声明、页面调用这三层的结构都是可预测的,只要把规则定好,机器就能替你写代码。
2.2 代码生成器不是“模板字符串拼接”这么简单
我在设计 t3code 之初,犯过一个典型错误:以为代码生成就是把一堆模板字符串往里填数据。第一版确实是这样做的,用 TypeScript 的模板字符串拼 Prisma model、拼 tRPC router,结果很快发现维护性极差。一旦要把生成逻辑拆成“基础部分 + 可自定义部分”,字符串拼接方式就完全撑不住了,代码里全是if分支和逻辑判断,看两眼就想重写。
后来我参考了社区里成熟生成器的思路,比如 Plop、Hygen 的做法,把整个架构重新拆成了三层。第一层是模板层,使用 Handlebars 模板语法编写不同类型的代码骨架,比如router.hbs、page.hbs、form.hbs;第二层是数据层,负责从用户的输入(比如 model 名称、字段列表)中提取并整理一个 JSON 对象,专门用来给模板填充数据;第三层是渲染层,负责调用 Handlebars 编译器把模板和数据合并,再处理文件写入的时候碰到“目录不存在”“文件已存在”这类情况。
这样的分层带来的直接好处是,用户如果想改默认生成的代码格式,不需要去改生成器本身的 JS 逻辑,只需要改模板文件就可以了。我甚至会建议团队把模板文件放在 Git 里单独维护,代码生成器的本体不变,模板却是团队自己订制的,这样生成的代码就自带团队风格。
2.3 为什么不是直接复制现成模板,而要依赖一个 CLI 工具
有人可能会问:既然只是生成代码,我把一套写好的模板复制到项目里不就行了吗?这还真不太行。模板复制的效率很低,因为一个典型的 CRUD 模块至少涉及 Prisma model、DTO 校验 schema、tRPC router、React Query hook、页面组件这五类文件,每类文件之间还有交叉引用的依赖关系。比如 router 文件里要引用 Prisma 生成的类型,页面里要引用 router 的useQuery,这些引用串如果靠手改,复制十个模块大概率会漏掉某个 import,或者改错某个类型名。
t3code 做的事情本质上是把这个“交叉引用”自动化了:它会分析你填写的模型定义,自动算出需要引用哪些类型、需要 import 哪些路径,然后统一写进生成的代码里。这样你拿到手的基本是一套能直接跑起来的代码,剩下要做的只是根据业务需求微调。我认为这才是代码生成工具真正有价值的点:不是省掉“打字”的时间,而是省掉“理清依赖关系”的时间。
3. 核心功能拆解与实操:从模型定义到页面生成全流程
3.1 初始化与项目接入:30 秒让 t3code 跑起来
使用 t3code 的第一步是在你的 T3 项目里安装并初始化它。安装方式就是常规的 npm 全局安装或者用 npx 直接调用,没有特殊要求。
npm install -g t3code # 或者在不全局安装的情况下 npx t3code initinit命令会在项目根目录创建一个t3code.config.ts配置文件,同时生成一个templates文件夹,里面放着一套默认的 Handlebars 模板。这套模板我会持续维护更新,不过因为它是独立于工具本体的,你可以随时改。初始化过程还会检查当前项目是否具备 T3 Stack 的基本结构,比如是否安装了@trpc/server、@prisma/client、react-query,如果没有会给出提示,并且建议你先用官方脚手架创建 T3 项目再接入,避免工具跑了一半发现缺依赖。
配置文件的内容很直白,我默认生成的配置大体长这样:
import { defineConfig } from 't3code'; export default defineConfig({ prismaSchemaPath: './prisma/schema.prisma', routerDir: './src/server/api/routers', pageDir: './src/pages', hookDir: './src/utils/api', templateDir: './templates', typescript: true, });这里比较关键的是prismaSchemaPath,它指向你的 Prisma schema 文件位置。使用 t3code 生成业务模块的时候,工具会去读取这个 schema 文件,解析里面定义的 model,这也就意味着你可以在 Prisma Studio 里直接维护模型定义,然后在终端里一条命令把相关的 router、类型、页面全部生成出来,不会出现模型和前端类型脱节的情况。我建议你在初始化之后先跑一遍t3code status看看配置是否生效,这个命令会列出读取到的 model 列表,方便确认路径解析对不对。
3.2 基于 Prisma model 生成 tRPC router 的参数逻辑
t3code 最常用的命令是t3code generate,它允许你直接指定要生成哪个 model 对应的接口。平时我会先把 Prisma model 写好,比如我要做一个简单的“文章标签”模块:
model Tag { id String @id @default(cuid()) name String @unique color String @default("#6366f1") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }然后在终端执行:
t3code generate --model Tag生成器会做以下几件事。首先解析这个 model 的所有字段,识别出字段类型、默认值、可选性、Unique 约束,同时在 Prisma 的model名和 tRPC 的命名习惯之间做转换:Tag会被转换为tagRouter、输入类型TagCreateInput、TagUpdateInput等标准命名。其次根据字段类型自动推断 zod 校验规则,比如name是必填字符串,color是可选的带默认值字符串,createdAt这类由数据库自动管理的字段会从表单输入里排除掉。
接下来会把生成的动作按“标准 CRUD + 扩展批处理”来组织。标准 CRUD 包含getAll、getById、create、update、delete,这一部分几乎是固定模板;扩展批处理是指如果 model 里有@unique字段,会自动给getByUniqueField生成一个查询接口。生成完的 router 代码会自动拼接好路径引用的前缀,并处理好 procedure 的鉴权上下文(继承protectedProcedure还是publicProcedure)。这块默认用了 T3 Stack 脚手架里的鉴权上下文约定,但你在配置文件里可以切换,比如authMode: 'public'全局不鉴权,生成时就不会带protectedProcedure。
3.3 页面与 Hook 自动化生成:一个模块的前端代码全给安排上
t3code 一个比较受欢迎的功能是它可以连页面一起生成。在 T3 Stack 里,通常流程是“router 写好了,再到src/utils/api里定义 hooks,再到src/pages里写页面组件”。这两步的样板代码非常多,尤其是 React Query 的useQuery和useMutation,每次都要配置refetch策略、onSuccess回调,写多了就麻木了。
执行t3code generate --model Tag之后,工具会在src/utils/api下生成一个tag.ts,里面做好封装:
export const useGetAllTags = () => api.tag.getAll.useQuery(undefined, { staleTime: 60 * 1000, }); export const useCreateTag = () => api.tag.create.useMutation({ onSuccess: async () => { await utils.tag.getAll.invalidate(); }, });这里我默认配置了staleTime60 秒和一成功就刷新列表的失效策略,是多年全栈开发下来觉得比较稳的组合,你可以在配置里改成自己的习惯。页面方面,默认模板会生成一个“列表 + 新建/编辑弹窗”的复合页面,字段会自动映射为对应的输入控件。字符串类型且名字里带color的字段,会自动渲染成颜色选择器;布尔类型渲染成 Switch;数值类型渲染成number输入框;带@default的字段会被预填默认值;新增和编辑共用一个表单组件,靠传入的initialData区分。
这套联动逻辑当初写的时候挺费功夫,因为要同时处理“字段类型”和“表单控件”的映射,还要考虑枚举类型在 Prisma 和 zod 之间的命名差异。现在稳定下来之后,确实能明显感受到模块开发效率的提升,原来要花一上午写的一个带表单的管理页面,现在所有代码加起来也就几分钟,剩下的时间都花在处理真正的业务逻辑上。
3.4 模板自定义与多模块批量生成
除了按单个 model 生成之外,还有一个特别适合批量操作的用法:t3code generate --all。它会遍历 Prisma schema 里全部的 model,一次性生成所有 router、hook、页面,每个模块一个独立目录。如果你手头是个全新的项目,用一条命令就能得到“所有后端接口 + 全部管理后台页面骨架”,这一版就能跑起来看了。
批量生成要注意的一点是,如果某些 model 之间有关联字段(比如Post里引用了Author的 id),生成器会自动处理外键关系,把关联字段生成下拉选择器,数据源是关联 model 的getAll接口。这个功能我觉得是对团队协作特别友好的场景,因为一对多、多对多的关联是业务里最绕的部分,工具帮你把标准做法做掉了,出错的概率就低很多。
自定义模板放在templates目录里之后,只需要注意文件名和变量命名不要和内置模板冲突。t3code 暴露给模板的数据结构是固定的,包含modelName、fields[]、relations[]、hooksPath、routerPath等字段,动手改模板前建议先跑一次默认生成,看看渲染出来的文件内容,再拿 “默认内容” 和 “你想改成什么样” 做对比,最后去改 Handlebars 模板会事半功倍。
4. 开发过程中的疑难杂症与排查心得
4.1 Handlebars 模板的数据嵌套地狱
先聊一个我踩得最深的坑:Handlebars 模板嵌套数据时页面代码会变得非常难维护。比如我要在模板里根据字段是“普通字段”还是“关联字段”来决定渲染不同的表单控件,写法上一位追求在.hbs文件里做全部判断,结果模板里全是#if、#each套#if,看代码的人会怀疑人生。后来我换了一个思路:把所有的判断逻辑放在生成器端完成,不再让模板做复杂逻辑,而是在数据阶段就把每个字段的类型、控件类型、校验规则、是否参与表单这些都算好,模板的任务只剩“拿到一个渲染好的 HTML 片段直接输出”。
举个例子,在生成器里我会把每个字段处理成这样的结构:
{ name: 'color', zodType: 'z.string().optional()', inputTag: '<input type="color" ... />', isFormField: true, }模板那边只需要一个简单的{{#each formFields}}循环就能输出整个表单。这样模板只是做“布局”和“呈现”,逻辑全部集中在数据层,既容易测试又容易调试。这个设计看起来简单,但真的是我从用坏三四个模板之后才总结出来的经验。
4.2 Windows 和 macOS 上的路径问题
另一个用户反馈比较多的坑是跨平台路径分隔符。代码生成器不可避免要拼接路径,比如path.join(routerDir, 'tag.ts'),在 Windows 上会得到反斜杠\,而 tRPC 的路由路径习惯是正斜杠/。虽然两边文件系统都能工作,但如果你把生成的引用路径直接写入代码的 import 语句里,在 Windows 上就会生成类似import ... from '.utils\api\tag'这种字符串,到 mac 上就跑不起来了。解决方法是,所有写入代码内容的路径统一做一次replace(/\\/g, '/')处理,文件系统层面的操作保留原生分隔符,但写入.ts文件的字符串必须统一用正斜杠。
这个问题虽然看起来小,但确实能让一半的团队直接跑不动生成的代码。另外文件编码也要注意,我习惯强制模板文件用 UTF-8 无 BOM,尤其是在 Windows 环境下,BOM 会导致文件头部出现不可见字符,编译时直接报“标识符无效”。网上搜这个报错很容易搜到一堆无关内容,所以我这里单独提一句。
4.3 tRPC 类型推导不生效的排查清单
使用 t3code 生成完 router 后发现前端类型没有提示,这应该是排在第一的问题。大部分情况不是你代码写错了,而是src/server/api/root.ts文件里没有把新 router 挂载进去。因为我没打算做成“自动改 root 文件”那种侵入性太强的方案,生成器只负责创建独立的 router 文件,挂载这步需要你自己去补一行:
export const appRouter = createTRPCRouter({ tag: tagRouter, });如果挂载之后类型还是不生效,先别急着骂工具,打开tsconfig.json确认一下 Next.js 的 dev 服务器有没有重新加载类型声明。tRPC 的类型生成依赖 ts 的类型检查,有时候需要重启一下 dev server 或者执行npx tsc --noEmit来强制刷新。还有种比较隐蔽的情况是,你在 Prisma schema 里给字段定义了默认值,但生成 zod schema 的时候没有把.default()带上,导致前端调create接口时 zod 校验报错,这种问题在默认模板下一般不会出现,但一旦你自己改过模板就很容易复现,我的排查思路是先把自定义模板替回默认模板,确定是不是模板的问题再做修改。
4.4 常见问题速查表
| 问题 | 原因 | 解决办法 |
|---|---|---|
| Handlebars 模板渲染全空 | 数据层字段名和模板变量名不匹配 | 先打印数据层的 JSON,对照模板变量逐字段检查 |
| Prisma model 改了但生成代码没变化 | 生成器缓存了解析结果 | 手动删掉生成的旧文件后再跑 generate |
| Windows 生成的代码 import 路径是反斜杠 | 混淆了文件系统路径与代码引用路径 | 代码内容统一用正斜杠,文件系统操作用原生 API |
| 生成的页面字段顺序和 schema 不一致 | 没注意字段顺序在解析时被重新排序 | 在配置里确认preserveFieldOrder: true |
| 数据库迁移报错 duplicate key | 多个 model 生成了同名的 unique 字段校验 | 检查 schema 中 unique 约束,确认唯一索引命名规范 |
| tRPC 请求 404 | root.ts 未挂载新 router | 在 root.ts 中手动补挂载,并重启 dev server |
5. 关于模板工程化的进一步思考
如果你只是个人用,t3code 默认模板已经够了,但如果要在团队里推广,我强烈建议把模板作为工程的一部分来维护。几件值得做的事:第一,模板单独建一个包,跟随主项目发版,有修改时写清楚 semver;第二,模板文件的变量命名要形成规范,凡是和要求不一致的都应在代码评审时直接被拦下来;第三,生成代码的产物尽量不做手工改动,如果需要改,要么改模板,要么把改造后的代码挪出生成目录。
团队用生成器的节奏,我建议是“先固化再优化”:先用默认模板跑两周,团队里所有人不管怎么写,产出的 CRUD 接口都是一模一样的,这对新人尤其友好;过了磨合期以后,再由老手根据项目实际需求,把模板逐渐调整成团队的风格。直接一开始就设计一套复杂的自定义模板,往往会阻碍入门,而且容易把团队的公共依赖搞得太重。
6. 写在最后的操作建议
根据我这段时间实际使用的体会,t3code 最值得花时间研究的不是工具本身,而是你对自己业务模块里“什么是规律性工件”的理解。如果能在动手写业务代码之前,先把数据模型设计清楚,后面 t3code 生成的代码质量会高很多。反过来,如果模型改来改去,生成代码的维护成本也会跟着上升,这不是工具的问题,而是设计层面的改动成本。
最后分享一个小技巧:在t3code generate之后,我一定会做的一件事是把生成的代码拿 Git 做一次 diff,哪怕只是扫一眼。这不是信不过生成器,而是借助 diff 快速审视模型里的每个字段最终会落到页面的哪个位置,很多隐藏的设计问题(比如字段命名不一致、类型不合适)在这一步就能看出来,比写代码时才发现要省钱得多。工具能替代重复劳动,但替代不了你对业务的理解,把它当成“帮你提速的伙伴”,而不是“替你思考的引擎”,这套方式用起来会顺手很多。