使用TS的你还在自己写接口类型吗?

更新日期: 2024-01-17阅读: 277标签: 接口

刚做完公司项目的新架构,决定使用TS来解决项目中的类型问题,但是在写接口类型的时候,发现了一个问题,就是接口类型的定义,如果是一个复杂的类型,那么就会变得非常的麻烦。

每个接口的request,response不能说一点不相同,只能说完全不相同。甚至写类型的时间比写逻辑的时间都长。往往写到一半TS变成了AS。回过头来又对自己写AS的行为感到羞愧。

后来聪明的我想,Java他还能不写VO类吗?他写了我能不能直接用?然后就有了这篇文章

前言

如何能获取到Java的VO类呢?作为前端开发,我们能获取到的数据只有接口文档,那么我们能不能通过接口文档来生成VO类呢?

答案是肯定的。

获取到接口文档后,一开始甚至想自己写套转换工具。写着写着发现,这个工具的难度比写接口类型还大。于是放弃了。

然后,swagger-typescript-api 出现了。

swagger-typescript-api

swagger-typescript-api;

  • 通过swagger方案生成api。
  • 支持OA 3.0、2.0、JSON、yaml
  • 生成的api模块使用Fetch Api或Axios发出请求。

开始

官方提供了两种使用方式。使用npx或者使用node的方式进行;个人建议还是使用node的方式,可以按照个人或公司的实际需求进行。该插件核心是通过解析swagger文档,通过模板生成TS类型甚至连ajax请求都替我们写好了。

支持ajax和fetch两种请求方式。

模板

通过源码的阅读,在/templates下内置了3套模板

  • base
  • default
  • modular

default生成单个api文件,modular可以根据指定的命名空间生成多个api文件,base既可以生成单个api文件可以生成多个api文件;

举个简单的例子:接口文档有User,Book 两个controller

default 会将User和Book生成在同一个api文件中

// api.ts
UserApi,
BookApi

modular 会将User和Book分别生成在不同的api文件中

// user.ts
UserApi
// book.ts
BookApi

我们通常使用base模板中的modular模式来使用。没有人会想不同模板的接口放在一个页面中维护吧。为什选择 base模板,因为base模板提供了更全面的配置模板。

模板配置详解

base模板中的模板文件如下

  • api.ejs- (生成文件) Api类模块
  • data-contracts.ejs- (生成文件)来自 swagger 模式的所有类型
  • http-client.ejs- (生成文件) HttpClient类模块
  • procedure-call.ejs- (子模板) Api 类中的路由
  • route-docs.ejs- (生成文件) Api 类中的路由文档
  • route-name.ejs- (子模板) Api 类中的路由名称
  • route-type.ejs- (--route-types选项) (子模板)
  • route-types.ejs- (--route-types选项) (子模板)
  • data-contract-jsdoc.ejs- (子模板)为数据合约生成 JSDOC

我们常用的有着重讲解

  • base/route-docs.ejs
  • api.ejs
  • procedure-call.ejs
  • axios-http-client.ejs
  • http-client.ejs
  • route-type.ejs

讲解开始前来一段swagger的文档,结合着看

{
"swagger": "",
"info": "",
"host": "",
"basePath": "",
"tags": [
{
"name": "通用接口",
"x-order": "2147483647"
}
],
"paths": {
"post": {
"tags": [
"通用接口"
],
"summary": "获取活动列表",
"operationId": "getActivityPageListUsingPOST",
"consumes": [
"application/json"
],
"produces": [
"*/*"
],
"parameters": [
{
"in": "body",
"name": "request",
"description": "request",
"required": true,
"schema": {
"originalRef": "CommonActivityPageRequest",
"$ref": "#/definitions/CommonActivityPageRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"originalRef": "IPage«CommonActivityPageResponse»",
"$ref": "#/definitions/IPage«CommonActivityPageResponse»"
}
},
"201": {
"description": "Created"
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
},
"404": {
"description": "Not Found"
}
},
"x-order": "2147483647"
}
},
"definitions": {
"CommonActivityPageResponse": {
"type": "object",
"properties": {
"activityName": {
"type": "string",
"description": "活动名称"
},
"activityType": {
"type": "integer",
"format": "int32",
"description": "活动类型"
},
"title": "CommonActivityPageResponse"
}
}
}
}

其中主要部分包含tags, path,definitions,根据其中信息,使用插件生成后的代码如下

type.ts


// type.ts
/** CommonActivityPageRequest */
export interface CommonActivityPageRequest {
/** 活动名称,支持模糊查询 */
activityName?: string;
/** 活动类型,支持多选 */
activityType?: number[];
}

http-client.ts (部分代码)

// http-client.ts
import axios, { AxiosInstance, AxiosRequestConfig, HeadersDefaults, ResponseType } from "axios";

export type QueryParamsType = Record<string | number, any>;

export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseType;
/** request body */
body?: unknown;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;

...sth

export const httpRequest = new HttpClient();

common.ts

// commion.ts
/**
* @tags 通用接口
* @summary 获取活动列表
* @request POST:/common/getActivityPageList
*/
export const getActivityPageList = (request: CommonActivityPageRequest): Promise<IPageCommonActivityPageResponse> => {
return httpRequest({
url: '/common/getActivityPageList',
method: 'post',
data: request,
});
};

route-docs.ejs

该模板是用于生成API接口文档的JSDoc注释的。它会解析Raw API规范数据,并生成符合JSDoc标准的注释文档。

主要逻辑如下:

  • 从Raw API规范数据中提取出描述信息,生成@description注释。
  • 提取tags、summary等信息,生成对应注释。
  • 生成@request注释,标注请求方法和路径。
  • 如果有响应信息,遍历生成@response注释,包含状态码、响应类型等信息。
  • 使用模板字符串拼接所有信息,组成完整的JSDoc注释块。
  • 返回包含description和lines(注释细节)两个字段的对象。
  • 上方解析结果中的注释部分由该文件控制。

api.ejs

用来生成API服务接口的TypeScript client代码的。通用与procedure-call.ejs一起使用,主要包含以下几个方面

  • 导入必要的类型,包括HttpClient、响应类型等。
  • 如果使用了数据合约(Data Contracts),导入生成的数据合约类型。
  • 遍历所有API路由,为每个路由生成一个方法,调用HttpClient发送请求。
  • 使用ejs模板引擎,渲染procedure-call模板,生成每个方法的具体代码。
  • procedure-call模板会包含方法名、请求配置、响应类型解析等代码。
  • 最终生成的TypeScript代码可以直接用于前端调用后端API。
  • 只需要导入这个生成的模块,就可以轻松调用各个接口,不需要再手写重复的请求代码。

procedure-call.ejs

api.ejs中所需的生成模板内容,它通过模板引擎,根据API路由配置自动生成发起请求的函数。主要功能包括:

  • 引入httpRequest方法,用于发起请求。
  • 根据路由配置,渲染方法签名、请求参数等。
  • 插入由其他模板生成的JSDoc注释,作为方法注释。
  • 构造httpRequest配置,包含方法、URL、查询参数、请求体等。
  • 根据响应类型,设置返回Promise的泛型类型。
  • 如果有安全性配置,生成安全验证参数。
  • 支持从请求参数中提取查询参数、路径参数等。
  • 设置请求体和响应体的内容类型/格式。
  • 支持测试环境配置不同的实例。

axios-http-client.ejs

生成http客户端的模板。主要用途如下:

  • 定义了各种请求和响应相关的类型,如请求参数、响应格式等接口。
  • 实现了 HttpClient 类,封装了 axios 的实例,并定义了请求拦截、安全校验等逻辑。
  • 提供了请求方法 request,根据传入的请求参数,构造 axios 请求配置,发送请求。
  • 支持处理不同的请求数据格式,如 JSON、文本、FormData 等。
  • 支持响应格式化,和安全性校验。
  • 请求方法返回 Promise,并根据配置处理响应结果。
  • 默认导出一个 HttpClient 实例供外部使用。
  • 根据配置生成目标代码,开发者只需导入使用这个 HTTP 客户端即可,简化了HTTP请求逻辑的编码。

http-client.ejs

生成http客户端的入口文件在此可在初始化时选择使用fetch or axios;

route-type.ejs

生成接口的request和response类型,主要是通过definitions中的数据生成的。

  • 导入数据合约(Data Contracts)类型,如果配置了按模块分割数据合约的话。
  • 使用模块名生成接口命名空间,如UserApi。
  • 遍历所有模块下的路由配置。
  • 对每个路由,调用route-type.ejs模板,渲染接口类型定义。
  • route-type.ejs模板会生成每个接口的类型签名,包含请求参数、响应类型等。
  • 最终会组织成一个命名空间,导出所有接口类型。
  • 开发者可以直接导入这个生成的类型模块,在代码中使用接口类型,带来良好的代码提示、检查等支持。
  • 不需要手写或者维护接口类型定义,可以通过接口定义直接生成。


初始化

此时的目录结构应该如下:

|- templates
| - | - base
| - | - | - route-docs.ejs
| - | - api.ejs
| - | - procedure-call.ejs
| - | - axios-http-client.ejs
| - | - http-client.ejs
| - | - route-type.ejs
| - generator.js

我们通常在generator进行初始化。具体配置参照官方文档

我的配置文件

