ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

t3code:基于Schema驱动的前端代码生成器实战复盘

t3code:基于Schema驱动的前端代码生成器实战复盘

去年年中我们团队内部启动了一个代号叫t3code的小项目,初衷很朴素:前端重复性工作太多。列表页、表单页、详情页,每个模块长得差不多,但每个模块都得从零开始写路由、写请求、写状态管理,一套流程走下来少说大半天。后来我们把它做成了一个内部工具链,现在团队新项目从初始化到上线,平均能省掉 40% 的前端工作量。

这篇文章不聊高大上的架构,也不讲"颠覆性"的技术创新,纯粹是把我从设计、开发到落地t3code这半年多踩过的坑、想通的道理、最后沉淀出的方案做一个完整复盘。如果你也在做内部工具、或者打算搭建一套自己的代码生成/脚手架体系,这里面有不少可以直接抄作业的思路。

1. 项目整体设计与思路拆解

1.1 为什么需要一个"不完整"的低代码平台

在动工之前,我们其实调研过市面上的低代码平台,包括某些商业产品。结论很一致:太重了。商业低代码平台要兼顾所有行业所有场景,带来的结果是配置项极其繁琐,光是一个按钮的权限控制就有五六个联动配置。而且一旦业务跑起来,想要把平台生成的代码改成自己团队的代码风格,几乎不可能。

t3code的定位从一开始就很明确:只做体系内的重复劳动,不做业务逻辑的兜底。换句话说,它不是一个运行时平台,而是一台"代码打印机"。你给它一份 JSON 配置,它帮你把带有团队规范的 CRUD 页面代码打印出来。打出来的代码直接进 Git 仓库,该走 Code Review 走 Code Review,该改的业务逻辑随便改,没有任何黑盒约束。

这种设计思路带来的最大好处是可审查性。生成出来的代码是明文的,出了问题可以翻源码排查,不需要跟平台方扯皮。对于中小型团队来说,这种可控性比"零代码"的噱头重要得多。

1.2 技术选型:为什么是 TypeScript + Vite + Node.js

技术选型阶段其实没有太多纠结。我们的核心诉求有三点:生成的代码必须带完整的类型定义;生成器本身要能在本地和 CI 环境快速运行;整个链路要方便团队二次维护。

TypeScript 几乎是唯一选择。生成出来的代码如果不带类型,那和复制粘贴没什么区别。我们把.d.ts的类型定义也做成模板的一部分,根据用户的 JSON 配置自动推导出对应的接口类型,这样从数据请求到页面渲染的类型流是通的。Vite 则负责生成后工程的构建,它的启动速度和 HMR 体验比 Webpack 高出几个量级,团队内部的接受度很高。

生成器本体用了 Node.js + TypeScript 直接跑,没有用 Babel 之类的东西。原因是生成器的逻辑本身并不复杂,主要就是模板渲染 + 文件写入。用纯 Node.js 反而容易维护,也方便后续在 GitLab CI 里直接挂一个t3code generate命令做自动化。

这里有一个经验值得分享:不要为了技术炫技而引入复杂度。t3code的目录结构我和核心维护者重构了三次,最后稳定下来的版本非常简单——一个src/templates目录放模板文件,一个src/commands目录放命令行逻辑,一个src/utils目录放文件处理工具。任何新成员半天时间就能看懂整个项目的全貌。

1.3 目录结构与核心模块划分

稳定后的t3code项目结构长这样:

t3code/ ├── package.json ├── tsconfig.json ├── src/ │ ├── cli.ts # 命令入口 │ ├── commands/ │ │ ├── create.ts # 初始化完整前端工程 │ │ ├── generate.ts # 根据 schema 生成页面代码 │ │ └── inspect.ts # 校验 schema 合法性 │ ├── core/ │ │ ├── schemaValidator.ts # JSON Schema 校验器 │ │ ├── renderer.ts # 模板渲染核心 │ │ └── fileWriter.ts # 文件输出与冲突处理 │ ├── templates/ │ │ ├── crud/ # 增删改查页面模板 │ │ ├── form/ # 独立表单页模板 │ │ └── component/ # 业务组件模板 │ └── utils/ │ ├── nameConverter.ts # 字段名转驼峰/下划线等 │ └── pathResolver.ts # 路径解析 └── docs/ └── schema.md # JSON 配置的完整说明文档

