Skip to content

前端接口定义终极方案:类型即接口,和手写API说拜拜!

嘿,各位前端的兄弟姐妹们!大家好,我是一位在前端世界里摸爬滚打了多年的架构师。

今天不聊高深的框架源码,也不谈玄乎的未来趋势,我们就来聊聊日常工作中那个最常见,也最让人头疼的话题——接口对接

前言:那些年,我们一起"堆"过的 API

前后端分离的架构下,相信很多前端每天的日常就是:切图、看接口文档、定义请求服务、联调、改 Bug、再联调…… 如此循环往复。前端页面就像一座座等待装修的房子,而后端接口就是那些必不可少的水、电、煤气管道。

管道(接口)的铺设(对接)工作,既关键又繁琐。一个项目下来,几十上百个接口是家常便饭。这些接口定义充满了重复性的劳动,但又必须小心翼翼地处理,生怕哪个参数传错,哪个字段写歪,联调时换来后端小哥一个"亲切"的白眼。🙄

现状:刀耕火种的"手工业"时代

在很多团队里,我们对接接口的方式还非常"朴素"。后端同学甩过来一个 Swagger 地址或者一个 Word 文档,然后我们前端就开始了"CV 大法":

  1. 打开 src/api 或者 src/services 目录。
  2. 新建一个 user.ts 或者 order.ts 文件。
  3. 对着文档,一个一个地手写 axios 或者 request 的调用。

我们来看一个典型的"手工艺品":

typescript
// src/api/member.ts  
import request from '@/utils/request';

// 定义获取会员列表的参数类型  
interface MemberListParams {  
  page: number;  
  size: number;  
  nickname?: string;  
}

// 定义会员信息类型  
interface MemberInfo {  
  id: number;  
  nickname: string;  
  avatar: string;  
  level: number;  
}

// 获取会员列表  
export function getMemberList(params: MemberListParams) {  
  return request.get<MemberInfo[]>('/api/member/list', { params });  
}

// 获取会员详情  
export function getMemberDetail(id: number) {  
  return request.get<MemberInfo>(`/api/member/detail/${id}`);  
}

// ... 可能还有几十上百个类似的函数

看起来是不是很熟悉?这种方式有几个显而易见的问题:

  • 枯燥且耗时:纯粹的体力活,毫无技术含量,但又不得不做。
  • 容易出错:手写 URL、参数名、params 和 data 的位置,一不留神就写错了。
  • 维护困难:后端接口一变更(比如改个字段名,换个请求方式),前端就需要手动在成堆的代码里找到对应的位置进行修改,苦不堪言。

每当这时,我都在想,我们前端工程师,是来创造用户价值的,不是来当接口的"搬运工"的!我们必须找到一种更优雅、更高效的方式!🤖

新一代对接方案:swagger-typescript-api 闪亮登场

终于,我找到了解放生产力的利器——swagger-typescript-api

这是一个能根据 Swagger (OpenAPI) v2/v3 的 JSON/YAML 规范,自动生成 TypeScript 或 JavaScript 接口代码的库。它的核心思想就是:让代码生成代码

官方的使用方式很简单,通常一个命令就能搞定:

bash
npx swagger-typescript-api -p http://your.swagger.url/v2/api-docs -o ./src/api --no-client

执行完毕后,它会帮你生成类似下面这样的文件:

typescript
// -- out/api.ts --  
import { HttpClient, RequestParams } from "./http-client";

export class Users<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {  
  /**  
   * No description  
   *  
   * @tags user  
   * @name UsersList  
   * @request GET:/users  
   */  
  usersList = (params: RequestParams = {}) =>  
    this.request<User[], any>({  
      path: `/users`,  
      method: "GET",  
      ...params,  
    });  
}

这已经比手写好太多了!它自动生成了类、方法、参数类型和注释。但...对于追求极致的我们来说,这还不够完美。

内部实战:追求极致的"魔改"之路

官方的方案虽好,但在我们的实际项目中,还是发现了一些可以优化的地方:

  1. 耦合请求库:生成的代码通常会依赖它内部的 HttpClient,而我们公司使用的是自己封装的 request 库,有统一的拦截器、错误处理和认证逻辑。
  2. 代码冗余:每个接口都生成一个实体方法,如果项目有 500 个接口,就会生成 500 个方法。这会显著增加最终的打包体积。
  3. 不够灵活:调用方式相对固定,且命名不够直观,我们希望有更简洁的调用体验。

于是,我走上了一条"魔改"之路,目标是:只生成类型定义,通过代理(Proxy)实现动态调用

我们封装了自己的库 @wmeimob/swagger-api-templates,它利用 swagger-typescript-api 的自定义模板能力,以及自定义 CLI 功能。产出了我们想要的"终极形态"。

1. 工作流程革新

我们的整个自动化流程是这样的,请看图:

2. "魔改"后的代码产物

通过我们的流程,生成的代码长什么样呢?这才是最酷的部分!

产物一:纯类型接口定义 (Admin.ts)

注意看,这个文件里没有任何一个函数实现,它只是一个巨大的 interface,用 URL 作为 key,定义了每个接口的函数签名。

typescript
// src/request/Admin.ts  
import { ITaroRequestConfig } from "@wmeimob/request/src/types/taro-type";  
import * as DC from "./data-contracts";

type RequestConfig = Partial<ITaroRequestConfig>;

export interface APIGET {  
  /**  
   * @summary 详情  
   * @tags admin/系统-资源管理, 系统-资源管理  
   */  
  "/admin/api/sysResource/detail/{resource-id}": (  
    query: { resourceId: number } & { requestConfig?: RequestConfig }  
  ) => Promise<DC.JsonResultResourceDetailVo>;

