后台总有朋友问我:"t3code 是什么开源框架?在哪里可以下载?"其实它既不是框架,也不是我在维护的开源项目,而是我在一段时间里给自己和团队写的一个代码生成 CLI 工具。名字里的 t3 取自 three-tier 的缩写,意在表达这套工具的核心思想——把业务代码按"展示层、逻辑层、数据层"三层结构一次生成完。如果你平时要反复写列表页、表单页、弹窗、增删改查接口,或者刚带团队想统一项目代码风格,这篇文章值得你看完。我会把 t3code 从想法到落地的完整过程、实际代码、踩过的坑都摊开讲清楚。
1. 为什么会有 t3code:从一次两周的改版说起
1.1 那两周改版时期的原始痛点
先说背景。当时我接手的项目是一个已经迭代了两年的后台管理系统,页面数量粗略数了一下有 80 多个。功能上不算复杂,都是常规的列表、筛选、表单、详情、状态流转,但代码风格相当混乱:有的页面用 class 组件,有的用函数组件;有的请求直接写在组件里,有的走 Redux,有的走 MobX;字段校验有的用第三方库,有的手写 if。日常维护倒还能忍,真正逼疯我的是一次全字段改名。
那次产品要求把所有业务字段里的auditStatus统一改成reviewState,原因是客户的接口文档换了命名规范。听起来只是全局替换,但实际做起来远没有那么简单。我用 IDE 的全局搜索一查,auditStatus在代码里出现了 300 多次,分布在大约 40 个文件里。其中有一部分是组件内的本地变量,有一部分是 Ant Design Form 的name字段,有一部分是接口请求参数,还有一部分来自后端返回的数据结构。我花了两周时间才把这些改完,期间还因为漏改一个字段导致某个列表页的筛选条件提交后没有效果,被测试提了一个 P1 bug。
这种痛苦不是个案。经历过的人都知道,后台管理系统 80% 的页面长得都差不多:上方是筛选区,中间是表格,右侧或底部是操作按钮,点击新增或编辑弹出表单。但每个人的写法都不同,改起来就得一个个页面看代码,效率非常低。我当时的想法是:如果有一个工具,输入一份 JSON 描述,就能把整个模块的代码按照团队规范一次性生成出来,以后再遇到字段改名这种需求,改 JSON 重新生成一遍就行,那该多好。t3code 就是从这样一个朴素的念头开始的。
1.2 t3code 到底是什么
用一句话定义 t3code:一个面向后台管理系统的业务模块代码生成器。你给它一份结构化的 schema 描述,它按照内置的模板和约定,生成一整套模块代码,包含页面组件、状态管理、API 请求、类型定义和路由配置。
它的输入长这样:
{ "moduleName": "order", "pageTitle": "订单管理", "fields": [ { "key": "orderNo", "label": "订单号", "type": "text", "searchable": true }, { "key": "status", "label": "状态", "type": "enum", "options": ["PENDING", "PAID", "SHIPPED"], "searchable": true }, { "key": "amount", "label": "金额", "type": "number" }, { "key": "createdAt", "label": "创建时间", "type": "datetime" } ] }输出是一个可以直接放进项目里的目录:
src/modules/order/ ├── index.tsx # 列表页组件 ├── OrderForm.tsx # 新增/编辑弹窗表单 ├── OrderDetail.tsx # 详情页(可选) ├── api.ts # 接口请求 ├── store.ts # 状态管理 ├── types.ts # 类型定义 └── config.tsx # 列配置、表单配置等声明式配置如果你自己写,从新建文件到调通接口,一个模块通常要花半天到一天。用 t3code,跑一条命令几十秒就出来了,而且生成代码的风格统一,不需要额外 review 格式问题。它不是要取代程序员,而是把表格页这种"低创造性、高重复性"的劳动自动化,把时间留给真正需要思考的业务逻辑。
2. 叫 t3code 的原因:三层架构思想如何落到生成器里
2.1 T3 具体指哪三层
很多人第一次听到 t3code 会以为 t3 是版本号,类似 Angular 2、Vue 3 那样。其实不是。t3code 里的 T3 指three-tier architecture,对应到前端项目里就是展示层(Presentation)、逻辑层(Logic)、数据层(Data)。这个分层思路是整个生成器的灵魂。
在生成代码时,这三个层各自对应不同的目录和文件:
| 分层 | 对应目录/文件 | 职责 |
|---|---|---|
| 展示层 | index.tsx、OrderForm.tsx、config.tsx | 渲染 UI、收集用户输入、触发事件 |
| 逻辑层 | store.ts、OrderDetail.tsx中的业务判断 | 管理页面状态、处理交互流程、控制弹窗开关 |
| 数据层 | api.ts、types.ts | 发送请求、接收响应、定义接口入参出参类型 |
为什么要刻意在生成代码里保持这个分层?因为后台管理系统最容易腐化的地方就是逻辑和数据耦合。最常见的情况是:某个组件里直接调用axios,然后.then回调里又直接操作另一个组件的 DOM 引用,页面之间互相依赖,最后谁也不敢动谁。t3code 生成的代码从一开始就在目录层面把这三个层隔开,组件只通过store里的 action 发起请求,store只调用api.ts里封装好的函数,数据层只负责网络通信。这样再普通的团队也能维持一个底线:页面代码里看不到axios,请求参数和返回类型都有清晰定义,后续维护的人拿到代码能很快明白该改哪个文件。
2.2 它和 CRA、Vite 模板的本质差异
有人会说,现在 Create React App、Vite 官方模板也提供了脚手架,执行一条命令就能生成一个完整的 React 项目,t3code 做的事情不是和多此一举吗?这就得把两类工具分清楚。
CRA、Vite 这类工具解决的是"从一个空目录到一个可运行工程"的问题,它们生成的是静态样板。不管项目是电商后台还是内容管理系统,生成出来的结构几乎一模一样,你拿到手之后还是得自己写业务页面。如果把它们比作复印店,它们给你的是统一的空白表单,具体怎么填是你自己的事。
t3code 解决的是"从一份业务描述到一套完整业务模块"的问题,生成的是定制产物。它根据字段、枚举、搜索条件、接口地址这些信息,直接产出符合你当前业务需求的页面。同样用复印店类比,t3code 更像一个裁缝,你给它量体数据,它给你做一套合身的衣服,袖子长一分短一分都能控制。
这两类工具是互补关系。正规一点的工作流是这样:先用 Vite 创建工程骨架,再在工程里接入 t3code 的生成能力,后续每新增一个业务模块就跑一条生成命令。t3code 不碰构建配置、不碰路由入口、不碰全局样式,它只负责"业务模块"这一层,定位非常收敛。
3. 核心模块拆解与关键代码实现
3.1 输入层:schema 格式设计
整个工具的第一步是定义输入格式,这一步直接决定后续所有的逻辑复杂度。我在设计 schema 时给自己定了几个原则:字段少、语义清楚、接近业务直觉。
最开始的版本里,我参考过 JSON Schema 和 OpenAPI 规范,发现它们功能强大但过于复杂。对于表格页生成来说,90% 的需求用不到嵌套对象、条件约束、循环引用那些特性,反而会让使用成本变高。最后我收敛了一个 schema 核心模型:
{ "moduleName": "orderCenter", "pageTitle": "订单中心", "apiPrefix": "/api/admin/orders", "fields": [ { "key": "orderNo", "label": "订单号", "type": "text", "searchable": true, "tableColumn": true, "required": false }, { "key": "status", "label": "状态", "type": "enum", "options": [ { "label": "待付款", "value": "PENDING" }, { "label": "已付款", "value": "PAID" }, { "label": "已发货", "value": "SHIPPED" } ], "searchable": true, "tableColumn": true }, { "key": "amount", "label": "订单金额", "type": "number", "tableColumn": true, "sortable": true } ] }这里有几个关键设计点值得展开:
第一,type字段用的是业务类型而不是 HTML 类型。比如enum表示枚举,datetime表示时间,number表示数字。生成器拿到这些类型之后,自己决定映射到什么样的 UI 组件和校验规则。这样写 schema 的人不需要了解底层组件库,只需要知道业务上这是一个"状态选择"还是一个"金额输入"。
第二,searchable和tableColumn分开控制。这来自实际的教训:有些字段需要出现在筛选区但不需要展示在表格里(比如创建人 ID),有些字段只在详情页显示但不出现在列表。如果把"是否展示"定义成一个笼统的布尔值,生成出来的页面总是不符合预期,得手动改,生成器的价值就减半了。
第三,moduleName严格使用驼峰命名,生成器内部会用不同的转换函数把它变成目录名、文件名、组件名和变量名。这条规则后面引发了不少细节问题,我会在踩坑部分专门讲。
3.2 模板引擎选型:为什么选择 EJS
输入有了,下一步就是根据输入产出代码文件。这里有两个方案:一个是手写字符串拼接,另一个是引入模板引擎。我一开始天真地选择了前者,因为觉得代码生成器的模板都很简单,无非是"循环遍历 fields 生成一行行 JSX"。结果写着写着就发现不对劲。
举个例子,生成表格列配置时需要判断:
- 如果
type是enum,列要渲染 Tag 标签并映射颜色; - 如果
type是datetime,列要格式化时间; - 如果
field.required为真,表单校验规则里要加required: true; - 如果
searchable为真,筛选区要渲染对应的输入控件。
这些判断用字符串拼接也能做,但代码会变成一大坨if-else里嵌套+=字符串,可读性非常差,而且模板变化时很难维护。最后我换成了EJS 模板引擎。
EJS 的好处在于它保留 JavaScript 的语法,模板写法和普通 JSX/TSX 比较接近,学习成本低。一段模板长这样:
<div className="search-form"> <% fields.filter(f => f.searchable).forEach(f => { %> <Form.Item label="<%= f.label %>" name="<%= f.key %>"> <% if (f.type === 'enum') { %> <Select> <% f.options.forEach(opt => { %> <Option value="<%= opt.value %>"><%= opt.label %></Option> <% }) %> </Select> <% } else { %> <Input /> <% } %> </Form.Item> <% }) %> </div>这种模板写出来,任何一个接触过 React 的人都能看懂,也很容易修改。EJS 还支持<%-语法原样输出 HTML 内容,以及include子模板,可以把"表单控件渲染"这类公共逻辑抽出来复用。
选型上还有一个小考量:EJS 是纯 JavaScript 实现的模板引擎,没有原生依赖,在 Node 环境里安装即用,不像某些模板引擎需要同步编译原生模块,这在给团队分享使用时有很大优势——npm install ejs就够了,不会因为本机缺少编译环境而报错。
3.3 输出层:目录结构与文件命名规范
生成器最后一步是把渲染好的字符串写入文件系统。这部分的难点不在"写文件"本身,而在于命名规范的自动化。
在后台管理系统里,我们约定一个模块的文件命名规则:
- 列表页组件:
index.tsx - 表单弹窗:
OrderForm.tsx - 详情组件:
OrderDetail.tsx - API 文件:
api.ts - 状态管理:
store.ts - 类型定义:
types.ts
而目录名则来自moduleName的驼峰转短横线形式。比如moduleName: "orderCenter",目录就是src/modules/order-center/。组件内部使用的变量名,比如orderCenterStore、fetchOrderList,又从短横线形式转回驼峰。
这套命名逻辑本身不复杂,但容易出问题的地方是英语复数、缩写、连接词这些场景。我维护的过程中遇到过几个真实案例:
- 模块名是
userInfo,目录应该生成user-info还是user_info?团队约定只能用短横线,所以是user-info; - 模块名是
sku,组件内部的 store 变量应该是skuStore还是sku_store?约定所有代码内变量一律驼峰,所以是skuStore; - 模块名是
orderDetail,文件名是OrderDetailForm.tsx还是OrderDetail.tsx?约定如果是列表模块,弹窗组件统一叫<ModuleName>Form.tsx,详情页才用<ModuleName>Detail.tsx。
这些规则写进生成器之后,等于把团队规范固化在工具里了。新同学加入团队,不需要背一份十几页的命名规范文档,只要用 t3code 生成代码,产出的命名自动就是符合规范的。这比任何 review 都管用——因为人总有漏看的时候,但生成的代码 100% 统一。
4. 跑通 t3code:从安装到生成一个完整订单模块
4.1 初始化与配置方式
使用 t3code 之前需要做一些基础配置。工具本身是全局安装的 npm 包,初始化命令:
npm install -g t3code t3code initinit会在项目根目录生成一个t3code.config.js文件,内容如下:
module.exports = { framework: 'react', uiLibrary: 'antd', language: 'ts', apiClient: './src/utils/request', modulesDir: 'src/modules', routeFile: 'src/router/routes.tsx', style: 'less' };这些配置项逐一说明:
framework和uiLibrary决定模板引擎从哪组模板目录里读取文件。我内置了 React + Ant Design 的最佳实践模板,后续可以扩展 Vue 版本;language支持ts和js,两种语言对应不同的模板分支;apiClient指向团队封装的请求函数,生成出来的api.ts会统一从该路径导入request方法,而不是直接使用 axios,这样拦截器、错误提示、token 刷新这些逻辑都集中在团队自己的封装里;modulesDir是模块的输出目录,routeFile是路由配置文件路径,生成器会在该文件末尾自动追加新模块的路由;style决定样式文件的扩展名,目前内置支持 less 和 css。
配置完成后,工具会在内存里预编译所有模板,并检查配置项的合理性。比如routeFile路径不存在时,init会尝试自动创建,并在文件里写入一个基础路由骨架,避免生成模块时报错。
4.2 实际生成一个订单中心的完整命令
现在我用一个完整的例子演示 t3code 的使用过程。假设我要生成一个"订单中心"模块,包含订单号、状态、金额、创建时间四个字段,其中订单号和状态支持搜索,金额支持排序。我先准备好 schema 文件order-center.schema.json:
{ "moduleName": "orderCenter", "pageTitle": "订单中心", "apiPrefix": "/api/admin/orders", "fields": [ { "key": "orderNo", "label": "订单号", "type": "text", "searchable": true, "tableColumn": true }, { "key": "status", "label": "状态", "type": "enum", "options": [ { "label": "待付款", "value": "PENDING" }, { "label": "已付款", "value": "PAID" }, { "label": "已发货", "value": "SHIPPED" } ], "searchable": true, "tableColumn": true }, { "key": "amount", "label": "订单金额", "type": "number", "tableColumn": true, "sortable": true }, { "key": "createdAt", "label": "创建时间", "type": "datetime", "tableColumn": true } ] }然后执行生成命令:
t3code generate order-center.schema.json工具会先去读取配置文件,分析 schema,接着调用模板渲染,最后写入文件并更新路由。控制台输出大致是这样:
[✓] 读取 schema 完成,共 4 个字段 [✓] 渲染 templates/views/index.ejs [✓] 渲染 templates/forms/Form.ejs [✓] 渲染 templates/api/api.ejs [✓] 渲染 templates/store/store.ejs [✓] 写入 src/modules/order-center/index.tsx [✓] 写入 src/modules/order-center/OrderForm.tsx [✓] 写入 src/modules/order-center/api.ts [✓] 写入 src/modules/order-center/store.ts [✓] 写入 src/modules/order-center/types.ts [✓] 更新 src/router/routes.tsx,新增路由 /admin/order-center生成出来的api.ts大致长这样:
import request from '../../utils/request'; import { OrderCenterEntity, OrderCenterListParams } from './types'; export function fetchOrderCenterList(params: OrderCenterListParams) { return request.get('/api/admin/orders', { params }); } export function fetchOrderCenterDetail(id: string) { return request.get(`/api/admin/orders/${id}`); } export function createOrderCenter(data: Partial<OrderCenterEntity>) { return request.post('/api/admin/orders', data); } export function updateOrderCenter(id: string, data: Partial<OrderCenterEntity>) { return request.put(`/api/admin/orders/${id}`, data); } export function deleteOrderCenter(id: string) { return request.delete(`/api/admin/orders/${id}`); }types.ts里会根据 schema 里的enum字段自动生成联合类型,比如:
export type OrderOrderStatus = 'PENDING' | 'PAID' | 'SHIPPED';整个生成过程足够快,基本感觉不到等待时间。对于要提交到 git 仓库的代码,我建议生成后先跑一遍类型检查和 lint,因为 t3code 生成的代码不会存在语法错误,但在实际项目中仍然可能因为接入的请求函数签名差异、UI 组件版本差异而出现类型不匹配。
5. 我的踩坑记录:这些问题不调试你根本发现不了
5.1 模板中的转义符问题导致生成的代码报错
第一个让人头皮发麻的问题出现在模板渲染阶段。EJS 默认使用<%和%>作为标签,但在生成 React 组件时,JSX 自己就包含大量花括号语法,比如{record.status}、{loading ? <Spin /> : null}。这些花括号和 EJS 的标签没有直接冲突,真正冲突的是 EJS 对<%-和<%=的处理。
有一次我在模板里写了这样一段:
const columns = [ <% fields.forEach(f => { %> { title: '<%= f.label %>', dataIndex: '<%= f.key %>' }, <% }) %> ];表面看没问题,但遇到某个字段的 label 本身包含单引号时,比如label: "客户's 订单",生成的代码就会变成:
{ title: '客户's 订单', dataIndex: 'creatorName' },JS 字符串被截断了,整个文件直接语法错误。更坑的是,这类问题在生成阶段不会报错,模板渲染是成功的,只有在编辑器里打开代码、甚至浏览器运行时才会暴露。
解决办法是在所有输出字符串的位置统一使用一个自定义过滤器,对单引号、双引号和反斜杠做转义。我在模板的公共部分定义了safeStr方法:
function safeStr(str) { return String(str).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"'); }所有<%= safeStr(f.label) %>输出都经过这道防线。这件事提醒我:代码生成器输出的东西也是代码,它的转义规则比输出普通文本更严格。从那之后,我把"安全性优先于便利性"写进了模板开发的检查清单,只要任何一个字符串可能要进入 JS 源码,就强制走转义函数。
5.2 moduleName 的大小写转换和复数问题
第二个坑和命名有关。团队里实际用过 t3code 后,我回收了不少意见,其中占比最高的是"生成出来的文件名不是我想要的"。深入排查后发现,大部分问题出在我对moduleName的处理过于死板。
比如有人把 moduleName 写成uOrder,他本意是想让目录名和接口路径uOrder保持一致,结果 t3code 按照驼峰转短横线规则生成了u-order,目录还是小写开头。再比如有人写orderCenter1,这种带数字的后缀也被转成了order-center1,看起来没什么问题,但是当模块名是merchantInfo时,生成的组件名是MerchantInfoForm.tsx,而团队约定缩写词最好全大写MerchantInfo也可以接受——这就产生了争论。
还遇到过复数问题。category模块生成列表页时,API 路径是/api/admin/categorys,但正确写法应该是/api/admin/categories。这种涉及英语语法的变形,靠字符串转换函数很难穷尽。最后我只能做两个改进:
- 在 schema 里增加一个可选的
apiName字段,专门用于指定接口路径中的资源名,如果没有则默认取moduleName转短横线; - 把驼峰转短横线的逻辑单独抽成一个
kebabCase函数,并加上一组大小写缩写词表,OrderID会转成order-id而不是order-i-d。
改完之后,生成器不再试图猜测业务语义,而是把决定权交还给使用者。工具的本质是辅助,不是替代——命名这种事,约定好规则后,人直接指定反而更可靠。
5.3 重复执行的幂等性:如何避免二次生成覆盖手改代码
第三个坑是在增量开发场景里暴露出来的。团队在某个模块生成后,又手工调整了很多细节,比如在OrderForm.tsx里加了一个自定义校验、在index.tsx里加了批量操作按钮。过了一段时间,产品要求给这个模块加一个新字段"收货地址",大家第一反应就是改 schema 再跑一遍 t3code。
结果跑完之后发现,手工加的代码全被覆盖了。那场面相当崩溃,一下午的手工修改瞬间消失。幸好当时项目刚提交过 git,才没有造成不可挽回的损失。
这让我认识到:代码生成器必须考虑幂等性,尤其是在团队协作环境里。我采用的解决方案是引入"区块标记"机制。模板生成的每个文件,在头部会写入一段标记注释:
// ================= t3code generated start ================= // 本文件由 t3code 生成,请勿手动修改生成区间内容 // ================= t3code generated end =================同时,我把每个生成文件分成了"受控区"和"自由区"。受控区是 t3code 每次生成都会重写的部分,比如字段定义、列配置、API 函数。自由区是模板里预留的扩展区域,生成器在二次执行时会保留自由区中的内容。具体实现上,t3code 在覆盖文件前先读取旧文件,用正则把旧文件自由区的文本提取出来,拼接到新文件的对应位置。
这个设计后来被团队接受,大家逐渐养成了一个工作习惯:简单的字段增删改直接改 schema 重新生成,复杂的手工逻辑写在自由区里。但我也在文档里强调,核心业务逻辑不应该依赖代码生成器的保留机制——毕竟保留机制本质上是一把双刃剑,太依赖它反而会让代码变得更难审查。
6. 从 t3code 里沉淀出来的设计思路
6.1 任何团队都可以做一个"自己的生成器"
t3code 具体代码只服务于我们团队的技术栈和业务模式,但它背后的一套设计方法,我认为任何一个有一定页面重复度的团队都可以借鉴。核心思路就一句话:把重复变成配置,把配置变成代码。
具体落地时,可以先做一次"重复度盘点"。把团队最近三个月新写的页面拉出来,统计它们之间的共同结构。通常你会发现,后台管理系统的页面可以归纳成不到十种模式:查询列表、表单新增/编辑、详情展示、树形选择、Tab 页签、步骤流程、弹窗确认、批量操作。把每种模式抽象成一个模板,再给模板定义一个最小化的配置输入,一个最简版本的代码生成器就成型了。
不要一开始就搞大而全的平台。我的建议是先做一个命令行工具,直接用 EJS 管模板,配置文件和 schema 都用 JSON 或 JS 对象。等用的人多了,模板慢慢丰富,再考虑要不要做成 Web 端的可视化配置界面。路径可以渐进,但原则要坚持:生成器只解决有明确模式的问题,不要试图覆盖所有边缘场景。
6.2 从代码生成延伸到接口文档与 Mock 数据的同步生成
t3code 跑通之后,我很快发现一个更让人兴奋的应用场景——把 schema 同时用作代码生成和接口文档生成的数据源。既然一份 schema 已经定义了字段名、类型、是否必填、枚举值、搜索条件,那么它本质上就是接口文档的雏形。
我后来在工具里加了一个t3code mock命令,它读取同一个 schema 文件,生成一份可以直接导入接口管理平台的 OpenAPI 规范文件,以及一份用于本地联调的 Mock 数据文件。Mock 数据的生成规则基于字段类型:
- 字符串类型生成
"示例文本" + 随机数; - 数字类型生成区间内随机数;
- 枚举类型在 options 里随机取一个值;
- 日期时间类型生成当前时间附近的随机时间点。
这样一来,前端写完页面直接有 Mock 数据可以联调,后端还没有开发完接口时,前端可以并行推进。等后端接口就绪,只需要把api.ts里的请求地址从 Mock 环境切到真实环境即可。schema 成了唯一的业务事实来源,页面代码、类型定义、接口文档、Mock 数据全部从它派生,彻底消除了"文档和代码不一致"的经典问题。
从全局来看,t3code 给我的最大启发不是具体技术选型,而是工作中那些让人烦躁的重复劳动,值得花时间去观察它背后的模式,再用工具去解决它。做之前,你以为自己在增加额外的工作量;做之后,你会发现省下来的时间是几倍甚至十几倍。尤其是当整个团队都开始习惯用同一个工具、遵循同一套命名和结构规范时,后期的维护成本下降是非常明显的。
如果你正在维护一个后台管理系统,而且发现每次新功能都要从复制粘贴开始,我建议你也试着写一个属于自己的"t3code"。先别管工具本身做得多完善,哪怕只生成一个列表页的初始代码,也比每次都手动新建五六个文件要强得多。最后再分享一个小技巧:schema 的字段定义顺序尽量和产品文档保持一致,生成出来的代码阅读顺序会很自然,后续改的时候也更不容易遗漏字段——这是我在多个模块的迭代里验证过的小细节。