基于 TypeScript 和 swagger 后端文档进行接口智能提示和校验的研究续集

2,160 阅读3分钟

本文是上篇文章的深入研究和继续探索。

上篇文章使用函数重载的方式让 typescript 得以智能提示和校验,在实践过程中发现存在很多的问题:

  1. 对于函数重载,typesripe 会提示多个重载,并不好区分多个重载的参数,在参数录入错误,后提示并不友好;
  2. 生成代码的难度非常的高,会生成很多的格式代码,增加生成文件的大小。

本文讨论通过泛型和统一的接口类型做提示和校验的可能性。经过观察 swagger 和 openapi 的 json 文档定义,发现我们可以将 typescript 的接口文档和 swagger 的定义格式类似,请求地址可以直接包含 swagger 定义的地址,将请求参数拆分为 path、query、body 分别区分不同的参数位置,以及 response 定义接口返回类型等,然后通过 typescript 工具函数和泛型,对接口请求函数进行定义,最终达到校验和提示的作用,那咱们开始具体的步骤。

  1. 首先定义一个统一的接口配置类型,里面以请求链接地址作为 key,请求方法、参数以及接口返回作为类型进行定义,代码如下:
// swagger.ts
export interface SwaggerInterface1 {
    "/api/login": {
        post: {
            param: { body: { username: string; password: string; grant_type: string } };
            response: { token: string; expire_in: number; refresh_token: string };
        };
    };
    "/api/user/{id}": {
        get: {
            param: { path: { id: string } };
            response: { id: string; name: string };
        };
    };
    "/api/user/{id}/friends": {
        get: {
            param: {
                path: { id: string };
                query: { name: string; age: number };
            };
            response: { id: string; name: string };
        };
    };
}
  1. 定义工具函数
// swagger.utils.ts
import { SwaggerInterface1 } from "swagger.ts";

// 多个接口定义可以合并到一起
type SwaggerInterface = SwaggerInterface1;

// 对于一些统一的返回接口,可以通过这样的方式将没必要的统一格式类型拆开得到需要用到的格式
// 这样处理后在接口请求函数里面也需要进行拆分处理
type ReturnDataType = { code: number; data: any; message: string };
type ReturnData<T extends ReturnDataType | any> = T extends ReturnDataType ? T["data"] : T;

export type UrlKey = keyof SwaggerInterface;
export type MethodKey<U extends UrlKey> = string & keyof SwaggerInterface[U];

type SwaggerInterfaceSingle<U extends UrlKey, M extends MethodKey<U>> = SwaggerInterface[U][M];
type SwaggerField<U extends UrlKey, M extends MethodKey<U>> = keyof SwaggerInterfaceSingle<U, M>;
type SwaggerFieldType<U extends UrlKey, M extends MethodKey<U>, F extends SwaggerField<U, M>> = SwaggerInterfaceSingle<U, M>[F];

export type Param<U extends UrlKey, M extends MethodKey<U>> = SwaggerFieldType<U, M, "param" & SwaggerField<U, M>>;
export type Response<U extends UrlKey, M extends MethodKey<U>> = ReturnData<SwaggerFieldType<U, M, "response" & SwaggerField<U, M>>>;
  1. 定义接口请求方法,这儿只是演示代码,具体的请求方法还需要做一些必要的处理,比如 headers 带上 token、错误处理等
// fetch.ts
import { UrlKey, MethodKey, Param, Response } from "swagger.utils.ts";

function customFetch<U extends UrlKey, M extends MethodKey<U>>(url: U, method: M, params: Param<U, M>): Promise<Response<U, M>> {
    let { path, query, body } = (params || {}) as { path?: any; query?: any; body?: any };
    let iUrl = url as string;
    // 处理url path
    if (path) iUrl = mergeUrlParam(url, path);
    // 处理url query
    if (query) iUrl = mergeUrlQuery(url, query);
    return fetch(iUrl, { method, body: body })
        .then((res) => res.json())
        .then((res) => {
            if (res.code == 200) {
                return res.data;
            }
            return res
        });
}
interface PathParams {
    [key: string]: string | number | boolean;
}

function mergeUrlParam(url: string, paths: PathParams): string {
    // merge code
    return "";
}

function mergeUrlQuery(url: string, query: PathParams): string {
    // merge code
    return "";
}
  1. 然后就能正常的使用函数方法了,如下是得到的提示效果

截屏2022-03-10 11.28.33.png 截屏2022-03-10 11.28.50.png 截屏2022-03-10 11.29.20.png 截屏2022-03-10 11.29.44.png 截屏2022-03-10 11.29.56.png

  1. 接下来就是将 swagger 文档通过程序转换成第一步的结构了,由于两者结构相似,转换起来相对容易很多。代码可以参考 vite-plugin-swagger2ts,这是一个 vite 插件,功能也很简单,将 swagger 接口地址和输出地址填入插件配置中,vite 启动的时候将会去拉取 swagger 的代码并生成对应的 typescript 接口文件。

至此,基于 TypeScript 和 swagger 后端文档进行前端接口智能提示和校验的研究基本结束。

ps: 让人恼火的是实际项目遇到了一些麻烦导致只能通过手写录入接口:1. 后端接口并不怎么规范,不得不手动编写接口以修改接口的错误类型;2. 生成的代码过于庞大导致 vscode 卡住。即便如此,对于项目的规范化还是有非常大的好处。