你可能见过 create-t3-app但未必听说过 t3code。这是我最近两个月深度使用并重点研究的一个代码生成工具它不做 UI 组件也不做业务逻辑它专门负责一件事把“项目骨架”从手工复制粘贴里解放出来。它定位在脚手架和低代码平台之间用一种非常朴素的方式——配置文件加模板引擎把 TypeScript 全栈项目的目录结构、基础配置、连数据库的样板代码一次性生成完毕。今天这篇东西不是官方文档的翻译也不是功能清单而是我把 t3code 用在真实项目里之后踩坑、填坑、最后让它成为团队标配的全过程适合那些正在维护多个微前端或 monorepo 子项目、受够手工搭环境的同学参考。1. t3code 的核心思路与设计拆解1.1 手写基础代码的最大浪费在哪先聊一个真实场景。我的团队同时维护三个业务线每个业务线都用 Next.js TypeScript tRPC Prisma 这套技术栈按说统一技术栈应该很省事但实际上每个新仓库从零搭起来都要半天起步。不是环境不会配而是“重复劳动”太多初始化 Next.js、装 tRPC、配 Prisma、写 health check、配置 ESLint 和 Prettier、把 CI 脚本从别的项目拷过来改名字、把 Dockerfile 复制一遍改端口……这些事没有任何技术含量却消耗了大量时间。更麻烦的是三个仓库里的配置文件因为拷贝时间不同版本漂移得很厉害有的用 pnpm有的还用 npm有的 Prisma schema 字段命名风格都不一样。t3code 解决的就是这个问题。它的核心思路不是做“向导式问答”而是把项目结构描述成一份可读、可版本化的配置然后由 CLI 根据配置去渲染模板文件。这意味着你可以把整个团队的“标准骨架”固化在一份配置里以后任何新项目都从同一份模板生成而不是从某个资深员工的内存里复制。1.2 配置优先而不是交互优先用过 create-next-app 的人都知道脚手架工具通常是一路回车或者带几个 flag最终产出固定结构。t3code 不一样它把决定权全部交给了配置文件。你可以理解成传统脚手架是“你选了菜厨师按菜单做”;而 t3code 是“你直接把食谱写成代码厨师严格按代码执行”。这样设计有一个明显的好处——可审计。每个项目里都会生成一个.t3code/config.json文件它记录了项目是从哪个版本、用了哪些模板、开关了哪些插件生成出来的。以后团队成员看到项目结构不用猜“当初是怎么搭的”直接翻这个文件就行。我后来复盘时认为这是 t3code 最值得称道的一点因为大部分项目烂尾并不是从写业务代码开始的而是从“没人说得清项目基础配置为什么要这样设”开始的。1.3 与同类工具的关键差异拿它和流行的脚手架工具做个对比工具交互方式项目结构灵活性配置可复用性生成后自定义create-t3-app交互式问答中等有固定选项低每次都要选择靠人工改Plop交互式问答模板高可自定义 generators中generator 可复用靠人为维护t3code纯配置文件高支持条件渲染、循环、插件高一份配置复用多个项目可配置后重新生成Plop 也很强但它更贴近“生成单个文件或小块代码”而 t3code 更侧重“生成整个工程骨架”。如果你只是要生成一个组件Plop 可能更快但如果你要在一个新仓库里生成完整的后端服务加管理端页面框架t3code 的体量刚好合适。我选择它就是看中它能把项目结构做到“声明式”和“可版本化”。2. 安装、环境准备与快速初始化2.1 环境要求t3code 是一个 Node.js CLI 工具目前支持 Node 18 和 Node 20npm 版本建议 9 以上。它对包管理器没有硬性要求但推荐在package.json里统一声明packageManager字段否则生成出来的 lockfile 会五花八门。另一个隐藏要求是 Git因为 t3code 在生成项目后会尝试自动初始化 Git 仓库并读取当前用户名和邮箱写入新项目的.git/config如果你机器的 Git 全局配置不全这步会失败。2.2 安装命令安装很简单全局安装即可npm install -g t3code你也可以用 npx 临时调用避免污染全局环境npx t3codelatest init my-project个人建议在团队内部固定一个版本号比如t3code1.2.5避免某个成员用了新版本生成的模板和其他人不一样。我们团队踩过一次这个坑有个同事用 npx 装了最新的 alpha 版本生成的 prisma schema 里多了一个字段然后其他人基于旧版本生成代码 review 时对不上排查很久才发现是版本不一致。2.3 初始化项目进入一个空目录执行mkdir my-project cd my-project t3code init命令会检查当前目录是否为空然后提示你选择“模板源”。模板源可以是 t3code 官方仓库内置的模板也可以指向你自己维护的 Git 仓库。这一步就是先拉取模板索引不立即生成文件。真正生成之前你会看到一份预览清单哪些文件将被创建、哪些目录会被跳过、哪些变量需要你补充。确认后t3code 会把所有文件渲染出来。整个 init 过程耗时通常不超过 15 秒比起手动搭环境快了大概二十倍。提示init 生成的并不是最终可用项目它只生成骨架。如果模板里声明了依赖t3code 不会自动执行包管理器安装这算是一个特意留白的阶段。3. 配置文件与模板引擎的核心机制3.1 一个最简单的 t3code.json 长什么样t3code 所有行为都由根目录下的t3code.json控制。下面是我一个真实项目的简化配置{ name: my-service, version: 1.0.0, templateSource: githttps://github.com/my-org/t3code-templates.git, outputDir: ., variables: { projectName: my-service, packageManager: pnpm, port: 3000, enableApi: true }, plugins: [next, trpc, prisma, eslint], files: { include: [ src/**/*, prisma/**/*, public/**/*, package.json, tsconfig.json, .eslintrc.cjs, .prettierrc, Dockerfile, .env.example ], exclude: [node_modules/**, .git/**, dist/**] } }几个关键字段的作用templateSource指定模板仓库地址variables里定义的是模板中需要替换的变量plugins决定启用了哪些框架适配files控制生成文件的范围。这个设计很像把“复制模板目录”这件事情参数化了但 t3code 在参数化之上还做了条件渲染——如果enableApi是 false那么src/server/api整个目录都不会生成。3.2 模板语法不只是简单的字符串替换t3code 的模板基于 Handlebars 语法意味着你可以写简单的逻辑判断和循环。比如生成一个按模块拆分的路由文件{{#each apiModules}} import {{this}}Router from ./{{this}}/router; {{/each}} export const appRouter createRouter({ {{#each apiModules}} {{this}}: {{this}}Router, {{/each}} });如果你在t3code.json里设置apiModules: [user, order, product]渲染出来的产物就会自动有三个 import 和三行 router 挂载。这个能力非常实用它让“项目结构”变成了数据驱动而不是散落在一堆目录里的重复文件。还有一个值得说的点t3code 默认对.env文件做特殊处理不会渲染模板语法因为在.env里经常会出现$、${}这种和模板引擎冲突的字符。如果你需要在.env里注入变量必须把变量写成{{projectName}}这种形式并且文件后缀要改成.env.t3c生成后自动改名.env。这个设计避免了“变量替换把环境变量的美元符吞了”的经典故障。3.3 插件机制与框架适配t3code 的插件本质上是一组预构建好的模板片段和生命周期钩子。以prisma插件为例它会在生成前检查你是否指定了数据库类型然后选择对应的provider字段生成 schema 开头generator client { provider prisma-client-js } datasource db { provider postgresql url env(DATABASE_URL) }next插件则会在next.config.js里预置transpilePackages配置方便 monorepo 场景下引用内部包。插件之间还有依赖关系比如启用了trpc插件但没有启用nextt3code会报错提示因为 tRPC 在 Next.js 项目里通常要挂载到 App Router 上。它不会强行替你决定但会在生成前给一份插件兼容矩阵让你自己确认。注意不要把插件理解成“安装就完事”。插件的本质是模板片段所以如果你改了模板仓库里插件的源码所有新的生成项目都会受影响。这意味着插件维护也是技术债升级前最好在临时目录先跑一次全套生成对比产物差异再决定是否纳入。4. 实操用 t3code 生成一个全栈项目4.1 先定义需求再生成纸上谈兵没意思下面记录一次完整的实操。假设我要生成一个叫store-api的后端服务技术栈要求Next.js 的 API 路由对外提供接口Prisma 连 PostgreSQLtRPC 做内部调用ESLint 统一代码规范同时带 Dockerfile。需求拆解下来我需要配置以下变量t3code init store-api --template my-org/ts-backend-template这条命令会直接把模板仓库克隆到临时目录并按默认配置生成内容。但我希望自定义一下端口和模块于是先手动创建store-api目录在里面写下t3code.json把variables配置好variables: { projectName: store-api, port: 3100, dbProvider: postgresql, apiModules: [product, inventory, cart], enableSwagger: false }4.2 生成命令与参数说明配置写好之后执行t3code generategenerate命令会做四件事校验t3code.json里声明的变量是否全部有值缺失的会用交互式提示补齐。拉取templateSource指向的模板仓库缓存到~/.t3code/cache。根据plugins过滤模板文件只保留匹配files.include规则的文件。用 Handlebars 渲染模板内容输出到outputDir。整个过程日志非常啰嗦但最后会给出一个统计表格生成文件数量、模板渲染耗时、是否有文件被跳过。我这次生成了 47 个文件耗时 1.02 秒。4.3 生成后的产物检查生成完毕第一时间看目录store-api/ ├── src/ │ ├── app/ │ │ └── api/ │ │ ├── trpc/ │ │ │ ├── [trpc]/ │ │ │ └── route.ts │ │ └── health/ │ │ └── route.ts │ ├── server/ │ │ ├── api/ │ │ │ ├── productRouter.ts │ │ │ ├── inventoryRouter.ts │ │ │ └── cartRouter.ts │ │ ├── trpc.ts │ │ └── db.ts │ ├── lib/ │ │ └── env.ts ├── prisma/ │ └── schema.prisma ├── public/ ├── .env.example ├── Dockerfile ├── package.json ├── tsconfig.json └── .eslintrc.cjs逐个检查下来比较惊喜的是src/server/api下确实按照apiModules自动生成了三个 router 文件每个文件里都预置了tRPC的protectedProcedure示例。不过也有一个小问题prisma/schema.prisma里示例 model 叫做Product而我不需要这个示例需要手动删掉这不是 bug但确实说明生成的模板还是带有“教学味”生产使用前需要清理。4.4 依赖安装与运行验证生成只是第一步安装依赖才是真正消耗耐心的时候。由于生成器没有自动安装依赖我手动执行cd store-api pnpm install这里我遇到一个容易踩坑的点模板的package.json里锁定了非常多包的版本其中包括next14.2.3和prisma/client5.14.0但t3code生成时不会自动把 lockfile 生成出来它故意忽略 lockfile所以安装完依赖会发生一次大版本的依赖解析极大概率和我原先模板测试时的版本不完全一致。我的解决方法是在模板仓库里直接提交一份pnpm-lock.yaml然后让t3code在生成后把这个文件也复制到项目里。这样每个新项目都用同一套锁定版本避免“测试没问题一到生产安装就各种翻车”的情况。提示如果你要这么做记得把pnpm-lock.yaml加入模板仓库的files.include列表否则默认规则会忽略它。5. 常见问题与排查心得实录5.1 模板渲染后出现双重转义有一次生成出来的next.config.js内容变成了module.exports { transpilePackages: [quot;repo/uiquot;], }quot;是 HTML 实体编码很明显问题出在 Handlebars 的转义机制上。默认情况下{{var}}会把变量里的特殊字符转义成 HTML 实体要想原样输出必须改成三对大括号{{{var}}}。排查方法很简单在t3code.json里加一行unsafeJavascript: true这个全局选项会让所有变量在渲染到.js、.ts、.tsx文件时使用非转义模式。我当时并不知道有这个开关花了一个小时在模板里逐个把双大括号改成三面括号后来在文档的“高级选项”里才看到。真是教训。5.2 文件覆盖保护机制t3code 默认不会覆盖已存在的文件这是好事也是坑。比如你在生成之后手改了src/app/api/health/route.ts然后再跑一次t3code generate它会提示File src/app/api/health/route.ts not overwritten (already exists)但有些文件我并不想保留旧版比如prisma/schema.prisma。我的做法是在配置里加白名单overwriteFiles: [prisma/schema.prisma, t3code.json]这样相当于把这几个文件标记为“以模板为准”。配置这个字段必须小心如果你把src/**/*全部加入覆盖列表那么项目里所有人工改过的代码都会被重新生成等于自杀。我的经验是覆盖列表里只放配置文件不放源代码。5.3 生成后 tRPC 路由报 404第一次跑服务时访问http://localhost:3100/api/trpc/health一直返回 404。我以为是插件生成的代码有问题检查了半天最后发现是我在t3code.json里填的port变量被模板用来生成.env.example里的PORT3100但.env文件没有生成因为那个文件叫.env.example不会自动变成.env。也就是说服务实际跑在 3000 端口而我在 3100 端口访问自然 404。这提醒我t3code 生成的是“模板”而不是“可运行环境”环境变量文件的创建永远需要人工完成。后来我在团队规范里加了一条任何模板的 README 开头必须写清楚“生成后第一条命令是复制 .env.example 为 .env”。5.4 monorepo 下的绝对路径解析问题我们项目的模板里默认使用project/*这样的路径别名这依赖tsconfig.json里的paths配置。t3code 渲染时如果变量里包含路径字符串比如monorepoRoot: ../../模板里最外层目录的tsconfig.json会正常引用但src深处相对路径会出错。这个问题本质上不是 t3code 的锅而是模板编写者对相对路径预估错误。我的解决办法是在模板仓库里写一个.t3code/path-check.js它会在生成后扫描所有 import 语句解析失败的关键一行。虽然不能完全自动化但至少能在 CI 阶段把坏引用揪出来。5.5 常用问题速查现象直接原因解决方案渲染后出现lt;/gt;模板变量被 HTML 转义使用三花括号或开启unsafeJavascript同名文件没有被更新默认覆盖保护在overwriteFiles中加入目标文件端口对不上.env未生成手动复制.env.example为.env依赖安装版本漂移无 lockfile模板仓库中提交 lockfile插件之间报版本冲突插件依赖关系未检查在临时目录重跑生成对比产物6. 后续扩展与个人体会最后聊点不写在文档里的东西。t3code 最让我意外的是它和 Git 工作流的配合。因为t3code.json本身就是文本文件所以模板升级时可以走正常的 Git diff、Code Review、分支发布流程。我把团队模板仓库当成普通代码仓库来维护每个插件调整都像提交一个 feature。几个星期下来新项目初始化从半天变成了两分钟而且生成出来的项目之间差异极小code review 时基本不用再花时间讨论配置了。不过要提醒一句t3code 不是万能魔法。它不适合做“一次生成、永久不更新”的项目因为只要模板仓库更新了老项目不会自动获得更新除非你主动重跑 generate。对于长期迭代的项目我的建议是把 t3code 用作“初始骨架生成器”后续项目演进还是靠常规迭代。如果你想真正享受模板统一带来的好处就必须把模板仓库当作公共依赖来维护定期升级、写变更日志、做好版本标签。我现在的日常操作模式很简单新建一个目录写一段十几行的t3code.json敲下generate然后把手动敲配置的时间全部用来写业务代码。这大概也是我这两年月维护多个仓库时最满意的一个效率提升方式。