t3code:模板驱动生成TypeScript/React代码,告别重复样板 最近团队来了几个新同学代码里开始出现各种“有点不一样”的列表页写法有人用useEffect拉数据有人用react-query有人干脆把接口返回直接塞进useState。我耐着性子一个个去对齐改到第三天实在受不了了。与其天天做代码警察不如把这类重复的样板代码交给机器去生成。于是就有了t3code这个小工具一个本地优先、模板驱动的代码生成器输入一份 YAML 配置输出可以直接用的 TypeScript / React 代码。如果你也在为重复度极高的 CRUD 页面、数据模型、枚举定义发愁或者团队刚立项想统一代码风格这篇分享应该对你有点用。我会把 t3code 的定位、模板语法、真实使用链路和踩过的坑全部写出来代码可以直接抄思路可以复用到你自己写的任何脚手架里。1. 为什么我会写一个叫 t3code 的代码生成器1.1 痛点手写样板代码的隐性成本先说个数据。我统计过自己一个月的工作量真正需要动脑子的业务逻辑大概只占三成剩下七成是在重复搭建结构相似的模块数据模型、API 封装、列表页、表单页、枚举字典、状态管理。这些模块里除了字段名不一样骨架几乎完全一样。更坑的是每个人对“骨架”的理解还不一样。有人喜欢把请求逻辑直接写在组件里有人习惯拆成独立 service 文件有人用null做空态判断有人非要undefined。代码评审的时候这些差异会引发大量“没营养”的评论真正要评的业务逻辑反而被淹没。这种成本很难量化但它真实存在。新人上手慢老人重复劳动review 效率低接口字段一变相关页面要手动改五六个地方。我们缺的不是更多规范文档而是一个能把规范直接固化成产物的工具。1.2 t3code 是什么以及解决什么问题t3code 的思路很朴素先定义一份标准的配置YAML再准备一批模板文件基于 Handlebars 语法做裁剪然后一条命令把配置和模板揉在一起生成完整代码。整个过程完全在本地完成不需要联网不依赖后端服务不对你的项目做任何侵入式改动。它解决的问题主要有三类统一代码风格模板由技术负责人维护生成出来的代码天然同一种写法新人不需要事后纠正。接口字段变更的批量处理只需要改配置里的字段定义重新生成相关文件不用再满项目搜索手工修改。降低重复性工作的心理负担写配置比写一堆样板代码快得多而且不容易出错生成完再手工补充业务细节节奏感会舒服很多。我用它跑了几个真实的中型项目之后团队里的“僵尸代码”数量明显下降至少大家不会再把同样的列表页写出四五种花样。1.3 t3code 能做到什么程度以及边界在哪坦白讲t3code 不打算做“点一下生成整个项目”那种全家桶式解决方案。它的定位很明确负责生成那些结构确定性高的文件比如数据模型、DTO、API 封装、CRUD 页面骨架、状态管理模板。真正复杂的业务逻辑、特殊交互仍然需要你手工完成。这套边界很重要。一旦工具试图覆盖所有业务场景模板就会变得极其复杂最后变成维护模板比维护业务代码还费劲。所以 t3code 有意保持了“轻度”和“透明”生成出来的代码就是普通文本文件不依赖运行时库不需要引入 SDK你随时可以删掉生成的文件改成手写代码没有任何负担。2. 核心设计三层模型与两个关键取舍2.1 输入配置、模板层、渲染引擎的协作方式t3code 的内部结构不复杂就三层配置层、模板层、渲染引擎。配置层用一份t3code.yaml描述项目级的上下文和文件生成清单。比如项目名、作者、语言类型、公共变量以及“哪个模板生成到哪个目标路径”。模板层是一个目录里面存放.hbs模板文件每个模板头部有一段frontmatter用 YAML 指定输出路径和渲染前置条件。渲染引擎则是基于 Handlebars 定制的一层封装负责把配置里的变量注入模板并解析出最终的输出文件。可以想象成做菜的流程配置是菜单模板是菜谱渲染引擎是炒锅。菜单决定今天做什么菜菜谱决定怎么做炒锅负责把材料变成成品。三个部分互不依赖你改了菜单不需要动菜谱换了炒锅也不影响菜单和菜谱。2.2 关键取舍一不解析源代码只做文本渲染在设计初期我认真考虑过要不要引入 AST抽象语法树解析。比如读取现有的 TypeScript 文件分析接口结构然后自动插入新字段。听起来很智能但实现复杂度会直接爆炸——你需要处理各种奇怪的语法、注释、装饰器、import 排序随便一个边界情况都能让你修一下午。最后我决定退回到文本渲染。t3code 不需要知道目标文件原来是长什么样的它只是根据配置和模板生成一份全新的文件覆盖到指定位置。好处是逻辑简单、稳定可靠、几乎不出错代价是它无法做增量合并每次生成都是整体覆盖。这个取舍让我保住了双休。如果你也想做类似工具建议先别碰 AST 解析除非你已经明确知道解析它要解决什么不可替代的问题。2.3 关键取舍二约定大于配置但保留必要的动态能力t3code 的模板输出路径允许写变量表达式比如src/modules/{{kebabCase moduleName}}/api.ts也允许用{{#if withApi}}控制某段代码是否输出。这给了使用者很大的灵活性。但灵活性是一把双刃剑。如果模板里到处都是条件分支和动态路径最后整个模板会变得难以阅读和写成天书差不多。所以我在文档里定了一条不成文的规矩80% 的模板应该保持简单的变量替换20% 的模板可以用条件和循环。凡是分支里嵌套超过两层的模板就是该拆分成多个小模板的时候了。这一条让我在后续维护里少掉了很多头发。开发者在面对“太灵活”的工具时缺的往往不是能力而是克制。代码生成工具尤其需要这种克制。3. 从零跑通 t3code安装、配置与第一个生成任务3.1 安装与项目初始化t3code 是 Node.js CLI 工具全局安装或者用npx跑都可以。我平时习惯在项目里装成 devDependency这样每个同事的版本都能锁在package.json里避免“在我电脑上是好的”这类问题。npm install -D t3code然后在项目根目录执行npx t3code initinit 命令会生成一套推荐的目录骨架templates/ model.hbs api.hbs page.hbs t3code.yamlt3code.yaml是这个工具的总配置。第一版长得像下面这样project: name: shop-admin language: typescript style: prettier context: author: zhang3 reactVersion: ^18.2.0 files: - template: model.hbs output: src/models/{{camelCase name}}.ts - template: api.hbs output: src/api/{{camelCase name}}.tscontext里的变量是全局的所有模板都能拿得到。files数组则定义了由哪个模板生成到哪个路径。注意这里的output是支持模板表达式的{{name}}是运行时你传入的参数。3.2 一个最简模板的生成链路下面是一个简单的数据模型模板model.hbs--- output: src/models/{{camelCase name}}.ts --- {{! 数据模型模板 }} export interface {{pascalCase name}} { id: string; createdAt: string; updatedAt: string; {{#each fields}} {{this.name}}: {{this.type}}; {{/each}} }命令行下执行npx t3code generate --name product --fields title:string,price:number,status:stringt3code 会把你传入的name和fields合并进全局上下文然后找到model.hbs渲染输出到src/models/product.ts。生成的文件会带有author zhang3之类的注释头这部分在 frontmatter 里通过meta控制。你可能已经注意到这个链路没有用到任何数据库连接、没有运行后端服务纯粹是“配置 模板 命令”三件套。这也是 t3code 一个很重要的特性离线可用不碰业务数据。3.3 参数规范与命名转换的约定为了让命令行参数不会越传越多t3code 里内置了一套命名转换 helper你可以在模板里随时调用helper 名称输入示例输出示例适用场景camelCaseproduct_listproductList变量名、文件名pascalCaseproduct_listProductList类名、接口名kebabCaseProductListproduct-list目录名、URLsnakeCaseProductListproduct_list数据库字段名constantCaseProductListPRODUCT_LIST枚举值、常量这套命名转换体系是在实操中被逼出来的。团队里有人用下划线命名接口字段有人用驼峰命名互相看不惯。有了同一套 helper 之后模板里统一写{{pascalCase name}}不管配置里传什么格式进来生成出来的接口名永远是标准帕斯卡。这不是什么高深技术但对团队协作的改善立竿见影。4. 模板语法与渲染能力真正拉开差距的地方4.1 变量插值和内置判断逻辑模板语法以 Handlebars 为基础所以{{变量名}}是最基础的插值方式。除此之外我封装了一些更贴近生成场景的判断逻辑。比如在一个 API 文件模板里你可能想根据withDelete参数决定是否生成 DELETE 方法{{#if withDelete}} export async function delete{{pascalCase name}}(id: string) { return request.delete(/api/{{kebabCase name}}/${id}); } {{/if}}if和each是最常用的两个 helper。unless也有偶尔用到的场合比如“只有当某个值为空时才生成默认导出”{{#unless customExports}} export default { get: get{{pascalCase name}}, create: create{{pascalCase name}} }; {{/unless}}4.2 循环遍历与索引控制生成表单或者表格列定义的时候each循环是主力。t3code 在循环内部额外注入了一些辅助变量比如index当前下标、first是否第一个、last是否最后一个。export const columns [ {{#each fields}} { title: {{this.label}}, dataIndex: {{this.name}}{{#if last}}, fixed: right{{/if}} }, {{/each}} ];这段模板的逻辑是最后一个字段自动加上fixed: right。实际项目里表格的最后一列往往是操作按钮固定右侧确实合理。如果你不用last就得在数据配置里多塞一个isLast字段那配置就会越来越脏。4.3 片段复用避免模板文件越长越难维护当多个模板里有相同片段时t3code 支持用include语法把公共部分拆出来。我在维护一个商城后台时每个页面的搜索表单结构几乎一致于是抽了一个search-bar.hbs片段。{{#include partials/search-bar fieldssearchFields}}片段文件放在templates/partials/目录里。片段的变量传递和普通模板一样关键在于片段的命名空间是独立的不会因为外部模板里有个同名变量就把渲染结果污染掉。这一点看着简单实际使用时能帮你绕开很多隐蔽 bug——比如你配置里有一个name变量片段里声明的name不会覆盖外层的值两边各算各的。调试时不用再满脑子推断“模板里的这个 name 到底是哪里传进来的”。4.4 错误提示是模板设计不可忽略的一环渲染模板最怕遇到“类型对不上”的静默错误。比如你配置里写的是fields: []模板里却写了{{#each fields}}循环体内部访问this.name理论上应该报错但如果底层引擎忽略空白、静默返回空字符串你生成出来的代码就会出现很多 “undefined” 字符串而且很难定位。t3code 在此处做了两个增强一是对未知 helper 直接抛错二是对缺失变量给出明确的错误信息比如“模板 model.hbs 第 3 行引用了不存在的变量fields”。这两个增强在开发模板时能救命。谁都不想在凌晨两点的上线前夜对着一个空白的 undefined 字段发呆。5. 实战案例用 t3code 生成一个完整的 React 数据列表页5.1 准备配置与模板结构理论讲再多都不如跑一个完整案例直观。下面我用一个“订单列表页”的生成过程带你看完全链路。在t3code.yaml里新增一组配置- template: page.hbs output: src/pages/{{kebabCase moduleName}}/index.tsxpage.hbs模板的 frontmatter 指定输出路径模板内容大致包含列表组件骨架、状态管理、搜索表单、表格列定义、调用 API 的入参类型。这里我不把完整模板贴出来代码太长只贴几个关键片段。列表页模板头部--- output: src/pages/{{kebabCase moduleName}}/index.tsx --- import React from react; import { Table, Button } from antd; import { get{{pascalCase moduleName}}List } from /api/{{kebabCase moduleName}}; {{#if hasSearchForm}}import SearchForm from ./components/SearchForm;{{/if}}表格列定义const columns [ {{#each fields}} { title: {{this.label}}, dataIndex: {{this.name}}, key: {{this.name}}, {{#if this.width}}width: {{this.width}},{{/if}} }, {{/each}} ];5.2 执行生成并补充业务细节执行命令npx t3code generate \ --moduleName order \ --hasSearchForm true \ --fields orderNo:string,amount:number,status:stringt3code 会生成src/pages/order/index.tsx到你的项目里。此时文件里大部分是可直接运行的代码但业务细节自然还需要你手工补比如某个字段需要自定义 render、某个按钮需要接弹窗逻辑。这一步正是前面说的“工具生产 80% 的骨架你补 20% 的肉”。这里有个重要心得生成的文件一定要允许手工修改。t3code 不会在文件里加“本文件由工具生成禁止修改”之类的警告它只负责生成不负责锁死。因为真实开发的后续迭代里几乎每个生成文件都会被人改过。你要是锁死文件那这个工具就会被团队彻底抛弃。5.3 二次生成时如何处理手工修改过的文件这是代码生成工具最容易翻车的地方。我见过很多自己写脚手架的人配置改了一个字段重新生成整份文件被覆盖手写的逻辑全丢了。t3code 没有做智能合并但我用了一个土办法避免悲剧约定文件分片规则。在模板里我把“工具生成区域”和“人工扩展区域”用注释显式隔开// GENERATED ZONE START ...这里由工具覆盖... // GENERATED ZONE END page.hbs只覆盖GENERATED ZONE之间的内容。如果你在区间之外写代码重新生成时不会被清掉。这个方法远不如 AST 级合并“高级”但胜在可理解、可维护团队每个人用过来都不会踩坑。5.4 生成结果的检查与格式化生成代码之后我还会建议你在自己的编辑器里执行一遍 Prettier。t3code 本身不依赖 Prettier但生成的模板可能会出现空白字符不一致的问题统一格式化一下最稳妥。在 CI 脚本里我会加一条npx t3code generate npx prettier --write .这样提交到仓库里的代码全都会被 Prettier 洗一遍不会出现“你生成的文件没对齐就没法过 lint”这种尴尬事。这一条是小细节但体验提升非常明显。6. 实战部署之后遇到的坑幂等性、编码、跨平台路径6.1 幂等性问题重复生成不能产生无意义 diff这是我最早遇到的坑。最开始我把模板写成每次在文件尾部追加一行“generated by t3code”的注释结果代码里只要有人手工改过二次生成就会把整个文件 diff 搞得巨大完全没法 review。后来把所有模板改成整体覆盖 固定顺序输出才算摸到幂等性的门道。简单说幂等就是你连续运行两次t3code generate生成的代码应该是完全一样的不会因为时间戳、随机数、Map 遍历顺序等因素产生 diff。如果做不到那你的配置管理就失控了。在我的实践里有三条规矩保证幂等不输出当前时间戳这个极度容易引起 diff。禁止在遍历对象时依赖插入顺序fields在配置里用数组而不是对象来维护。所有模板变量必须有默认值避免同一个配置在环境变量不同的机器上渲染出不一样的结果。6.2 Windows 环境下的换行符与路径分隔符团队里不可能人人都是 macOS。Windows 用户跑同一个模板可能出现\n换行变成\r\n或者路径分隔符从/变成\这会导致 diff 异常。我的解决方案是在配置里增加lineBreak选项默认统一使用LFoutput: lineBreak: LF在渲染输出文件时统一把\r\n替换成\n。如果你想尊重 Windows 用户也可以改成CRLF但前提是团队统一不能一半人用 LF、一半人用 CRLF。这个坑看起来小实际炸过不少次提交记录。6.3 不要在模板里拼接 SQL 或密码等敏感内容写代码生成器的时候我一度很喜欢在模板里顺手生成一点 DDL 或者初始化 SQL比如建表语句。后来发现问题很严重同事图省事直接把生成的 SQL 拿到生产库执行幸好只是测试环境。从那以后我在 t3code 里默认禁用了 SQL 生成能力凡是和数据库结构相关的操作都要求开发者人工审视。另外一个在模板里绝不该出现的东西是密钥、token、连接串。即使这些值放在配置里侥幸没有提交到 Git模板一旦流传出去风险就不可控。建议把配置中所有敏感字段设为由环境变量传入并默认禁用输出到代码文件。6.4 模板工程的目录漂移问题最后一个是工程层面的坑。项目越做越大模板会越来越多早期我只有两三个模板后来膨胀到 30 多个目录结构变得很乱。后来我用一个简单的命名规则做管理templates/ entity/ model.hbs api.hbs page/ list.hbs form.hbs detail.hbs common/ partials/ helpers/配置里template字段允许写相对路径page/list.hbs。这样一个庞大的模板库也能保持清爽。如果哪天模板数量超过了你的管理能力第一反应不该是写一个更复杂的模板调度器而应该反省是不是把自己该干的活都塞给模板了。7. 给想复用这套思路的同行提几点建议不要把模板做成“超人”。代码生成器的目的是减少重复劳动不是消灭手工代码。凡是模板里出现三层以上的 if 嵌套、超过 20 行的拼接逻辑基本就是过度设计了。把这部分从模板里拆出去改成普通函数或者组件反而更让人看得懂。配置的默认值很重要。你在命令行没传hasSearchForm时模板里应该把它默认为false而不是渲染出一个 undefined。t3code 在解析参数时会对缺省值做校验能提前堵住一大批低级 bug。一定要提供t3code validate子命令。我之前吃过亏某次改动模板后语法错误直到生成代码时才暴露。后来我在 t3code 里加入了 validate 模式它只检查配置和模板是否合法不执行生成。这样在 CI 里加一步校验能快速发现问题你好我好大家好。版本化你的模板目录。模板不是一次性产物它会随着项目技术栈升级而迭代。把templates/目录和t3code.yaml放进版本管理和代码库一起 tag。哪天升级 Ant Design 或者换掉状态管理库时你可以快速比较新旧模板的差异而不是靠脑子回忆当初是怎么生成的。我在实际使用 t3code 几个月后发现最值得投入的不是某个模板写得多么炫酷而是能在生成完之后让开发者忘掉 t3code 的存在把精力放回业务本身。如果你也打算写一个类似的代码生成工具我建议从小场景切入比如先只生成数据模型跑通全链路再逐步加模板。工具是拿来用的不是拿来炫技的。现在团队里的同事已经习惯把重复模块丢给 t3code 去生成遇到特殊逻辑再手工补整体的代码一致性和开发效率都好了不少。如果你也想尝试欢迎照着我这套思路搭一个简化版踩过坑再回来交流。