通常,前端封装完基本的 axios 实例后,还需要将每个 API 封装成一个个函数,比如:
js
import { req } from './axiosInstance'
export const userApi = {
getPages: (params) => req.get('user/pages', { parmas }),
update: (id, data) => req.put(`user/${id}`, data),
//... 其他用户相关的接口
}
这种方式会有很多的样板代码,而且要给每个接口起名字。 本文提供一种新颖的方式,只需少量代码即可封装 API 请求。请看代码和注释:
js
const createReq = (axiosInstance) =>
new Proxy({},{// 创建一个代理对象
get: (_target, prop) => //无论获取什么属性,都返回一个函数
(url, ...rest) => //第一个参数是URL,其他视为请求参数或者配置
axiosInstance.request({//将参数交给 axios.request 处理
method: prop,//默认要获取的属性是 method
url,
...transformPayload(url, rest), //转换对应的 params,query,body, 往下看有源码
}),
}
)
调用示例:
js
const req = createReq(axios.create({}))
req.get('user/pages', { query:{ ... } })
// URL的 :id 会在 transformPayload 中处理
req.put('user/:id', { params:{ id }, body:{ ... }})
// 如果有其他配置,比如timeout, 在最后一个参数配置
req.get('user/pages', { query:{ ... } }, { timeout: 3000 })
// 如果接口本来没有参数,第二个参数就是配置
req.get('user/self', { timeout: 3000 })
如果你在 JS 项目这样封装,大概率会被打,因为有几个问题没解决:
- 没有限制 req 能使用哪些属性,比如 req.any() 也是会被受理。
- 没有任何智能提示,req(url 什么字符串都可以填,且经常需要查阅文档)。
解决方案是: 用 Typescript,步骤如下:
- 定一个 ApiRecord 的类型, API 的信息定义在这里
- 基于 ApiRecord 映射出 req 的类型
首先,制定一个类型,为了能够轻松的拿到API信息,这里选用 key,value 结构。 并使用两层这样的对象,第一层对象的 key 是 method,第二层对象的 key 是 url, value 则是这个接口对应的请求参数,和返回值
ts
type ApiType = Record<string, Record<string, any>>
// 实际类型结构
type ActualApi = {
get: {
'user/pages': {
query: { skip: number; take: number }
response: UserDTO
// body
// parmas
// headers
} //基本就是这 5 个属性
}
}
开始写代码,需要一定的 TS 范型知识,暂时不懂也没关系,认真看注释。
ts
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
// 将第一层对象简单重映射,保留key,value映射成函数类型
type MakeReqType<T extends ApiShape> = {
[Method in keyof T]: RequestFn<T[Method]>
}
//将第二层对象传入范型T,得到一个函数类型,
type RequestFn<T extends Record<string, any>> = <K extends keyof T>(
url: K, //第一个参数是url
...rest: MakeParams<T[K]> //其他参数
) => Promise<AxiosResponse<T[K]['response']>['data']> //返回值类型
// 解析参数类型, 使用 具名元组 来描述参数,
// [number,string]这个叫元组,元素有固定类型的数组。
// [a:number,b:string] 每个元素还具有名字,叫具名元组
type MakeParams<
T extends BaseShape,
Payload = Omit<T, 'headers' | 'response'>,
Config = AxiosRequestConfig & Simplify<Pick<T, 'headers'>>
> = Payload extends EmptyObject
? [config?: Config] //没有请求参数时, 第二个参数就是config, 而且是可选的
: [payload: Simplify<Payload>, config?: Config]
//基本的类型
type BaseShape = {
headers: Record<string, any>
response: unknown
}
//请求参数的类型
type PayloadType = {
query?: any
params?: any
body?: any
}
//工具类型, 代码来自 type-fest
declare const emptyObjectSymbol: unique symbol
type EmptyObject = { [emptyObjectSymbol]?: never }
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {}
const descendByKeyLength = (kvs: [string, any][]): [string, any][] =>
kvs.sort((a, b) => b[0].length - a[0].length)
// input('user/:id', ['id' '666']),
// output => 'user/666'
const replaceURL = (url: string, sortedKeyValues: [string, any][]) =>
sortedKeyValues.reduce(
(acc, [k, v]) => acc.replace(':'.concat(k), String(v)),
url
)
//主要负责转换请求参数
const transformPayload = (
url: string,
args: DynamicParmasType
): AxiosRequestConfig => {
if (args.length < 1) return {}
const [payload, config] = args
const { query, params, body, ...rest } = payload as PayloadType
const actualUrl =
params == null
? url
: replaceURL(url, descendByKeyLength(Object.entries(params)))
return {
url: actualUrl,
params: query,
data: body,
...rest,
...(config ?? {}),
}
}
// 中间态
type DynamicParmasType =
| [config?: AxiosRequestConfig]
| [payload: PayloadType, config?: AxiosRequestConfig]
export const createReq = <T extends ApiShape>(axiosInstance: AxiosInstance) =>
new Proxy({} as T, {
get:
(_target, method: string) =>
(url: string, ...rest: DynamicParmasType) =>
axiosInstance.request({
method,
url,
...transformPayload(url, rest),
}),
})
基于Proxy的请求对象封装完成,实际使用的时候,只需要定义好 ApiRecord 类型,把它传给 createReq 的范型参数,后续新增或者变更,修改 ApiRecord 类型即可,req 不用变。
ts
// types/api-record.ts
type UserDTO = {
id: number
name: string
age: number
}
export type ApiRecord = {
get: {
'user/pages': {
query: { skip: number; take: number; where: any }
response: UserDTO
}
'user/:id': {
params: { id: number }
response: UserDTO
}
}
post: {
user: {
body: { name: string; age: number }
response: UserDTO
}
}
//..others
}
ts
// api/req.ts
import axios from 'axios'
import { ApiRecord } from '../types/api-record'
const instance = axios.create({
baseURL: '/api',
})
instance.interceptors.response.use(v => v.data)
export const req = createReq<ApiRecord>(instance)
现在,根据 ApiRecord 的定义, req 只有 get,post 两个属性,整个调用过程都会有智能提示。
动图展示
总结
通过 Proxy + 类型定义,封装 API 请求, 简化接口的维护成本,调用时伴随智能提示,优化开发体验。
彩蛋
使用 CLI 工具 create-tealina
,快速创建一个TS全栈脚手架项目,其中前端自带这一套方案,而且 ApiRecord 也不需要手动编写 这里有文章介绍
bash
pnpm create tealina