用 Proxy 封装 API 请求

通常,前端封装完基本的 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 项目这样封装,大概率会被打,因为有几个问题没解决:

  1. 没有限制 req 能使用哪些属性,比如 req.any() 也是会被受理。
  2. 没有任何智能提示,req(url 什么字符串都可以填,且经常需要查阅文档)。

解决方案是: 用 Typescript,步骤如下:

  1. 定一个 ApiRecord 的类型, API 的信息定义在这里
  2. 基于 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
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax