1. 从“t3code”这个名字说起:它到底是什么
第一次看到“t3code”这个词,很多人会下意识地把它当成某个开源库、某个命令行工具,或者某个小众框架的缩写。我在几个技术群里也见过类似的讨论,有人猜是“TypeScript 3 Code”的简写,有人觉得是某个代码生成器的代号,还有人把它和某些低代码平台联系在一起。实际上,如果你去翻一翻近两年的开发者社区讨论,会发现“t3code”更多时候是以一种“项目代号”或者“个人工具集”的身份出现的——它不是一个官方标准,也没有一个统一的定义,而是被不同的人用来指代各自那套“围绕代码生成、代码转换、代码模板”的小型工程实践。
我最早接触这个词,是在一个前端工程化的内部分享里。当时一位做中后台系统的朋友提到,他们团队内部把一套“从接口定义自动生成前端请求代码和类型声明”的脚本集合叫做 t3code。后来我又陆续看到几种不同的用法:有人用它指代“把设计稿转成代码”的尝试,有人用它命名自己写的“多语言代码片段管理工具”,还有人干脆把它当成一个“代码模板仓库”的代号。这种模糊性其实很有意思,它说明“t3code”背后真正吸引人的,不是某个具体产品,而是一个共性的需求:如何让代码的生成、转换和复用变得更自动、更可控、更贴合团队自己的习惯。
所以这篇文章,我不打算去考证“t3code”的官方定义,而是把它当作一个“引子”,来聊一聊这类以“代码”为核心对象的轻量级工程实践。它适合那些已经写过一些脚本、做过一些模板、但总觉得不够系统的人;也适合刚入行不久、想了解“代码生成”到底能解决什么问题的朋友。我会从整体设计思路、核心细节、实操过程、常见问题几个角度展开,尽量把我在实际项目里踩过的坑和总结出来的经验都写进去。你不需要有很深的编译原理背景,只要写过业务代码,就能看懂。
2. 整体设计与思路拆解:为什么是“生成”而不是“手写”
2.1 核心需求:重复代码的边际成本太高
任何一个稍微大一点的项目,都会出现大量结构相似的代码。比如前端要写几十个接口请求函数,每个函数的差别只是 URL、请求方法和参数类型;后端要写几十个 CRUD 接口,每个接口的差别只是实体名和字段;甚至写文档、写测试用例,也有大量重复的骨架。这些代码单看每一段都不难,但加起来就会消耗大量时间,而且容易出错——复制粘贴的时候漏改一个字段名,或者参数类型写错,都是很常见的事。
t3code 这类实践的核心目标,就是把这些“有规律但重复”的代码,从“手写”变成“生成”。这里的“生成”不一定是复杂的代码生成器,也可以是一个简单的脚本、一个模板文件、甚至是一组编辑器 snippet。关键在于:把变化的量抽出来,把不变的结构固定下来。这样你只需要维护一份“源头定义”,就能批量产出符合规范的代码。
我见过很多团队一开始是靠“复制粘贴 + 全局替换”来应付的,短期看确实快,但一旦源头定义变了,比如接口字段调整了,你就得把所有生成过的代码再改一遍。而如果一开始就用生成的方式,改源头、重新生成,几分钟就能同步所有地方。这个账其实很好算:假设你有 50 个接口,每次改动平均涉及 3 个文件,手写同步一次大概要半小时,一周改两次就是 1 小时;而写一个生成脚本可能只要 2 小时,之后每次同步只要几秒钟。只要项目周期超过一个月,生成方案就明显更划算。
2.2 方案选型:模板驱动 vs AST 驱动
在具体实现上,t3code 这类实践通常有两条路线:模板驱动和AST 驱动。模板驱动就是准备一个代码模板文件,里面用占位符表示可变部分,然后用数据去填充占位符,生成最终代码。AST 驱动则是先解析现有代码,构建抽象语法树,再对树进行修改或生成,最后输出代码。两者各有优劣,选择哪个取决于你的具体场景。
模板驱动的优点是上手快、门槛低。你不需要懂编译器原理,只要会写字符串替换就行。比如一个简单的请求函数模板:
export function ${functionName}(${params}) { return request({ url: '${url}', method: '${method}', data: ${data} }); }然后用一个 JSON 文件描述每个接口的 functionName、url、method、params,循环渲染就能生成所有请求函数。这种方式的缺点是灵活性有限,如果生成逻辑很复杂,模板里会塞满条件判断,可读性会变差。而且模板生成的代码格式往往不够理想,需要额外做格式化。
AST 驱动的优点是精确、灵活。你可以精确控制每个节点的生成,生成的代码格式也更好。但缺点是门槛高,需要熟悉解析器(比如 Babel、TypeScript Compiler API、Tree-sitter 等),调试也更麻烦。我个人的经验是:如果只是生成新代码,模板驱动足够;如果需要修改现有代码,或者生成逻辑涉及复杂的类型推导,AST 驱动更合适。很多团队会混合使用:用 AST 解析接口定义,提取出结构化数据,再用模板生成代码。这样既利用了 AST 的精确性,又保持了模板的简单性。
2.3 数据来源:接口定义、数据库 Schema 还是设计稿
生成代码的前提是有“源头数据”。t3code 类项目常见的数据来源有三种:接口定义文件(如 OpenAPI/Swagger、GraphQL Schema、Proto 文件)、数据库 Schema(如 SQL 建表语句、ORM 模型定义)、设计稿(如 Figma、Sketch 的 JSON 导出)。不同的数据来源,对应的生成目标和难度也不同。
接口定义是最常见的来源。OpenAPI 和 GraphQL 都有结构化的描述文件,解析起来相对容易。你可以从这些文件里提取出路径、方法、参数、响应类型,然后生成前端请求代码、后端 Controller 骨架、甚至接口文档。数据库 Schema 则适合生成后端 CRUD 代码、实体类、迁移脚本。设计稿转代码难度最大,因为设计稿里的信息往往不够精确,需要大量人工干预,目前更多是辅助而不是全自动。
我建议刚开始做的时候,优先选择结构化程度最高的数据源。比如你们团队已经在用 OpenAPI 描述接口,那就从它入手,不要一上来就挑战设计稿转代码。先把一个场景跑通,再逐步扩展。另外,数据源的质量很关键:如果接口定义本身就不完整、不规范,生成出来的代码也会有问题。所以在生成之前,最好先做一轮数据校验,把缺失的字段、不一致的类型都处理掉。
2.4 输出目标:代码、类型、文档还是测试
t3code 类实践的输出目标也很多样。最常见的是生成业务代码,比如请求函数、Controller、Service。其次是生成类型声明,比如 TypeScript 的 interface、type,或者后端的 DTO。还有生成文档的,比如把接口定义转成 Markdown 或 HTML 文档。生成测试用例的也有,比如根据接口定义生成基础的单元测试骨架。
我的建议是:不要贪多,先聚焦一个输出目标。很多团队一开始想“既然都解析了接口定义,那就把代码、类型、文档、测试全生成了吧”,结果每个都做了一点,每个都不够好用。更好的做法是先选一个最痛的点,比如“前端请求函数手写太麻烦”,就只生成请求函数和对应的类型声明。等这个流程稳定了,再考虑扩展到文档和测试。这样每一步都有明确的收益,也更容易获得团队认可。
3. 核心细节解析与实操要点:从数据到代码的关键环节
3.1 数据解析:把非结构化变成结构化
不管数据源是什么格式,第一步都是把它解析成程序能处理的结构化数据。以 OpenAPI 为例,你可以用现成的解析库,比如swagger-parser、openapi-types,也可以自己写一个简单的解析器。解析的目标是提取出每个接口的:路径、HTTP 方法、请求参数(路径参数、查询参数、请求体)、响应类型、接口描述。这些信息会作为后续生成代码的输入。
这里有个细节很容易被忽略:参数的类型映射。OpenAPI 里的类型是 JSON Schema 类型,比如string、integer、boolean、array、object,而你要生成的代码可能是 TypeScript、Java、Python,需要做类型转换。比如 OpenAPI 的integer在 TypeScript 里是number,在 Java 里可能是int或long,在 Python 里是int。如果类型映射做错了,生成的代码就会报错。我建议把类型映射单独抽成一个配置文件,方便调整和扩展。
另一个细节是命名规范。接口定义里的字段名可能是下划线风格(user_name),而生成的代码可能需要驼峰风格(userName)。你需要一个命名转换函数,把不同风格的名称统一成目标语言的习惯。这个函数看起来简单,但实际写起来要考虑很多边界情况,比如连续下划线、数字开头、保留字冲突等。我一般会准备一个toCamelCase、toPascalCase、toSnakeCase的工具函数,在生成前统一处理。
3.2 模板设计:让生成的代码像人写的
模板是生成代码的“模具”,它的质量直接决定了生成代码的可读性。一个好的模板应该满足几个条件:结构清晰、占位符明确、易于维护。我见过一些模板,里面塞满了if-else和循环,读起来比生成的代码还费劲。这种模板虽然能工作,但维护成本很高,一旦需求变化,改起来很痛苦。
我的经验是:把复杂的逻辑从模板里抽出来,放到数据预处理阶段。比如,如果某个接口需要特殊处理,不要直接在模板里写if (interfaceName === 'xxx'),而是在解析数据的时候就把这个接口标记出来,生成时用统一的方式处理。这样模板里只有简单的占位符替换,逻辑都集中在数据层,更容易测试和调试。
另外,模板的格式也很重要。生成的代码最好能直接通过项目的 lint 检查,不需要手动格式化。你可以在生成之后调用 Prettier、ESLint 或 gofmt 等工具做一次格式化。这样生成的代码就能和手写代码保持一致,减少 review 时的摩擦。我一般会在生成脚本的最后加一步prettier --write,确保输出的代码风格统一。
3.3 生成策略:全量生成还是增量生成
生成代码的时候,有一个策略选择:全量生成还是增量生成。全量生成就是每次根据数据源重新生成所有文件,覆盖旧文件。增量生成则是只生成有变化的文件,保留手动修改过的文件。两者各有适用场景。
全量生成的优点是简单、一致,不会出现“生成代码和源头定义不一致”的情况。缺点是如果生成的文件被手动修改过,重新生成会覆盖掉这些修改。所以全量生成适合那些“完全由生成器控制”的文件,比如纯请求函数、纯类型声明。增量生成则适合那些“生成后还需要手动补充”的文件,比如 Controller 骨架,生成后可能还要加业务逻辑。增量生成需要记录生成状态,实现起来更复杂,但能避免覆盖人工修改。
我一般会采用混合策略:对于纯生成的代码,用全量生成,并在文件头加一个注释“此文件由 t3code 自动生成,请勿手动修改”;对于需要人工补充的代码,用增量生成,只在文件不存在时生成,或者用特殊的标记区分生成区域和手动区域。比如:
// <t3code-generated> export function getUser() { ... } // </t3code-generated> // 以下为手动补充代码这样重新生成时,只替换标记之间的内容,手动代码不受影响。这个技巧在实际项目中非常实用,可以大大减少“生成覆盖手动修改”的烦恼。
3.4 版本管理:生成代码要不要提交到仓库
这是一个经常被讨论的问题:生成的代码要不要提交到 Git 仓库?我的观点是:看情况,但大多数时候建议提交。不提交的理由是“生成代码是衍生物,不应该进版本库”,听起来很合理,但实际执行起来会有问题。比如新同事拉下代码后,需要先跑一遍生成脚本才能启动项目,增加了上手成本;CI 环境也需要额外配置生成步骤;如果生成脚本依赖的数据源在另一个仓库,还可能因为权限问题拉不到。
提交生成代码的好处是:开箱即用,减少环境依赖。新同事 clone 下来就能跑,CI 也不需要特殊配置。缺点是每次修改数据源后,都要重新生成并提交,仓库里会有一些“看起来像手写但其实是生成”的代码。为了减少混淆,我建议在生成的文件头加注释说明,并且在 README 里写清楚哪些目录是生成的、如何重新生成。这样既方便了日常开发,也保留了可追溯性。
如果你们团队坚持不提交生成代码,那至少要保证生成脚本足够稳定、足够快,并且有明确的文档说明。另外,可以在 CI 里加一个检查:如果生成代码和源头定义不一致,就报错提醒。这样能避免“有人改了源头但忘了重新生成”的情况。
4. 实操过程与核心环节实现:一个可复现的 t3code 小项目
4.1 环境准备与依赖安装
下面我以一个具体的例子来演示 t3code 类项目的完整实现。假设我们有一个 OpenAPI 描述文件api.yaml,目标是生成 TypeScript 的请求函数和类型声明。这个例子足够简单,你可以跟着一步步做,也可以根据自己的需求调整。
首先准备环境。你需要 Node.js(建议 18 以上)和 npm。然后创建一个新目录,初始化项目:
mkdir t3code-demo && cd t3code-demo npm init -y npm install js-yaml swagger-parser prettier --save-dev这里用了三个依赖:js-yaml用来解析 YAML 格式的 OpenAPI 文件,swagger-parser用来校验和解析 OpenAPI 结构,prettier用来格式化生成的代码。如果你用的是 JSON 格式的 OpenAPI,可以不用js-yaml。swagger-parser的好处是它会帮你处理$ref引用,把嵌套的定义展开,省去很多手动处理的麻烦。
接下来准备一个简单的api.yaml:
openapi: 3.0.0 info: title: Demo API version: 1.0.0 paths: /users: get: operationId: getUsers summary: 获取用户列表 parameters: - name: page in: query schema: type: integer responses: '200': description: 成功 content: application/json: schema: type: array items: $ref: '#/components/schemas/User' /users/{id}: get: operationId: getUserById summary: 获取单个用户 parameters: - name: id in: path required: true schema: type: string responses: '200': description: 成功 content: application/json: schema: $ref: '#/components/schemas/User' components: schemas: User: type: object properties: id: type: string name: type: string age: type: integer这个文件描述了两个接口:获取用户列表和获取单个用户。每个接口有 operationId、参数和响应类型。我们的目标是根据这个文件生成对应的 TypeScript 代码。
4.2 解析 OpenAPI 并提取关键信息
接下来写解析脚本。创建一个generate.js文件:
const SwaggerParser = require('swagger-parser'); const fs = require('fs'); const path = require('path'); async function parseApi(filePath) { const api = await SwaggerParser.dereference(filePath); const operations = []; for (const [route, methods] of Object.entries(api.paths)) { for (const [method, operation] of Object.entries(methods)) { if (method === 'parameters') continue; operations.push({ operationId: operation.operationId, method: method.toUpperCase(), route, summary: operation.summary || '', parameters: operation.parameters || [], responseSchema: extractResponseSchema(operation), }); } } return { operations, schemas: api.components?.schemas || {} }; } function extractResponseSchema(operation) { const successResponse = operation.responses?.['200'] || operation.responses?.['201']; if (!successResponse) return null; const content = successResponse.content?.['application/json']; return content?.schema || null; }这段代码做了几件事:用SwaggerParser.dereference解析并展开引用,遍历所有路径和方法,提取出 operationId、HTTP 方法、路由、参数、响应 schema。dereference会把$ref替换成实际的定义,这样后面处理起来就不用再关心引用了。
这里有个细节:operation.responses里的状态码可能是字符串'200',也可能是数字200,不同版本的 OpenAPI 写法不一样。我一般会同时检查'200'和200,或者用Object.keys遍历找到第一个 2xx 的响应。另外,有些接口可能返回 204(无内容),这时候没有 content,需要特殊处理。
4.3 生成 TypeScript 类型声明
有了结构化的数据,接下来生成类型声明。我们先处理components.schemas里的每个 schema,把它转成 TypeScript 的 interface。写一个generateTypes函数:
function mapType(schema) { if (!schema) return 'any'; if (schema.type === 'string') return 'string'; if (schema.type === 'integer' || schema.type === 'number') return 'number'; if (schema.type === 'boolean') return 'boolean'; if (schema.type === 'array') return `${mapType(schema.items)}[]`; if (schema.$ref) { const name = schema.$ref.split('/').pop(); return name; } if (schema.type === 'object' && schema.properties) { const props = Object.entries(schema.properties) .map(([key, value]) => ` ${key}: ${mapType(value)};`) .join('\n'); return `{\n${props}\n}`; } return 'any'; } function generateTypes(schemas) { const lines = []; for (const [name, schema] of Object.entries(schemas)) { lines.push(`export interface ${name} {`); for (const [prop, propSchema] of Object.entries(schema.properties || {})) { lines.push(` ${prop}: ${mapType(propSchema)};`); } lines.push('}'); lines.push(''); } return lines.join('\n'); }这个mapType函数处理了基本类型、数组、对象和引用。实际项目中,你可能还需要处理enum、oneOf、allOf、nullable等情况。我建议一开始只支持最常见的几种,遇到不支持的就在生成日志里打警告,后续再逐步补充。不要试图一次性覆盖所有 OpenAPI 特性,那样会陷入无穷无尽的边界情况。
4.4 生成请求函数
类型声明生成之后,接着生成请求函数。每个 operation 对应一个函数,函数名用 operationId,参数根据 parameters 生成,返回值类型根据 responseSchema 生成。写一个generateRequests函数:
function generateRequests(operations) { const lines = []; lines.push("import request from '@/utils/request';"); lines.push(''); for (const op of operations) { const params = op.parameters.map((p) => { const type = mapType(p.schema); const optional = p.required ? '' : '?'; return `${p.name}${optional}: ${type}`; }); const returnType = op.responseSchema ? mapType(op.responseSchema) : 'void'; const paramStr = params.length ? `params: { ${params.join('; ')} }` : ''; lines.push(`export function ${op.operationId}(${paramStr}): Promise<${returnType}> {`); lines.push(` return request({`); lines.push(` url: \`${op.route.replace(/{(\w+)}/g, '${$1}')}\`,`); lines.push(` method: '${op.method}',`); if (params.length) { lines.push(` params,`); } lines.push(` });`); lines.push('}'); lines.push(''); } return lines.join('\n'); }这里有几个细节值得说明。第一,路由里的路径参数{id}需要转成模板字符串${id},我用了一个正则替换。第二,参数统一放在一个params对象里,这样调用时更清晰。第三,返回值类型用Promise<ReturnType>,假设request返回的是 Promise。实际项目中,你可能需要根据不同的请求方法决定参数放在params还是data里,这里为了简化统一用了params。
4.5 整合与格式化输出
最后把类型声明和请求函数整合起来,写入文件,并用 Prettier 格式化:
async function main() { const { operations, schemas } = await parseApi('./api.yaml'); const typesContent = generateTypes(schemas); const requestsContent = generateRequests(operations); const outputDir = path.join(__dirname, 'src', 'api'); fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync(path.join(outputDir, 'types.ts'), typesContent); fs.writeFileSync(path.join(outputDir, 'requests.ts'), requestsContent); console.log(`Generated ${operations.length} requests and ${Object.keys(schemas).length} types.`); } main().catch((err) => { console.error(err); process.exit(1); });运行node generate.js,你会在src/api目录下看到types.ts和requests.ts。生成的types.ts大概是这样:
export interface User { id: string; name: string; age: number; }requests.ts大概是这样:
import request from '@/utils/request'; export function getUsers(params: { page?: number }): Promise<User[]> { return request({ url: `/users`, method: 'GET', params, }); } export function getUserById(params: { id: string }): Promise<User> { return request({ url: `/users/${id}`, method: 'GET', params, }); }到这里,一个最基础的 t3code 流程就跑通了。你可以把它扩展成支持更多特性,比如生成 React Query 的 hooks、生成 Mock 数据、生成接口文档等。关键是把核心流程跑通,然后再逐步迭代。
5. 常见问题与排查技巧实录:那些文档里不会写的事
5.1 生成代码和手写代码冲突怎么办
这是最常见的问题。你生成了一个文件,然后手动改了几行,下次重新生成时改动被覆盖了。解决思路有三种:一是把生成和手写分离,生成的文件只包含纯生成内容,手写内容放到另一个文件里,通过继承或组合的方式使用。比如生成的UserServiceBase只包含 CRUD 方法,手写的UserService继承它并添加业务逻辑。二是用标记区分生成区域和手动区域,重新生成时只替换标记之间的内容。三是用增量生成,只在文件不存在时生成,已存在的文件跳过。我一般推荐第一种,因为它最清晰,不会出现“生成代码和手写代码混在一起”的情况。
5.2 类型映射出错怎么排查
类型映射是生成代码时最容易出错的地方。常见的问题包括:OpenAPI 的integer被映射成了string,array没有正确处理items,$ref没有解析导致生成了any。排查的时候,我建议先把解析出来的结构化数据打印出来,看看每个字段的类型是什么。如果类型不对,再检查mapType函数的逻辑。另外,swagger-parser的dereference会把$ref展开,但有时候展开后的结构和你预期的不一样,需要仔细看文档。我一般会在生成脚本里加一个--debug参数,开启后打印详细的解析日志,方便定位问题。
5.3 生成的代码格式混乱怎么办
模板生成的代码往往格式不理想,比如缩进不一致、换行位置奇怪。最省事的办法是生成之后调用 Prettier 或 ESLint 格式化。你可以在生成脚本的最后加一步:
const prettier = require('prettier'); const formatted = prettier.format(content, { parser: 'typescript' }); fs.writeFileSync(filePath, formatted);如果项目有自己的 Prettier 配置,Prettier 会自动读取.prettierrc,保持和手写代码一致的风格。这样生成的代码就能直接通过 lint 检查,减少 review 时的摩擦。另外,模板本身也要注意格式,尽量让生成的代码在格式化之前就接近最终形态,这样即使格式化工具出问题,代码也不会太难看。
5.4 数据源更新后如何同步
数据源更新后,你需要重新运行生成脚本。如果生成代码提交到了仓库,记得把重新生成的文件也提交上去。为了避免“有人改了数据源但忘了重新生成”,可以在 CI 里加一个检查:运行生成脚本,然后检查git diff是否有变化,如果有就报错。这样能强制大家在修改数据源后重新生成。另外,如果数据源在另一个仓库,可以考虑用 Git Submodule 或者定时任务来同步,确保生成脚本总是基于最新的数据源。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 生成的类型是 any | $ref 未解析或类型映射缺失 | 打印解析后的 schema | 检查 dereference 是否生效,补充 mapType 分支 |
| 路径参数未替换 | 正则匹配失败 | 检查路由字符串格式 | 调整正则,支持{id}和:id两种风格 |
| 生成代码格式混乱 | 模板缩进不一致 | 对比模板和输出 | 生成后调用 Prettier 格式化 |
| 重新生成覆盖手动修改 | 全量生成策略 | 检查文件是否有手动改动 | 改用增量生成或标记区域替换 |
| 参数可选性错误 | required 字段未正确处理 | 检查 OpenAPI 的 required 数组 | 根据 required 决定是否加? |
| 枚举类型生成错误 | enum 未处理 | 检查 schema 是否有 enum | 生成 TypeScript 的 union type 或 enum |
6. 扩展思路:t3code 还能怎么玩
6.1 生成 React Query Hooks
如果你在用 React Query,可以在生成请求函数的基础上,进一步生成对应的 hooks。比如:
export function useGetUsers(params: { page?: number }) { return useQuery(['getUsers', params], () => getUsers(params)); }这样组件里直接调用useGetUsers就行,不用再手动写 queryKey 和 queryFn。生成 hooks 的关键是确定 queryKey 的规则,我一般用[operationId, params]作为 key,这样参数变化时能自动重新请求。另外,对于 mutation 类型的接口,可以生成useMutation的封装,把 invalidateQueries 的逻辑也一并生成。
6.2 生成 Mock 数据
有了类型声明和接口定义,还可以生成 Mock 数据。根据 schema 的类型,随机生成符合结构的假数据。比如string类型生成随机字符串,integer生成随机数字,array生成指定长度的数组。这样前端在后端接口还没 ready 的时候就能先联调。生成 Mock 数据的关键是处理好嵌套结构和引用,避免无限递归。我一般会设置一个最大深度,超过深度就返回空对象或 null。
6.3 生成接口文档
把 OpenAPI 定义转成 Markdown 或 HTML 文档,也是 t3code 类实践的常见扩展。你可以用模板生成每个接口的说明,包括路径、方法、参数、响应示例。这样文档和代码同源,不会出现“代码改了文档没改”的情况。如果团队用 Confluence 或语雀,还可以把生成的 Markdown 直接推送到对应平台。我见过一些团队把文档生成集成到 CI 里,每次合并代码后自动更新文档,效果很好。
6.4 多语言支持
如果你的项目涉及多种语言,比如前端用 TypeScript,后端用 Java,可以基于同一份 OpenAPI 定义,生成不同语言的代码。这时候类型映射和模板都需要按语言区分。我建议把语言相关的部分抽成独立的模块,比如generators/typescript.js、generators/java.js,每个模块负责自己的类型映射和模板。这样新增语言时只需要加一个模块,不用改动核心解析逻辑。
7. 我个人的一些实操体会
做这类代码生成项目,最大的体会是:不要追求一步到位,而是小步快跑。我见过一些团队一开始就想做一个“万能代码生成平台”,支持所有数据源、所有输出目标、所有语言,结果做了半年还没上线。更好的做法是选一个最痛的点,用最简单的方案先跑通,让团队看到收益,然后再逐步扩展。比如先只生成前端请求函数,等大家用习惯了,再考虑生成类型、文档、测试。
另一个体会是:生成代码的质量比生成速度更重要。如果生成的代码格式混乱、类型错误,大家用一次就不想再用了。所以在早期,宁可少生成一些,也要保证生成的代码能直接用。格式化、lint 检查、类型校验这些步骤不能省。我一般会在生成脚本里加一个“自检”环节,生成之后跑一遍 TypeScript 编译,如果有类型错误就报错,避免把问题代码提交到仓库。
最后,文档和沟通很关键。生成脚本是谁维护的、怎么运行、数据源在哪里、生成的文件能不能手动改,这些问题都要在 README 里写清楚。否则过几个月,连你自己都忘了当初是怎么设计的。我习惯在生成脚本的头部写一段注释,说明用途、用法和注意事项,这样即使换了人维护,也能快速上手。
这个内容后续还可以这样扩展:把生成脚本打包成一个 CLI 工具,支持配置文件,让其他项目也能复用;或者集成到编辑器的保存钩子里,保存 OpenAPI 文件时自动重新生成代码。这些方向都值得尝试,但前提是先把核心流程跑稳。