前言
团队内推行TS
的一大阻力是api
接口的类型定义,十来个有效字段倒还好说,但如果是几十个有效字段呢?传统手艺是等接口通了,根据接口返回的数据按自己所需字段一个一个定义
但现在已经2025年了,聪明的你可能会采取以下手段:
- 对接口文档进行截图,贴给
AI
,让其产出TS
类型 - 将接口返回数据复制下来,贴到在线转换工具将数据转成
TS
类型 - 借助
apifox
等工具来管理接口,产出TS
类型
目前主流的开发模式是前后端分离,所以前后端是并行开发的,接口文档出来之后并不代表api
接口就有数据,这时候前端通常会通过各种手段生成mock
数据:
- 手动定义一份符合数据结构的
mock
数据 - 借助
mockjs
等第三方库来生成mock
数据 - 编写一个浏览器插件,拦截接口,返回
mock
数据 - 公司接口服务有提供
mock
接口
上面提到的各种手段的问题点:
- 功能点单一独立,无整合手段,使用起来有割裂感
- 无逻辑抽象,功能无法在多种应用场景下复用,如
CLI
、IDE
插件等功能应用场景 - 缺少统一代码规范手段,如何降低遵循规范的心智成本是我们推行规范的关键点之一,毕竟规范做得再好,遵循成本高,推行难度就大
- 不够灵活,无法根据实际业务场景进行自定义规则
- 不够高效,依然有一定的人工成本
针对这些问题,我(点子王)有个点子🙋♂️
正文
我们先思考一个问题:api
接口的类型和mock
有什么关系?
答案是:它们的数据源都来自接口平台 ,同一数据源可以通过node
脚本快速生成代码
功能载体
快速生成文件的交互,有两个方向:CLI
、vscode
插件,对比下这两个方案的优劣势
vscode
插件
优势:
- 使用灵活,随意想在哪个目录生成,直接右键操作即可
劣势:
- 开发成本高,需要有
vscode
插件开发知识储备 - 维护成本高,目前公司只有我写过
vscode
插件,没有backup
开发人员 - 插件需要发布到开放市场,存在安全性问题
CLI
优势:
- 接入
CLI
插件体系,node
脚本编写,支持技术共建 - 有使用过
UMI
或nestjs
命令的狂喜,相同的交互体验 - 内部发包,无安全性问题
劣势:
- 对目录结构要求较高,非常规目录无法使用
- 对于习惯使用独立终端运行命令的开发同学,使用体验上会有割裂感
综合来看,CLI
方案更加合适,虽然它对目录结构要求比较高,但我们工程化所推行的应用框架是Umi
和Taro
,目录结构还是比较稳定的,没多少影响
方案设计
通过设计图不难发现,其实功能并不复杂,简称有手就行。其中比较关键的几个点:获取配置文件的配置内容、中间件、数据转换的核心逻辑
转译配置文件
配置文件类似于Umi
和Taro
,由核心层提供一个支持类型提示的defineConfig
函数,第一版协议还不够开放,仅做参考
配置文件必然是ts
文件了,node23
以下版本是不支持动态引入ts
文件的,所以需要将ts
文件转成cjs
,再获取其导出的内容
转译工具太多了,我这里使用的是esbuild
,因为转译后内容是string
类型,所以再通过require-from-string
这个库读取导出的内容
说明一下配置文件为什么一定要是ts
文件,根本原因是类型提示,我已经回想不起来对接第三方库没有ts
类型提示的日子是什么样的了(忆往昔)
数据中间件
由于公司内部的接口平台有两个,所以我们需要有两个中间件将两个平台的接口数据转成核心逻辑所需要的数据结构。统一输入数据格式,数据转换作为抽象逻辑就变得稳定可复用,后续哪怕更换接口平台,也只需增加对应的中间件即可
数据转换核心逻辑
这部分功能我当时是让AI
帮我实现的,对于逻辑比较复杂的单一功能,比较建议使用AI
来提高效率
输出方案有两种:ATS
、string
,ATS
的好处在于可以精准的控制文件内容,开发时间有限则可以使用string
,我这里采用的就是string
,直接fs.writeFileSync
一把梭
这里贴一下比较关键的几处代码,仅供参考
typescript
// mock.js
/**
* 生成mock数据模板
*/
function generateMockTemplate(schema: IFieldSchema): any {
if (isObjectType(schema)) {
if (isEmptyObject(schema)) {
return {};
}
const mockObj: any = {};
schema.child?.forEach((field) => {
// 递归处理所有字段,不论类型
const mockValue = generateMockTemplate(field);
if (isArrayType(field)) {
if (isEmptyArray(field)) {
mockObj[field.name] = [];
} else {
// 对于数组类型,使用正确的 mockjs 语法
const arrayKey = `${field.name}|1-${field.initialValue || 10}`;
mockObj[arrayKey] = [mockValue];
}
} else {
mockObj[field.name] = mockValue;
}
});
return mockObj;
}
if (isArrayType(schema)) {
// 递归处理数组项
return generateMockTemplate(schema.child![0]);
}
if (![undefined, null, ''].includes(schema.initialValue)) {
return schema.initialValue;
}
// 处理基础类型
const mockTemplate = getMockTemplate(schema.name, schema.type);
if (mockTemplate) {
return mockTemplate;
}
return getDefaultValue(schema.type);
}
typescript
// typescript.ts
/**
* 生成子项类型定义
*/
export interface IGenerateTypeItemsDefinitionProps
extends Pick<IGenerateTypeDefinitionProps, 'typeDefinitions' | 'parentFieldName'> {
schema: IFieldSchema[];
}
const generateTypeItemsDefinition = ({
schema,
typeDefinitions,
parentFieldName,
}: IGenerateTypeItemsDefinitionProps) => {
return schema
.map((field) => {
// 递归生成子类型
generateTypeDefinition({
schema: field,
typeDefinitions,
parentFieldName,
fieldName: field.name,
});
const optional = !field.required ? '?' : '';
const tsType = getTypeReference(field, parentFieldName, field.name);
const comment = field.remark ? ` // ${field.remark}` : '';
return ` ${field.name}${optional}: ${tsType};${comment}\n`;
})
.join('');
};
/**
* 生成类型定义并存储到Map中
*/
export interface IGenerateTypeDefinitionProps {
schema: IFieldSchema;
typeDefinitions: Map<string, string>;
parentFieldName: string;
fieldName: string;
}
function generateTypeDefinition({ schema, typeDefinitions, parentFieldName, fieldName }: IGenerateTypeDefinitionProps) {
const typeName = getTypeName(parentFieldName, fieldName);
if (isObjectType(schema) && !isEmptyObject(schema)) {
if (!typeDefinitions.has(typeName)) {
let definition = `export type ${typeName} = {\n`;
definition += generateTypeItemsDefinition({
schema: schema.child!,
typeDefinitions,
parentFieldName: fieldName || schema.name || '',
});
definition += '}';
typeDefinitions.set(typeName, definition);
}
// return typeName;
}
if (isArrayType(schema) && !isEmptyArray(schema)) {
const itemSchema = schema.child![0].child!;
const arrayItemTypeName = typeName + 'Items';
// 先生成数组项的类型
if (!typeDefinitions.has(arrayItemTypeName)) {
let definition = `export type ${arrayItemTypeName} = {\n`;
definition += generateTypeItemsDefinition({
schema: itemSchema,
typeDefinitions,
parentFieldName: fieldName || schema.name || '',
});
definition += '}';
typeDefinitions.set(arrayItemTypeName, definition);
}
// return `${arrayItemTypeName}[]`;
}
// return typeMap[schema.type];
}
/**
* 获取类型引用
*/
function getTypeReference(schema: IFieldSchema, parentFieldName = '', fieldName = ''): string {
const typeName = getTypeName(parentFieldName, fieldName);
if (isObjectType(schema)) {
return isEmptyObject(schema) ? typeMap[schema.type] : typeName;
}
if (isArrayType(schema)) {
return isEmptyArray(schema) ? typeMap[schema.type] : `${typeName}Items[]`;
}
return typeMap[schema.type];
}
功能澄清
在推行过程中,有部分开发以为使用了该工具就有mock
能力了,主要是他混淆了mock
数据和mock
能力的概念
mock
能力:是由框架或插件提供的接口代理能力,比如开启Umi
的mock
配置,在Taro
中使用@tarojs/plugin-mock
插件
mock
数据:基于mock
能力,为mock
接口提供的数据源,所以要先有mock
能力,才有mock
数据
该工具只生成mock
数据,而不具备提供mock
能力
总结
总的来看,这个功能的逻辑并不复杂,除了几个比较关键的技术点,问题在于是否有这样的提效想法,提效的手段千千万,迈出第一步是关键
- 最后,与君共勉 *