这个分法没什么花哨的,但把生成器的核心能力拆得很清楚:校验输入、渲染模板、落盘文件。inspect命令是我后来加上去的,专门用来在 CI 里校验前端提交的 schema 文件是否合法。

2. 核心细节解析与实操要点

2.1 schema 驱动的页面生成机制

t3code的核心是"schema 驱动"。我们定义了一套 JSON 格式,用来描述一个页面或模块长什么样。最简单的 schema 长这样:

{ "page": { "name": "userManage", "title": "用户管理", "route": "/system/user" }, "table": { "columns": [ { "key": "id", "title": "ID", "width": 80 }, { "key": "name", "title": "姓名", "searchable": true }, { "key": "email", "title": "邮箱" }, { "key": "status", "title": "状态", "type": "enum", "options": ["启用", "禁用"] } ], "operations": ["view", "edit", "delete"] }, "search": { "items": [ { "key": "keyword", "title": "关键词", "type": "input" }, { "key": "status", "title": "状态", "type": "select" } ] } }

生成器读到这份 JSON 后,会做几件事:先做字段合法性校验,比如key不能重复、route必须符合/xxx/xxx格式;然后把这些配置和模板文件进行拼接渲染;最后输出一个包含路由文件、.tsx页面组件、API 请求模块的完整目录。

这里最关键的设计点是模板引擎变量的命名规范。为了保持生成代码风格统一,我们约定:模板中的逻辑都用{% xxx %}包裹,变量插值用{{ xxx }}。这套语法是团队自己定的,简单到不需要额外学习成本,新成员看一遍就知道怎么改模板。

2.2 模板系统的设计:从复制粘贴到可组合

早期的t3code其实就是一个"复制粘贴加速器"——把上一个项目的代码拷过来,然后全局替换名字。后来发现这样做有两个问题:一是替换不干净,经常漏掉一两处硬编码的接口路径;二是不同页面的表格列数、操作按钮都不一样,死模板没法覆盖所有情况。

于是我们引入了可组合模板的概念。拿列表页举例,它被拆成三个片段:头部搜索区、表格主体区、分页与操作区。每个片段都可以单独用一个模板文件描述,渲染时根据 schema 里的字段决定哪些片段需要拼接进来。比如 schema 里如果没有配置search段,那么整个头部搜索区就不会出现在最终代码里。

这种做法带来的直接好处是:新页面的开发成本从"写一遍"降到了"拼一遍"。当一个业务模块的配置被复用三四次之后,模板本身的价值就会凸显出来——因为模板是被打磨过的,生成的代码质量通常比手写还要稳定。

2.3 CLI 交互设计要诀

t3code的命令行交互我前后调了三版。第一版是全交互问答式的,类似npm init,一步一步问你项目名、技术栈、要包含哪些模块。实测下来发现很烦人,尤其是团队里用脚本批量初始化项目的时候,交互式的 CLI 根本无法自动化。

第二版改成了全参数式:

t3code create my-app --template react-ts --router react-router --state zustand

一次性传参,适合自动化脚本调用。但问题是对新手不友好,参数记不住,动不动就报错。

最终版是两者结合:如果命令带参数,就直接用参数执行;如果不带参数,再进交互式。这个逻辑实现起来很简单,却同时满足了自动化跑批和手动创建两种场景,属于性价比很高的改进。

3. 实操过程与核心环节实现

3.1 三十秒完成工程初始化

使用t3code初始化一个新前端工程的过程,本质上就是执行一条命令:

npx t3code create user-center --template react-ts

这条命令内部做了什么?我把它拆一下:

  1. 从t3code的内置模板仓库拉取react-ts模板的基线代码;
  2. 读取模板根目录下的_t3code.config.json,找到所有需要动态替换的占位符;
  3. 用命令行传入的项目名user-center替换掉包名、路由前缀、标题等变量;
  4. 执行npm install,并输出一条提示告诉你生成的工程路径。

整个过程大概 30 到 40 秒。对比之前新成员入职配环境、拉模板、改配置要折腾两三个小时,这个提速是相当明显的。

实操心得:这里有个小细节值得提一下——user-center这种 kebab-case 的项目名,在生成package.json时是合法的包名,但在生成 React 组件的类名或 TS 接口名时是不合法的。所以工具里必须内置一个nameConverter,负责把 kebab-case 转成 PascalCase 和 camelCase。这个转换函数不复杂,但漏了它,生成出来的代码根本跑不起来。

3.2 从 schema 到可运行页面的全过程

