t3code:模板驱动的全栈CRUD代码生成工具实战
如果你跟我一样每个新项目都要花大半天搭骨架、写 CRUD、补类型定义那你大概率会喜欢 t3code。这是我花了不少业余时间维护的一个代码生成工具核心就一件事把全栈项目里那些又重复又容易出错的样板代码交给模板去生成。它不是要取代你写业务逻辑而是把那些“不写不行、写了又没技术含量”的部分用一条命令稳妥地搞定。这篇文章我会把 t3code 从设计思路到实际使用完整拆一遍包括为什么用模板驱动而不是直接改 AST、初始化项目和生成模块的具体操作、自定义模板的机制以及我踩过的几个坑。适合正在搭全栈项目、或者想给自己的团队做一套提效工具的人参考。无论你最终用不用 t3code这套思路本身是通用的值得花几分钟看看。1. 为什么会有 t3code项目定位与设计拆解1.1 从重复劳动到模板化先说痛点。我之前参与的几个 TypeScript 全栈项目技术栈基本固定在 Next.js tRPC Prisma Tailwind 这一套上。听起来很现代但实际操作起来每次新增一个业务模块流程几乎一模一样先改 Prisma schema 加 model再写 zod 的校验 schema然后去 tRPC router 里注册查询和变更接口接着写 React Query 的 hook最后还要做列表页和表单页。一个模块下来光复制的代码就有几百行而且字段名稍微改一下所有地方都要跟着改漏一处就是编译错误或者运行时报错。这种活儿说难不难但特别消磨耐心。于是我想为什么不把这些流程抽象成一份配置让工具自动生成t3code 就是从这个问题开始的。它的核心思路非常简单用一个model定义类似上一张数据表结构然后通过一套约定好的模板一次性生成 Prisma model、zod schema、tRPC router、React Query hooks 和基础页面。取名叫 t3code也有一部分致敬 T3 Stack 的意思但更多是因为我把它拆成三个关键词Type-safe类型安全、Templated模板化、Turbo快速。这三件事分别对应了工具的三个目标生成的代码类型正确、生成过程完全模板驱动、生成速度足够快到无感知。1.2 工具链选型为什么是 CLI 模板引擎选技术路线时我考虑过两种做法一种是用代码 AST抽象语法树去修改现有文件比如拿到某个 tRPC router 的源码解析后往里插入新的路由另一种就是我最熟悉的模板引擎方案根据输入配置渲染出一整套文件。最后选了模板引擎。原因很实在AST 方案对目标代码的形态要求极高只要项目结构稍微改动或者用了某些语法糖解析器就要跟着升级维护成本是持续性的。而模板方案天然适合我想要的“约定优先”模式——代码长什么样由模板决定生成结果稳定可预测。具体技术栈上我用 Node.js 写 CLI命令解析用的 commander模板引擎用的 handlebars。选 handlebars 而不是 ejs是因为它的语法更克制逻辑少更不容易在模板里写出复杂判断。而且 handlebars 支持自定义 helper适合做一些字段名转换之类的操作。比如把createdAt转成created_at、把Post转成posts这些用 helper 就能干净地处理。方案优点缺点我的取舍AST 修改保留已有代码、可增量修改解析器维护成本高、对代码风格敏感不采用字符串拼接简单直接复用性差、转义地狱不采用模板引擎渲染稳定可预测、易调试、生态成熟需要维护模板本身采用 handlebars道理其实和租房子一样AST 方案像是拿到钥匙后自己慢慢改装修模板方案则像是拎包入住菜色都固定了但每天都能准时开饭。对我这种一个人维护开源工具的场景稳定压倒一切。2. 核心功能解析t3code 能帮你做什么2.1 init一键生成标准化工程骨架t3code init负责项目最开始的工程化配置。你不需要手动去把 Next.js、tRPC、Prisma、Tailwind 一套组合拳全部配一遍运行命令后它会问你几个问题然后直接生成整个骨架。交互式问题大致包括项目名、包管理器npm / pnpm / yarn、是否需要 Prisma、是否需要 Docker 开发环境、CI 用 GitHub Actions 还是直接跳过。这些配置项最终会汇总成一个.t3code/config.json文件后续所有 generate 命令都会参考这份配置。init 生成的文件结构大致是这个样子my-app/ ├── src/ │ ├── pages/ │ │ ├── api/trpc/[trpc].ts │ │ └── index.tsx │ ├── server/ │ │ ├── db.ts │ │ └── api/routers/_app.ts │ └── styles/globals.css ├── prisma/ │ └── schema.prisma ├── .t3code/ │ ├── config.json │ └── templates/ ├── .env.example ├── package.json └── tsconfig.json这里有个设计上的小细节config.json不止是给 init 用的它也是团队约定的一部分。任何人拿到项目仓库跑一下t3code doctor就能看到当前项目配置与默认配置的差异。这样可以避免“我本地能跑你本地跑不起来”的经典问题。2.2 generate数据模型驱动的模块生成t3code generate是整套工具里用得最多的命令。它的输入是一个模块定义输出是该模块相关的全套代码。基本用法是t3code generate module Post --fields title:string, content:text, published:boolean这条命令会做以下几件事更新prisma/schema.prisma追加 Post model生成src/server/api/routers/post.ts包含 CRUD 对应的 tRPC 过程生成src/validation/post.ts即 zod 校验 schema生成src/hooks/post.ts封装 React Query 的查询和变更 hook生成src/components/post/PostList.tsx和PostForm.tsx的初始版本更新_app.ts把新 router 注册进去。你可能注意到了这个工具默认所有 string 类型的字段都会同时出现在列表和表单里。这是有意的取舍工具生成的是最通用的 CRUD 形态如果你有特殊需求比如某个字段只在创建时出现、某些字段要做下拉框可以在生成后手动调整或者通过自定义模板把团队规范固化进去。2.3 validate一致性校验与代码门禁模板生成的代码多了以后容易忽略一个问题某个模块已经生成了后来你在 Prisma schema 里加了一个字段但 zod schema、页面组件都还停留在旧状态。t3code 为此提供了一个 validate 命令t3code validate它的工作方式不是靠静态分析插件而是很“笨”但有效重新读取当前项目的 Prisma schema 和.t3code/config.json按照同样的模板渲染一份“期望输出”到内存里然后和磁盘上的实际文件做逐字对比。不一致的地方会作为 diff 输出并提示你是手动改过的还是删除过的。这套设计让我避开了维护 TypeScript AST 解析器的坑同时又实现了类似“类型同步检查”的效果。毕竟模板是唯一的真相来源只要现状与模板不一致系统就知道哪里需要关注。配合 CI 使用效果更好提交代码时跑一遍t3code validate --ci任何未同步的改动都能在合并前被发现。3. 实操演练用 t3code 生成一个完整 CRUD 模块3.1 环境准备与安装t3code 要求 Node.js 18 及以上版本。安装非常简单我推荐用 npm 全局安装或者直接用 npx 拉最新版npm install -g t3code然后确认版本号t3code --version如果网络环境一般建议把包管理器的 registry 切到国内镜像否则首次安装依赖时可能要等很久。这个属于环境问题后面在常见问题里也会提到。3.2 初始化项目从零到可启动找一个空目录执行t3code init my-app cd my-app npm installinit 会生成完整骨架但为了不让你陷入“依赖黑洞”它不会主动执行包安装。安装完依赖后先启动一次开发服务确认基础环境正常npm run dev第一次跑起来看到 Next.js 欢迎页说明 init 生成的骨架没问题。这里我还是建议你立刻git init并提交一次初始代码后面用 generate 生成代码时所有文件变更都清清楚楚出了问题可以直接回退。3.3 生成 Post 模块字段定义到代码落地现在假设我们要做一个博客系统先从 Post 模块开始。执行t3code generate module Post --fields title:string, content:text, published:boolean, viewCount:int命令结束后我们先看 Prisma schema 里的变化。prisma/schema.prisma会增加一段model Post { id Int id default(autoincrement()) title String content String db.Text published Boolean default(false) viewCount Int default(0) createdAt DateTime default(now()) updatedAt DateTime updatedAt }这里的类型映射规则是内置的string - Stringtext - String db.Textboolean - Boolean并给默认值falseint - Int带默认值 0。每次生成都会自动补id、createdAt、updatedAt这些通用字段不用你重复声明。接着看 tRPC router。src/server/api/routers/post.ts会生成类似这样的代码import { z } from zod; import { createTRPCRouter, publicProcedure } from ../trpc; import { postSchema, postCreateSchema, postUpdateSchema } from ../../validation/post; export const postRouter createTRPCRouter({ list: publicProcedure .input(postSchema.partial().optional()) .query(async ({ ctx }) { return ctx.db.post.findMany(); }), getById: publicProcedure .input(z.object({ id: z.number() })) .query(async ({ ctx }) { return ctx.db.post.findUnique({ where: { id: ctx.input.id } }); }), create: publicProcedure .input(postCreateSchema) .mutation(async ({ ctx }) { return ctx.db.post.create({ data: ctx.input }); }), update: publicProcedure .input(postUpdateSchema) .mutation(async ({ ctx }) { return ctx.db.post.update({ where: { id: ctx.input.id }, data: ctx.input, }); }), delete: publicProcedure .input(z.object({ id: z.number() })) .mutation(async ({ ctx }) { return ctx.db.post.delete({ where: { id: ctx.input.id } }); }), });注意这里的publicProcedure是示例默认值如果你的项目接入了鉴权可以在.t3code/config.json里把 procedure 类型改成protectedProcedure生成的代码就会自动替换不用每条都手工改。再打开src/validation/post.ts会看到对应的 zod schema。这块的易错点在于 create 和 update 的校验逻辑其实不一样create 不需要 idupdate 又要求 id 必传。模板里已经把这两种形态拆开了create 用omit({ id: true })update 用extend({ id: z.number() })避免你无脑一套 schema 走天下。最后跑到页面上看一眼src/components/post/PostList.tsx里已经帮你写了useQuery拉列表的完整代码PostForm.tsx里也有基本的表单状态管理。这个版本不是最终形态但它给你提供了一个基本能跑、类型全对的起点你只需要往里面填业务细节。3.4 自定义模板让工具适配团队习惯默认模板不可能满足所有人所以 t3code 留了自定义模板的口子。项目根目录下的.t3code/templates/里存放着所有内置模板的副本你改了这个目录下的文件后续 generate 就会用你的版本而不是默认版本。举个例子。假设你们的团队规定所有 tRPC query 必须返回统一的{ data, message }包装结构那你可以打开post.ts.hbs这个模板文件把 query 的 return 部分改成你们需要的格式list: publicProcedure .input(postSchema.partial().optional()) .query(async ({ ctx }) { const data await ctx.db.post.findMany(); return { data, message: ok }; }),模板里的占位符是 handlebars 语法比如{{entityName}}会被替换成Post{{entityNameLower}}会被替换成post。字段遍历用的是{{#each fields}}...{{/each}}。如果你需要更复杂的命名转换可以往.t3code/templates/_helpers.js里注册自定义 helper。自定义模板这东西建议团队里由一个人统一维护不要每个人都各改各的否则模板版本一乱生成的代码风格就五花八门了。改完之后记得跑一下t3code validate确认模板本身没有破坏现有生成结果。4. 常见问题与避坑实录4.1 模板变量冲突与转义这是我早期踩过最深的坑。handlebars 用双花括号{{ }}作为变量标记而很多框架的模板字符串也用{}或者${var}语法。比如你要在生成的 React 组件中输出一个变量const [count, setCount] useState({{initialCount}});handlebars 会先把{{initialCount}}替换成实际配置值这没问题。但如果你在模板里写了一行包含 JavaScript 模板字符串的字面代码比如const url /posts/${id};问题就来了。handlebars 遇到${id}本身不会报错因为这不是它的变量语法。真正麻烦的是你写的手写代码里出现了{{tag}}这种双花括号却希望它原样保留到生成后的文件里。比如某些 CSS-in-JS 库的语法const Box styled.div color: ${(props) props.color}; ;这里的${(props) ...}是 JS 模板字符串语法handlebars 不会碰它所以其实不冲突。真正的冲突场景是 NoSQL 数据库查询或某些后端框架里{{ }}本身是占位符的情况。解决办法有两个一个是设置noEscapeAsCaretString之类的配置另一个是自定义一个 helper 专门输出原始字符串。我的建议是遇到这种文件直接在模板里用{{{{raw}}}}...{{{{/raw}}}}块把原文包起来明确告诉 handlebars 这段别动。4.2 文件名大小写与跨平台路径另一个容易翻车的地方是文件名。t3code 默认会根据实体名生成文件名比如Post - Post.tsx、post.ts。在 Linux 上没问题但 Windows 和 macOS 的默认文件系统对大小写不敏感如果你先生成了post.ts后来又生成了Post.ts系统会认为是同一个文件导致覆盖或冲突。我的处理方式是所有文件名统一使用 kebab-case 或 camelCase类名和组件名才用 PascalCase。比如组件文件post-list.tsx工具模块post-util.tsReact 组件名PostList这样既符合社区习惯也避免跨平台踩坑。另外生成文件时如果遇到同名文件默认策略是跳过并提示--force参数才会强制覆盖。日常使用中不要轻易开--force尤其当项目已经提交到 git 后宁可手动处理也别让模板一键覆盖掉你改过的业务代码。4.3 覆盖生成与增量更新t3code 生成的是静态文件不是增量修改。这就带来一个逻辑问题你生成了 PostForm.tsx然后在这个文件里加了很多业务逻辑之后因为某个字段变更又跑了 generate那文件会不会被重置答案是不会默认策略是“文件已存在则跳过”。但这样又引出了新问题改了 Prisma schema 的字段模板不会自动更新旧文件。所以我的工作流是先把 generate 当作一次性脚手架生成后所有代码就归开发者所有工具不再介入需要更新时直接用t3code validate对比差异再决定手动修改还是删除重生成。说实话这几个提示在 CLI 里也写得很明白但很多人不仔细看。我见过同事在 project 目录下直接t3code generate --force把两个模块的页面全部重置了然后慌慌张张从 git 里恢复。所以再强调一次--force是一个危险操作使用前请确认自己的文件已经提交过 git。4.4 ORM 与数据库类型映射不同类型数据库之间的字段类型差异很大SQLite 的TEXT、PostgreSQL 的TIMESTAMP(3)、MySQL 的DATETIME(3)看似差不多实际生成的 Prisma schema 会不一样。t3code 默认针对 PostgreSQL因为这个组合最常见。如果你用的是 SQLite 或 MySQL我建议你在.t3code/config.json里配置数据库类型工具会自动切换默认值策略。比如 Postgres 下boolean字段默认值可以写成default(false)SQLite 下也支持但text这类字段的映射就不同了。类型映射表内置了几十组常见规则遇到不认识的类型可以先查一下t3code types list避免生成后还要手忙脚乱地改 schema。这个问题的本质是工具内置的约定再全面也不如你团队的实际情况来得复杂。所以模板的意义不是替你决定所有事而是把那些你已经决定好的事以最快速度落地。4.5 性能问题大项目生成慢怎么办刚开始用的时候在很小的 demo 项目上跑 generate 几乎是瞬间完成。后来我把 t3code 用在一个几十个模块的中型项目上明显感觉到 init 之后 regenerate 所有模块需要几秒钟主要耗在读取并解析现有 Prisma schema 上。排查下来的主要瓶颈是文件读取和模板编译同步执行导致大量 I/O 操作排队。优化方向很明确改用异步 API、增加模板编译缓存、对无需处理的文件跳过 stat 检查。现在已经改善了很多如果你也在自己的工具里遇到类似问题可以优先考虑这两点第一模板引擎的 compile 过程一定要缓存不要每次生成都重新解析第二不要用递归同步遍历目录用异步遍历或者直接列出已知文件列表性能差距很大。另外如果你使用 Ant Design 等重型组件库生成的页面文件可能体积不小但这是最终产物的问题不是工具本身的瓶颈。t3code 只负责把文件落盘真正的构建性能还得靠你自己的打包器优化。说到底t3code 不是什么黑科技它的思路非常朴素把团队约定写成模板把重复劳动交给命令把注意力留给真正的业务逻辑。我自己维护这个工具大半年最大的收获不是“写代码变快了”而是项目里不同模块的风格终于整齐了类型定义和数据库模型不再经常打架。如果你也受够了每个模块都复制粘贴再改字段名的日子不妨先用文章里这套思路搭一个只覆盖你们业务主要场景的模板工具跑通之后再一点点加功能。