  /**  
   * @summary 列表  
   * @tags admin/会员-会员管理, 会员管理  
   */  
  "/admin/api/member/query": (  
    query: DC.AdminApiMemberQueryGetParams & { requestConfig?: RequestConfig }  
  ) => Promise<DC.JsonResultPagedResultMemberDetailOutputDto>;

  // ... 其他几百个接口类型  
}

export interface APIPOST {
    // ... POST 接口类型  
}  
// ... 其他请求方法的接口类型

产物二:数据传输对象 (data-contracts.ts)

这个文件包含了所有后端定义的 DTO (Data Transfer Object),同样是纯 interface。

typescript
// src/request/data-contracts.ts  
export interface JsonResultResourceDetailVo {  
  /** @format int32 */  
  code?: number;  
  data?: ResourceDetailVo;  
  msg?: string;  
}

export interface ResourceDetailVo {  
  id?: number;  
  name?: string;  
  // ...  
}

export interface AdminApiMemberQueryGetParams {  
  /** @format int32 */  
  page?: number;  
  /** @format int32 */  
  size?: number;  
  name?: string;  
}

产物三:灵魂所在——Proxy 调用入口 (index.ts)

这才是魔法发生的地方!我们导出一个 api 对象,它是一个 Proxy。当你调用 api.get['/admin/api/sysResource/detail/{resource-id}'] 时,Proxy 的 get 钩子会被触发。

typescript
// src/request/index.ts  
import * as Admin from "./Admin";  
import requestInstance from './instance';

export * from './data-contracts';

// ... 省略部分辅助函数

// 最终导出的 api 对象类型,聚合了所有请求方法  
export type UType = {  
  get: Admin.APIGET;  
  post: Admin.APIPOST;  
  // ... del, put  
};

// 创建一个 Proxy 对象  
export const api: UType = new Proxy(  
  {  
    get: createMethodProxy('GET'),  
    post: createMethodProxy('POST'),  
    del: createMethodProxy('DELETE'),  
    put: createMethodProxy('PUT'),  
    // ...  
  },  
  {  
    // ... 省略了兼容旧写法的逻辑  
  }  
);

// 为每种 HTTP 方法创建一个子 Proxy  
function createMethodProxy(method: string) {  
  return new Proxy({}, {  
    get: (_target: any, propKey: string) => getterHandler(propKey, method)  
  });  
}

// 核心处理函数  
function getterHandler(url: string, method: string) {  
  // 返回一个真正的函数,它接收参数并发起请求  
  return (args: any) => {  
    let _url = url;  
    let { requestConfig = {}, query = {}, ...rest } = args ?? {};

    // 处理动态路由参数,例如 /path/{id}  
    if (_url.includes('{')) {  
      // ... 逻辑:从 args 中找到 id,并替换 url 中的 {id}  
    }

    const config = { method, url: _url, ...requestConfig };

    // 根据请求类型,将参数放到 params 或 data 中  
    if (['POST', 'PUT'].includes(method)) {  
      config.data = rest;  
      if (Object.keys(query).length) {  
        config.params = query;  
      }  
    } else {  
      config.params = rest;  
    }

    // 调用我们项目统一的请求实例  
    return requestInstance(config);  
  };  
}

3. 优雅的调用方式

有了上面的魔法,我们在页面中的调用就变成了这样:

typescript
// src/pages/resource/index.tsx  
import { api } from '@/request';

async function getResourceDetail(id: number) {  
  // 看这里!调用如此清爽!  
  // 并且,你将获得完美的 TypeScript 类型提示!  
  // 当你输入 `{` 时,IDE 会自动提示你需要 `resourceId: number`  
  const { data = {} } = await api.get['/admin/api/sysResource/detail/{resource-id}']({ resourceId: id });  
    
  // `data` 的类型也会被自动推断为 `ResourceDetailVo`  
  console.log(data.name);   
}

这种方式的好处简直不要太明显:

  1. 极致的类型安全:从 URL 到参数再到返回值,全程享受 TypeScript 带来的丝滑体验,再也不怕传错参数了。
  2. 零实体代码:生成的接口文件全是类型定义,体积几乎可以忽略不计,有效减小了最终的打包产物大小。
  3. 调用即文档:api.get['/some/url'] 的写法本身就清晰地表明了请求的地址和方法,代码可读性极强。
  4. 维护成本趋近于 0:后端更新了接口?没关系,重新生成一下就好,整个过程不超过 1 分钟,剩下的时间,摸鱼喝茶不香吗?😎

总结:让我们一起,早点下班!

通过 swagger-typescript-api 的强大能力,结合自定义模板和 Proxy 的动态特性,我们成功地将前端接口对接从一项繁琐、易错的"手工业",升级为了全自动、高效率的"现代工业"。

配合 Apifox 这类强大的 API 设计工具,后端同学只需要点几下鼠标导出最新的 swagger.json,我们前端执行一个命令,所有接口的变更就自动同步到了代码中。

从此,我们告别了手写 API 的时代,将宝贵的时间和精力投入到更有创造性的工作中去。这不仅提升了开发效率和代码质量,更重要的是,提升了我们前端工程师的幸福感。

好了,不说了,又到了生成接口的时间,一键搞定,准备下班!🚀

希望这篇文章能给你带来一些启发。如果你对我们的方案感兴趣,不妨也尝试在你的团队中推广起来吧!