一键打通api、TS、mock

前言

团队内推行TS的一大阻力是api接口的类型定义,十来个有效字段倒还好说,但如果是几十个有效字段呢?传统手艺是等接口通了,根据接口返回的数据按自己所需字段一个一个定义

但现在已经2025年了,聪明的你可能会采取以下手段:

  • 对接口文档进行截图,贴给AI,让其产出TS类型
  • 将接口返回数据复制下来,贴到在线转换工具将数据转成TS类型
  • 借助apifox等工具来管理接口,产出TS类型

目前主流的开发模式是前后端分离,所以前后端是并行开发的,接口文档出来之后并不代表api接口就有数据,这时候前端通常会通过各种手段生成mock数据:

  • 手动定义一份符合数据结构的mock数据
  • 借助mockjs等第三方库来生成mock数据
  • 编写一个浏览器插件,拦截接口,返回mock数据
  • 公司接口服务有提供mock接口

上面提到的各种手段的问题点:

  1. 功能点单一独立,无整合手段,使用起来有割裂感
  2. 无逻辑抽象,功能无法在多种应用场景下复用,如CLIIDE插件等功能应用场景
  3. 缺少统一代码规范手段,如何降低遵循规范的心智成本是我们推行规范的关键点之一,毕竟规范做得再好,遵循成本高,推行难度就大
  4. 不够灵活,无法根据实际业务场景进行自定义规则
  5. 不够高效,依然有一定的人工成本

针对这些问题,我(点子王)有个点子🙋‍♂️

正文

我们先思考一个问题:api接口的类型和mock有什么关系?

答案是:它们的数据源都来自接口平台 ,同一数据源可以通过node脚本快速生成代码

功能载体

快速生成文件的交互,有两个方向:CLIvscode插件,对比下这两个方案的优劣势

vscode插件

优势:

  • 使用灵活,随意想在哪个目录生成,直接右键操作即可

劣势:

  • 开发成本高,需要有vscode插件开发知识储备
  • 维护成本高,目前公司只有我写过vscode插件,没有backup开发人员
  • 插件需要发布到开放市场,存在安全性问题
CLI

优势:

  • 接入CLI插件体系,node脚本编写,支持技术共建
  • 有使用过UMInestjs命令的狂喜,相同的交互体验
  • 内部发包,无安全性问题

劣势:

  • 对目录结构要求较高,非常规目录无法使用
  • 对于习惯使用独立终端运行命令的开发同学,使用体验上会有割裂感

综合来看,CLI方案更加合适,虽然它对目录结构要求比较高,但我们工程化所推行的应用框架是UmiTaro,目录结构还是比较稳定的,没多少影响

方案设计

通过设计图不难发现,其实功能并不复杂,简称有手就行。其中比较关键的几个点:获取配置文件的配置内容、中间件、数据转换的核心逻辑

转译配置文件

配置文件类似于UmiTaro,由核心层提供一个支持类型提示的defineConfig函数,第一版协议还不够开放,仅做参考

配置文件必然是ts文件了,node23以下版本是不支持动态引入ts文件的,所以需要将ts文件转成cjs,再获取其导出的内容

转译工具太多了,我这里使用的是esbuild,因为转译后内容是string类型,所以再通过require-from-string这个库读取导出的内容

说明一下配置文件为什么一定要是ts文件,根本原因是类型提示,我已经回想不起来对接第三方库没有ts类型提示的日子是什么样的了(忆往昔)

数据中间件

由于公司内部的接口平台有两个,所以我们需要有两个中间件将两个平台的接口数据转成核心逻辑所需要的数据结构。统一输入数据格式,数据转换作为抽象逻辑就变得稳定可复用,后续哪怕更换接口平台,也只需增加对应的中间件即可

数据转换核心逻辑

这部分功能我当时是让AI帮我实现的,对于逻辑比较复杂的单一功能,比较建议使用AI来提高效率

输出方案有两种:ATSstringATS的好处在于可以精准的控制文件内容,开发时间有限则可以使用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能力:是由框架或插件提供的接口代理能力,比如开启Umimock配置,在Taro中使用@tarojs/plugin-mock插件

mock数据:基于mock能力,为mock接口提供的数据源,所以要先有mock能力,才有mock数据

该工具只生成mock数据,而不具备提供mock能力

总结

总的来看,这个功能的逻辑并不复杂,除了几个比较关键的技术点,问题在于是否有这样的提效想法,提效的手段千千万,迈出第一步是关键

  • 最后,与君共勉 *
相关推荐
qq_3325394513 分钟前
React 前端框架推荐
前端·react.js·前端框架
拉不动的猪30 分钟前
刷刷题34(uniapp中级实际项目问题-1)
前端·vue.js·面试
奔跑的露西ly1 小时前
【HarmonyOS NEXT】实现文字环绕动态文本效果
前端·javascript·html·harmonyos
irving同学462383 小时前
Next.js 组件开发最佳实践文档(TypeScript 版)
前端
刺客-Andy3 小时前
React Vue 项开发中组件封装原则及注意事项
前端·vue.js·react.js
marzdata_lily3 小时前
从零到上线!7天搭建高并发体育比分网站全记录(附Java+Vue开源代码)
前端·后端
红尘散仙3 小时前
二、WebGPU 基础入门——基础知识
rust·typescript·gpu
小君3 小时前
让 Cursor 更加聪明
前端·人工智能·后端
顾林海3 小时前
Flutter Dart 异常处理全面解析
android·前端·flutter
残轩3 小时前
JavaScript/TypeScript异步任务并发实用指南
前端·javascript·typescript