const options = {
url: openApiUrl, //openapi接口url
output: outputDir, //输出目录
templates: path.resolve(__dirname, 'templates'), //模板目录
modular: true, // 为客户端、数据类型和路由生成单独的文件
cleanOutput: false, //清除输出目录
enumNamesAsValues: false,
moduleNameFirstTag: false,
generateUnionEnums: false,
extractRequestBody: true, // 生成请求体类型
extractRequestParams: true, //提取请求参数,将路径参数和查询参数合并到一个对象中
unwrapResponseData: true, // 从响应中展开数据项 res 或 res.data
httpClientType: 'axios', // 可选 'fetch' //http客户端类型
defaultResponseAsSuccess: false,
generateClient: true, //生成http客户端
generateRouteTypes: false, //生成路由器类型
generateResponses: false, //生成响应类型
defaultResponseType: 'void',
typePrefix: '', // 类型前缀
typeSuffix: '', // 类型后缀
enumKeyPrefix: '', // 枚举key前缀
enumKeySuffix: '', // 枚举key后缀
addReadonly: false, // 设置只读
/** 允许根据这些额外模板生成额外文件,请参阅下文 */
extraTemplates: [],
anotherArrayType: false,
fixInvalidTypeNamePrefix: 'Type', //修复无效类型名称前缀
fixInvalidEnumKeyPrefix: 'Value', //修复无效枚举键前缀
hooks: {
onPrepareConfig: (currentConfiguration) => {
const config = currentConfiguration.config
config.fileNames.httpClient = 'httpClient' //http客户端文件名
config.fileNames.dataContracts = 'types' //类型文件名
return {...currentConfiguration, config}
},
onFormatRouteName: (routeInfo, templateRouteName) => {
if (routeInfo.method === 'get') {
return `Get${lodash.upperFirst(routeInfo.moduleName)}Request`;
}
return templateRouteName;
},
onPreParseSchema: (originalSchema, typeName, schemaType) => {
if (originalSchema.type === 'integer' && originalSchema.format === 'int64') {
originalSchema.type = 'string';
originalSchema.format = 'string';
}
},
}
};

generateApi(options)

其中generateApi方法是官方提供的方法,用于生成api文件。该方法返回promise,可以在其生成后在进行其他额外的配置。

比如将首字母大写,将文件名改为index.ts等等。

generateApi(options).then(({files, configuration}) => {
// do something
})

最后

我们只要在接口文档更新后,每次使用node执行generator.js文件即可每次更新。使用也只需要在需要的地方导入接口即可。

一个字真他妈爽!


注意

该插件已经投入生产环境使用,也遇到了一些问题。

对后端代码来说,注释规范要求极高。否则生成的接口文档会出现问题。

vo类不能出现非英文字符,否则会报错。

接口如果存在使用query接受动态参数也会容易出错,前端解析后会出现$字符导致解析失败。

下载类接口返回流,无法生成responseType: 'blob',导致流解析失败。即使手动修改下载生成也会被覆盖。暂时解决 方案为约定末尾均为export字段在模板中加入。修改axios-http-client.ejs文件,在发起请求前加入。
作者:IAmor
https://juejin.cn/post/7295343805020274698

链接: https://www.fly63.com/article/detial/12638

为什么前后端分离了,你比从前更痛苦?

前后端分离可以让我们的职责更清晰,打破前端发挥的局限,工作解耦之后能更好的提高开发效率。然而因为没有规划好开发流程,导致了我们没有发挥出其应有的价值,造成了更多的浪费。

前后端分离项目的跨域及保持Session会话

当Web项目前后端分离开发的时候, 由于域名不一致, 会出现无法请求和无法维持会话的情况,在前端Ajax请求后台的时候, 打开控制台可以看到, 每一次请求之前都会有一次OPTIONS类型的请求

PHP面向对象(抽象类与抽象方法、接口的实现)

任何一个类,如果它里面至少有一个方法是被声明为抽象的,那么这个类就必须被声明为抽象的。定义为抽象的类不能被实例化。 被定义为抽象的方法只是声明了其调用方式(参数),不能定义其具体的功能实现。

vue 项目接口管理

在vue开发中,会涉及到很多接口的处理,当项目足够大时,就需要定义规范统一的接口,如何定义呢?方法可能不只一种,本文使用axios+async/await进行接口的统一管理。

免费的公共API接口_WebService接口大全

这篇文章为大家整理一下免费,常用的的WebService接口,列举一些搜集到的免费的公共API接口,希望对你有所帮助,天气预报Web服务,数据来源于中国气象局;IP地址来源搜索 WEB 服务;随机英文、数字和中文简体字

常用HTTP接口测试工具对比

从功能上Jmeter最为强大,可以测试各种类型的接口,不支持的也可以通过网上或自己编写的插件进行扩展。SoapUI专门针对HTTP类型的两种接口,其初衷更是专门测试Soap类型接口,对于其他协议的接口不支持

TypeScript接口(Interfaces)来定义对象的类型

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implements)

你不得不了解的前后端分离原理!

前后端分离已成为互联网项目开发的业界标准使用方式,通过nginx+tomcat的方式(也可以中间加一个nodejs)有效的进行解耦,并且前后端分离会为以后的大型分布式架构、弹性计算架构

vue项目接入mock&& axios 通用配置

兵马未动,粮草先行; 同理,项目开发过程中经常会出现接口未出, 前端页面已搭建完毕的情况;此时为了提高前端的开发效率,解放生产力,我们 FE 可以按照预定的接口文档做一些接口模拟的工作

vue中使用proxy配置不同端口和ip接口

使用vue-cli创建的项目,开发地址是localhost:8080,由于后台开发不同的模块,导致每个模块请求的ip和端口号不一致,解决问题:在vue.config.js中配置不同的端口号

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!