下面我展示一次完整的页面生成过程,这是t3code最有价值的功能。假设我们后端接口已经定义好了,返回的数据结构是:

{ "code": 0, "data": { "list": [ { "id": 1, "name": "张三", "department": "技术部", "status": "active" } ], "total": 1 } }

我们把这份数据落成userManage.schema.json,然后执行:

t3code generate -s userManage.schema.json -o ./src/pages/user

生成器会输出如下目录结构:

src/pages/user/ ├── index.tsx # 页面主组件 ├── UserManageTable.tsx # 表格组件 ├── UserManageSearch.tsx # 搜索区组件 ├── api.ts # 请求封装 ├── types.ts # 类型定义 └── config.ts # 列配置等常量

api.ts的生成逻辑值得一提。它会根据 schema 里的page.name自动拼接出请求路径/system/user/list,同时把后端返回的data.list定义成UserItem[]类型。这样前端代码里到处都有类型提示,不用再对着接口文档手写interface。

Table 组件的列配置也会被直接生成进去。config.ts里是一个数组,数组每个元素对应 schema 里的一列,包含dataIndex、title、width这些属性。前端改动列的优先级高于后端字段的优先级,但如果 schema 里某个字段被删了,重新生成时会同步删掉配置里的对应项。

关键参数的计算逻辑:列宽width如果没有在 schema 里指定,生成器不会瞎编一个数字。它有一个默认分配规则——如果列数在 5 列以内,每列宽度按内容预设值(姓名 120、邮箱 200、状态 80 等)填充;如果列数超过 5 列,表头会自动启用scroll={{ x: 1200 }},避免页面出现横向挤压。这些经验值都是我们团队实际开发中总结出来的,比让开发者自己慢慢调要靠谱得多。

3.3 模板渲染引擎的边界处理

模板引擎用到了一个叫做ejs的库,它的语法和我们自定义的{{ }}标签非常接近,所以做了简单的适配。但真正有挑战的不是渲染本身,而是输出代码的格式化。

模板里写完的代码片段拼在一起之后,大概率是乱七八糟的。我们的解决办法是:文件落盘后自动过一遍prettier。也就是说,生成器拿到模板渲染结果后,会先调用prettier.format()对代码做格式化,再写入磁盘。这么做虽然多耗几百毫秒,但保证每次生成的代码行尾分号、缩进、引号风格都是一致的。

这一块给其他想造轮子的人一个忠告:如果不做格式化,生成代码的可读性会非常差,团队成员迟早会绕开工具手动改代码。格式化的钱不能省。

3.4 让模板适配不同后端规范

我们团队早期有两个后端服务,一个返回的字段是snake_case,另一个返回的是camelCase。前端页面如果直接把后端字段作为表格列,同一个页面的代码在对接不同服务时要写两套。这个问题不解决,模板永远没法通用。

t3code的解法是在 schema 的全局配置里加一个选项:

{ "api": { "responseKey": "data.list", "fieldCase": "camelCase" } }

当fieldCase为snake_case时,模板渲染会做一次字段名映射:后端返回的user_name自动转换成前端使用的userName。这个转换发生在请求拦截器层,生成出来的api.ts里会自动植入一个transformResponse函数,把后端字段"翻译"成前端类型定义里的字段。

这样做的收益非常大——团队成员不用关心后端到底用什么风格命名字段,前端统一的 camelCase 就是唯一标准。

4. 常见问题与排查技巧实录

4.1 模板渲染报错:变量名对不上

现象:执行t3code generate时,终端直接报错,提示某个变量未定义,但 schema 里明明写了这个字段。

原因:模板文件中引用的变量名和 schema 解析出来的变量名不一致。最常见的坑是大小写和复数形式。比如 schema 里定义的是searchItems,但模板里写的是{% for item in search_items %},渲染引擎找不到这个变量。

排查方法:在生成器里加一个--debug参数,输出渲染前引擎收到的完整变量对象。打印出来一眼就能看到有哪些 key,跟模板里的引用对一下就定位到问题。这个调试参数我认为是一个生成器必备的能力,没有它排查这类问题会非常痛苦。

4.2 schema 合法但产物不符合预期

现象:t3code generate正常结束,但生成的页面里表格列全部为空。

原因:经过排查发现,schema 里columns数组中有几列的key是重复的。我们的schemaValidator在写的时候只校验了必填字段,没有校验数组内元素的唯一性。表格渲染时用重复的 key 作为 React 组件的key属性,就会造成渲染错乱。

