前端接口定义终极方案:类型即接口,和手写API说拜拜!
嘿,各位前端的兄弟姐妹们!大家好,我是一位在前端世界里摸爬滚打了多年的架构师。
今天不聊高深的框架源码,也不谈玄乎的未来趋势,我们就来聊聊日常工作中那个最常见,也最让人头疼的话题——接口对接。
前言:那些年,我们一起"堆"过的 API
前后端分离的架构下,相信很多前端每天的日常就是:切图、看接口文档、定义请求服务、联调、改 Bug、再联调…… 如此循环往复。前端页面就像一座座等待装修的房子,而后端接口就是那些必不可少的水、电、煤气管道。
管道(接口)的铺设(对接)工作,既关键又繁琐。一个项目下来,几十上百个接口是家常便饭。这些接口定义充满了重复性的劳动,但又必须小心翼翼地处理,生怕哪个参数传错,哪个字段写歪,联调时换来后端小哥一个"亲切"的白眼。🙄
现状:刀耕火种的"手工业"时代
在很多团队里,我们对接接口的方式还非常"朴素"。后端同学甩过来一个 Swagger 地址或者一个 Word 文档,然后我们前端就开始了"CV 大法":
- 打开 src/api 或者 src/services 目录。
- 新建一个 user.ts 或者 order.ts 文件。
- 对着文档,一个一个地手写 axios 或者 request 的调用。
我们来看一个典型的"手工艺品":
// 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 接口代码的库。它的核心思想就是:让代码生成代码。
官方的使用方式很简单,通常一个命令就能搞定:
npx swagger-typescript-api -p http://your.swagger.url/v2/api-docs -o ./src/api --no-client
执行完毕后,它会帮你生成类似下面这样的文件:
// -- 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,
});
}
这已经比手写好太多了!它自动生成了类、方法、参数类型和注释。但...对于追求极致的我们来说,这还不够完美。
内部实战:追求极致的"魔改"之路
官方的方案虽好,但在我们的实际项目中,还是发现了一些可以优化的地方:
- 耦合请求库:生成的代码通常会依赖它内部的 HttpClient,而我们公司使用的是自己封装的 request 库,有统一的拦截器、错误处理和认证逻辑。
- 代码冗余:每个接口都生成一个实体方法,如果项目有 500 个接口,就会生成 500 个方法。这会显著增加最终的打包体积。
- 不够灵活:调用方式相对固定,且命名不够直观,我们希望有更简洁的调用体验。
于是,我走上了一条"魔改"之路,目标是:只生成类型定义,通过代理(Proxy)实现动态调用。
我们封装了自己的库 @wmeimob/swagger-api-templates,它利用 swagger-typescript-api 的自定义模板能力,以及自定义 CLI 功能。产出了我们想要的"终极形态"。
1. 工作流程革新
我们的整个自动化流程是这样的,请看图:
2. "魔改"后的代码产物
通过我们的流程,生成的代码长什么样呢?这才是最酷的部分!
产物一:纯类型接口定义 (Admin.ts)
注意看,这个文件里没有任何一个函数实现,它只是一个巨大的 interface,用 URL 作为 key,定义了每个接口的函数签名。
// 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。
// 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 钩子会被触发。
// 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. 优雅的调用方式
有了上面的魔法,我们在页面中的调用就变成了这样:
// 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);
}
这种方式的好处简直不要太明显:
- 极致的类型安全:从 URL 到参数再到返回值,全程享受 TypeScript 带来的丝滑体验,再也不怕传错参数了。
- 零实体代码:生成的接口文件全是类型定义,体积几乎可以忽略不计,有效减小了最终的打包产物大小。
- 调用即文档:api.get['/some/url'] 的写法本身就清晰地表明了请求的地址和方法,代码可读性极强。
- 维护成本趋近于 0:后端更新了接口?没关系,重新生成一下就好,整个过程不超过 1 分钟,剩下的时间,摸鱼喝茶不香吗?😎
总结:让我们一起,早点下班!
通过 swagger-typescript-api 的强大能力,结合自定义模板和 Proxy 的动态特性,我们成功地将前端接口对接从一项繁琐、易错的"手工业",升级为了全自动、高效率的"现代工业"。
配合 Apifox 这类强大的 API 设计工具,后端同学只需要点几下鼠标导出最新的 swagger.json,我们前端执行一个命令,所有接口的变更就自动同步到了代码中。
从此,我们告别了手写 API 的时代,将宝贵的时间和精力投入到更有创造性的工作中去。这不仅提升了开发效率和代码质量,更重要的是,提升了我们前端工程师的幸福感。
好了,不说了,又到了生成接口的时间,一键搞定,准备下班!🚀
希望这篇文章能给你带来一些启发。如果你对我们的方案感兴趣,不妨也尝试在你的团队中推广起来吧!