重新定义 Axios 的使用方式
前言
✨ 某次刷到了VueUse或许不是唯一选择,试试这匹黑马 VueHooks Plus ?文章,内部推荐的
useRequest
让作为一名Vuer
的我眼前一亮,对上眼了?️?️。但是在后续的项目开发中,由于其基于插件方式进行扩展,不太符合完整的请求流,而且不适配Vue2
,之后借鉴了 SWR 和 Koa 开发出一个基于中间件的请求状态管理库,与Vue
和React
无关,但可通过中间件进行集成。
创建示例项目
作为一名 Vuer
当然是 vite
+ vue3
了?。
# 可自行选择初始化方式
pnpm create vite test-app --template vue-ts
安装依赖
# 进入项目根目录并且安装依赖
cd test-app && pnpm i
# 安装本次示例依赖
pnpm add axios @rhao/request @rhao/request-middleware-vue @rhao/request-middleware-axios @rhao/request-basic-middleware
依赖说明
- @rhao/request 基于中间件的请求管理库,只包含状态管理基础功能
- @rhao/request-middleware-vue 用于适配
Vue
的中间件,基于vue-demi
适配Vue2
和Vue3
- @rhao/request-middleware-axios 用于适配
Axios
的中间件 - @rhao/request-basic-middleware 包含常用功能(刷新
Token
、轮询、错误重试、防抖、节流、SWR
等)的中间件
使用方式
创建 axios
实例
// src/utils/axios.ts
import Axios from 'axios'
export const axios = Axios.create({
// ...
})
创建 useRequest
// src/hooks/useRequest.ts
import { createRequest } from '@rhao/request'
import { RequestVue } from '@rhao/request-middleware-vue'
import { RequestAxios } from '@rhao/request-middleware-axios'
import { axios } from '@/utils/axios'
// 导入需要注册的基础中间件
import { RequestSWR } from '@rhao/request-basic-middleware/swr'
export const useRequest = createRequest({
// 注册全局中间件
middleware: [
// 适配 Vue
RequestVue(),
// 搭建 Axios 与 useRequest 的桥梁,需要传入默认的 axios 实例
RequestAxios({ axios }),
// 注册 SWR 功能
RequestSWR(),
]
})
解析数据格式
上述仅创建了初始的 useRequest
,接受一个 fetcher
进行管理,可以自动解析其入参和出参,但是往往我们会和后端约定一种数据格式,所以可以全局设置 dataParser
对执行请求后的数据进行拆包。
// src/hooks/useRequest.ts
// ...
import type {
BasicRequest,
RequestFetcher,
RequestOptions,
RequestResult,
} from '@rhao/request-middleware-vue'
import type { AxiosResponse } from 'axios'
// 扩展调用签名,由于本次基于 axios 作为请求器,所以先对其返回类型进行拆包
interface UseRequest extends BasicRequest {
<TData, TParams extends unknown[] = unknown[]>(
fetcher: RequestFetcher<TData, TParams>,
options?: RequestOptions<TData, TParams>,
): RequestResult<TData extends AxiosResponse<infer D> ? D : TData, TParams>
}
// 与后端约定的数据格式
interface BackendDataFormat {
success: boolean
data: any
message?: string
}
export const useRequest = createRequest({
// ...
// 对数据进行拆包和解析,错误消息可直接抛出异常,便于被内部执行器捕获然后判定为失败
dataParser: (data: AxiosResponse) => {
// 响应状态码不是 200 时直接抛出异常信息,可自定义
if (data.status !== 200) throw new Error(data.statusText)
// 后端格式数据
const realData: BackendDataFormat = data.data
// 后端返回状态标识未成功,则抛出错误消息
if (!realData.success) throw new Error(realData.message)
// 返回解析后的数据
return realData.data
}
})
管理接口
为了方便接口调用的管理,我们在 src
目录下创建 apis
目录存储所有接口请求。
// src/apis/example.ts
import { axios } from '@/utils/axios'
// 接口入参
export interface Params {
a: number
b: string
}
// 接口数据单项
export interface DataItem {
a: number
b: string
}
// 导出查询列表接口
// axios 调用时需填入响应数据类型用于 useRequest 解析
export const queryList = (params: Params) => axios.get<DataItem[]>('/api/example/list', { params })
页面使用
import { useRequest } from '@/hooks/useRequest'
import { queryList } from '@/apis/example'
import { axios as axios1 } from '@/utils/axios'
// data => Ref<DataItem[] | null> // 这里会自动对 AxiosResponse 进行拆包
// params => Ref<[Params]> // 会自动推导出 queryList 的入参
// loading => Ref<boolean> // loading 仅在初次和最后一次调用时允许被更改,即多次并发调用仅触发两次更新
// error => Ref<Error | null> // 执行失败时的错误信息
const { data, loading, error, params, run } = useRequest(queryList, {
// 是否手动调用,默认自动调用
manual: true,
// 初次自动调用时的参数,设置 `manual` 为 `false` 时有效
// 类型会自动推导出 Params
defaultParams: [],
// 若 queryList 使用 axios1,则此处可更改执行时桥接的 axios 实例
axios: axios1,
// 这里可以传入 axios 的配置项,支持函数格式
axiosConfig: {
timeout: 10e3
},
// 默认 useRequest 的 key 呈自增形式,安装 swr 中间件后可通过相同的 key 共享数据而不会触发重复请求
key: 'shared-key'
})
// 所有的错误均通过 error 获取,run 只会返回执行后的数据
run({ a: 1, b: '123' })
开发中间件
createRequest
负责创建用于管理整个请求流程的 hook
函数,通过注册不同中间件来满足各种场景下的使用,支持函数和对象两种形式。
通过
hooks
事件开发和middleware
开发的区别:
- hook:before、after 回调顺序执行(先注册先执行),不符合整个请求流,且不支持中断后续回调
- middleware:before、after 成对顺序执行(参考洋葱圈模型),符合整个请求流,支持中间件自主中断后续执行
中间件可通过传入的 context
对 options
、result
进行扩展,来满足不同场景下的功能需求。
防抖中间件
/* eslint-disable unused-imports/no-unused-vars */
import type { RequestMiddleware } from '@rhao/request'
// 辅助工具包,可选其他工具
import { assign, isUndef, mapValues, pick, toValue } from '@rhao/request-utils'
// 直接使用成熟的 lodash.debounce
import { debounce } from 'lodash-es'
// 辅助类型包,可选
import type { MaybeGetter } from '@rhao/request-types'
// 定义接受的配置项
export interface RequestDebounceOptions {
/**
* 防抖等待时间,单位:ms,设置后开启防抖模式
*/
wait?: number
/**
* 延迟时最大等待时间
*/
maxWait?: MaybeGetter<number>
/**
* 在延迟开始前执行调用
* @default false
*/
leading?: MaybeGetter<boolean>
/**
* 在延迟结束后执行调用
* @default true
*/
trailing?: MaybeGetter<boolean>
}
// 导出防抖中间件,支持传入一定的默认配置项
export function RequestDebounce(initialOptions?: RequestDebounceOptions) {
// 中间件对象
const middleware: RequestMiddleware = {
// 中间件优先级,视情况而定,数值越大越先执行,也就可以获取到最原始的 fetcher、executor、result
// 存在相同类型扩展时由优先级和注册顺序决定谁先扩展
// 防抖、节流主要针对 executor,且与直接执行 executor 相关,所以优先级最低,这里给了 -1000,基本够用
priority: -1000,
// 每调用一次 useRequest 就会执行 setup 进行中间件初始化
// 可通过传入的 context 对 options、result、executor、fetcher 等进行扩展
setup: (ctx) => {
const options = assign(
// 默认的配置项
{ leading: false, trailing: true } as RequestDebounceOptions,
// 注册中间件时传入的配置项
initialOptions,
// 单次调用 useRequest 时传入的配置项
ctx.getOptions().debounce,
)
// 如果 wait 定义了则改造其 executor
if (!isUndef(options.wait)) {
// opts.maxWait 不能显示设置为空,这是 lodash.debounce 内部的处理导致
const opts = mapValues(pick(options, ['maxWait', 'leading', 'trailing']), (v) => toValue(v))
// 获取防抖后的 executor
const debouncedExecutor = debounce(ctx.executor, options.wait, opts)
// 注册取消事件,取消时也取消防抖后的调用
ctx.hooks.hook('cancel', () => debouncedExecutor.cancel())
// 修改原 executor,需返回 Promise
ctx.executor = (...args) => Promise.resolve(debouncedExecutor(...args))
}
},
}
// 返回中间件
return middleware
}
// 扩展 request 类型
declare module '@rhao/request' {
// 为 options 入自定义的配置项提供类型
interface RequestCustomOptions<TData, TParams extends unknown[] = unknown[]> {
debounce?: RequestDebounceOptions
}
}
刷新 Token
前端开发时,很多时候会用到 token
进行接口的安全调用,但 token
本身存在过期行为,此时由业务场景决定是直接跳转登录还是在一定时间内允许其刷新 token
,此次来看看如何开发一个刷新 token
的中间件。
/* eslint-disable unused-imports/no-unused-vars */
import { MiddlewareHelper } from '@rhao/request'
import type { MiddlewareStoreKey, RequestContext, RequestMiddleware } from '@rhao/request'
import { assign, ensureError, pick, toValue } from '@rhao/request-utils'
import type { AwaitableFn, PromiseFn } from '@rhao/request-types'
// 定义需要接收的配置项
export interface RequestRefreshTokenOptions {
/**
* 本次过期是否允许刷新令牌,默认均可刷新,也可通过传入的 key 和上下文对象判定当前是否需要刷新
* @default
* ```ts
* () => true
* ```
*/
allow?: AwaitableFn<[key: string, context: RequestContext<any, any[]>], boolean>
/**
* 验证刷新令牌是否过期,根据错误信息来验证
*/
expired: AwaitableFn<[error: Error], boolean>
/**
* 允许刷新令牌时的具体刷新操作
*/
handler: PromiseFn<[context: RequestContext<any, any[]>], void>
}
// 全局定义一个 store 来存储正在刷新的 promise
interface RefreshTokenGlobalStore {
initialed: boolean
// 若存在 promise 则意味着正在刷新 token,后续的请求需要等待 token 刷新后再真正执行
// 若不存在则意味着 token 在有效期内
refreshPromise: Promise<any> | null
}
// 全局 store key,参考了 vue InjectionKey
const globalStoreKey: MiddlewareStoreKey<RefreshTokenGlobalStore> = Symbol('refreshToken')
const { init: initGlobalStore, get: getGlobalStore } =
MiddlewareHelper.createGlobalStore(globalStoreKey)
// 初始化函数,保证仅初始化一次
function init() {
if (getGlobalStore()?.initialed) return
initGlobalStore({
initialed: true,
refreshPromise: null,
})
}
// 贯通 setup 和 handler 的 store,用来存储一些数据
interface RefreshTokenStore {
options: RequestRefreshTokenOptions
}
const storeKey: MiddlewareStoreKey<RefreshTokenStore> = Symbol('refreshToken')
export function RequestRefreshToken(options: RequestRefreshTokenOptions) {
// 注册时初始化,推荐全局注册 RefreshToken 插件
init()
// 定义中间件对象
const middleware: RequestMiddleware = {
// 定义优先级,这里因为要修改 fetcher,所以需要比同样修改 fetcher 的优先级要高,例如 RequestRetry
priority: 1000,
// useRequest 时调用,初始化配置项,当然这里也可以全部在 handler 内部实现
setup: (ctx) => {
MiddlewareHelper.initStore(storeKey, ctx, {
options: assign(
{ allow: () => true } as Partial<RequestRefreshTokenOptions>,
options,
pick(ctx.getOptions().refreshToken || {}, ['allow']),
),
})
},
handler: (ctx, next) => {
const globalStore = getGlobalStore()!
const { options } = MiddlewareHelper.getStore(storeKey, ctx)!
const { fetcher, getKey, getOptions } = ctx
// 修改 fetcher 执行方式
ctx.fetcher = async (...args) => {
// 正常执行
if (globalStore.refreshPromise == null || !toValue(options.allow, getKey(), ctx))
return fetcher(...args)
// 如果存在正在刷新 token 的请求则等待其结束后再调用
if (globalStore.refreshPromise)
return Promise.resolve(globalStore.refreshPromise).then(() => fetcher(...args))
try {
// 正常调用 fetcher,之后再通过 dataParser 解析数据,验证请求是否失败
const data = await fetcher(...args)
await getOptions().dataParser(data)
} catch (err: unknown) {
// 请求失败后验证根据当前的错误验证是否是 token 过期导致的
const error = ensureError(err)
const isExpired = await options.expired(error)
// 如果不是过期导致则返回异常错误
if (!isExpired) return Promise.reject(error)
// 如果是过期导致的则调用注册时传入的 handler 进行处理
if (!globalStore.refreshPromise) globalStore.refreshPromise = options.handler(ctx)
return Promise.resolve(globalStore.refreshPromise)
// 刷新成功后重新执行请求
// 新的请求出错则抛出新请求的错误
.then(() => Promise.resolve(fetcher(...args)).catch((e) => Promise.reject(e)))
// 异常时返回初次执行请求时的错误
.catch(() => Promise.reject(error))
.finally(() => {
// 结束后清空 promise
globalStore.refreshPromise = null
})
}
}
// 调用下一个中间件
return next()
},
}
return middleware
}
// 扩展 request 类型
declare module '@rhao/request' {
// 为 options 自定义配置项提供类型
interface RequestCustomOptions<TData, TParams extends unknown[] = unknown[]> {
refreshToken?: Pick<RequestRefreshTokenOptions, 'allow'>
}
}
最后
上述则是对 @rhao/request 的基础使用方式了,目前还比较粗糙,若大家还有什么想法或者意见的话可以评论区留言,也可以参与共建?。

相关链接
- 源码仓库
- @rhao/request 基于中间件的请求管理库
- @rhao/request-middleware-vue 适配
Vue
的中间件 - @rhao/request-middleware-axios 适配
Axios
的中间件 - @rhao/request-basic-middleware 包含常用功能(刷新
Token
、轮询、错误重试、防抖、节流、SWR
等)的中间件
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END