修复方法:在schemaValidator里加了一段规则——遍历columns,用Set检测key是否唯一,不唯一直接抛错并提示"第 3 个字段的 key 与第 1 个字段重复"。顺手把operations里的view、edit、delete这三个字符串也做了枚举校验,防止有人手误把edit写成eddit导致按钮不渲染。

4.3 生成的页面请求 404

现象:页面生成的完全正常,但一进页面就报 404,后端说没收到这个请求。

原因:t3code生成api.ts时,默认的请求 baseURL 是从环境变量VITE_API_BASE_URL读取的。如果本地的.env文件没有配置这个变量,或者配置的值和实际后端网关路径不一致,就会走到默认路径/api,404 自然难免。

排查方法:先看浏览器 Network 面板里请求的完整 URL,再检查.env里的配置。另外建议在生成器的初始化阶段直接生成一份.env.development和.env.production模板,把VITE_API_BASE_URL、VITE_APP_TITLE等常用变量全部预置好,新项目启动时只需要改值,不用凭空去猜该配什么。

4.4 重新生成代码覆盖了手写修改

现象:开发者手动改了生成的页面代码,然后重新跑t3code generate,手写部分全没了。

原因:这是"要不要允许生成器覆盖已有文件"的经典矛盾。我们第一版是直接覆盖,结果团队成员怨声载道;第二版改成只要目标文件存在就跳过生成,结果新增字段时页面没反应。

最终方案:采用冲突检测 + 差异化合并策略。生成器在写入前会对比目标文件的哈希值和当前磁盘文件的哈希值,如果不一样,就输出一列冲突清单,让开发者自己决定是覆盖还是保留。另外我们追加了一个--force参数,CI 自动化场景下可以直接覆盖。

这个设计确实需要多写一些代码,但它是工具能否在团队落地生根的分水岭。一个不尊重开发者已有产出的工具,就算再好用也会被抵制。

4.5 不同类型字段忘了传查询参数

现象:表格上方有"状态"筛选框,但切换状态后表格数据没有任何变化。

原因:搜索区生成的代码在onChange里只更新了搜索表单的 state,并没有触发表格数据请求。因为搜索表单的searchItems配置里,没有给select类型的字段标记submitOnChange: true。

解决办法:在 schema 的search.items中,将"状态"这一项的submitOnChange设置为true:

{ "key": "status", "title": "状态", "type": "select", "submitOnChange": true }

重新执行生成命令,模板就会在onChange回调里追加一个fetchList({ page: 1, status: value })的调用。这个参数如果作为默认行为——所有搜索字段变更后都自动提交——往往会让用户点每一下下拉框就发一次请求,接口压力太大。所以设计上做了保守处理,只有明确标记的字段才触发自动搜索。

5. 工具如何融入团队协作流程

5.1 和 GitLab CI 的集成

工具做出来只是第一步,让团队真正用它才是关键。我们在 GitLab CI 里加了一个 Job,专门处理 schema 变更:

  • 每当 MR 中涉及.schema.json文件的变更;
  • CI 自动运行t3code generate -s xxx.schema.json -o ./src/pages/xxx;
  • 如果生成后的代码与 MR 中的内容不一致,CI 直接失败,并提示"请执行 t3code generate 后提交生成产物"。

这一个环节解决了很多协作问题。以前前端改 schema 后忘更新页面代码,导致页面和后端字段不一致的问题时有发生。现在机器帮团队记着这件事,谁想偷懒都不可能。

5.2 我眼中 t3code 的边界在哪里

用了大半年之后,我越来越清楚t3code的能力边界在哪里。它能加速,但不能替代思考。

复杂的状态流转、精细的权限控制、跨模块的数据共享——这些依然是前端工程师的核心产出,模板没法覆盖。假如哪天业务抽象出来了新的重复模式,比如多种不同类型的图表页、导流落地页,我们会评估后再沉淀进t3code的模板库里。

最后说一点个人体会。开发t3code的过程中,最让我意外的不是技术难点的攻克,而是团队文化的改变——以前大家各写各的,代码风格五花八门;现在因为模板统一,所有人都默认采用同一套状态管理方案、同一个组件库约定。这种"看不见的一致性",对代码质量的提升比任何制度规定都好用。工具的价值最终不在于省了多少工时,而在于它让团队在技术决策上更省心。

返回列表