前言
大家好,我是 Ray,一个前端开发者,同时也是 vue-hooks-plus 库的成员。今天,我想和大家分享一下如何基于该库的 useRequest 方法学习如何插件化设计。
什么是插件化
插件化,就是将一个功能模块化,然后通过插件的形式进行注入。然后通过插件的形式进行调用,使得在主功能不变的情况下,通过插件的形式进行功能扩展。并且插件化可以使得主功能更加简洁,更加容易维护。
useRequest 的基础设计
这是 useRequest 的基础设计,通过这个设计,我们可以看到,useRequest 的运行流程是:
graph TD
A[开始] --> B[传入参数]
B --> C1[service 方法]
B --> C2[options 配置项]
B --> C3[plugins 自定义插件]
C2 --> D[合并配置项]
C3 --> E[合并插件]
D & E --> F[运行 run/runAsync 方法]
F --> G[调用 service 方法]
G --> H[运行插件系统]
H --> I[结束]
代码实现
类型定义
// types.ts
/**
* 异步服务函数类型
* @template TData 返回数据类型
* @template TParams 参数类型数组
*/
export type Service<TData, TParams extends any[] = any[]> = (
...args: TParams
) => Promise<TData>
/**
* useRequest 返回值类型
* @template TData 返回数据类型
* @template TParams 参数类型数组
*/
export interface Result<TData, TParams extends any[] = any[]> {
loading: Ref<boolean> // 加载状态
data: Ref<TData | undefined> // 请求数据
error: Ref<Error | undefined> // 错误信息
params: Ref<TParams | []> // 当前请求参数
run: (...params: TParams) => Promise<TData> // 触发请求方法
runAsync: (...params: TParams) => Promise<TData> // 异步请求方法
refresh: () => Promise<TData> // 使用当前参数重新请求
cancel: () => void // 取消当前请求
}
/**
* 配置选项类型
* @template TData 返回数据类型
* @template TParams 参数类型数组
*/
export interface Options<TData, TParams extends any[] = any[]> {
manual?: boolean // 是否手动触发
defaultParams?: TParams // 默认参数
pollingInterval?: number // 轮询间隔(ms)
cancelOnUnmount?: boolean // 组件卸载时是否取消请求
onBefore?: (params: TParams) => void // 请求前回调
onSuccess?: (data: TData, params: TParams) => void // 请求成功回调
onError?: (error: Error, params: TParams) => void // 请求失败回调
onFinally?: (params: TParams, data?: TData, error?: Error) => void // 请求完成回调
}
/**
* 插件返回的状态类型
*/
export interface PluginState<TData, TParams extends any[] = any[]> {
loading?: boolean
data?: TData
error?: Error
params?: TParams
}
/**
* 插件返回值类型
* @template TData 返回数据类型
* @template TParams 参数类型数组
*/
export interface PluginReturn<TData, TParams extends any[] = any[]> {
onBefore?: (params: TParams) => void | PluginState<TData, TParams>
onRequest?: (
service: Service<TData, TParams>,
params: TParams,
) => Promise<TData>
onSuccess?: (data: TData, params: TParams) => void
onError?: (error: Error, params: TParams) => void
onFinally?: (params: TParams, data?: TData, error?: Error) => void
onCancel?: () => void
}
/**
* 插件类型
* @template TData 返回数据类型
* @template TParams 参数类型数组
*/
export type Plugin<TData, TParams extends any[] = any[]> = {
(instance: Result<TData, TParams>, options: Options<TData, TParams>): Partial<
PluginReturn<TData, TParams>
>
}
方法实现
// useRequest.ts
import { ref, onUnmounted, type Ref } from 'vue'
import type {
Service,
Options,
Plugin,
Result,
PluginReturn,
PluginState,
} from './types'
/**
* useRequest Hook
* @template TData 返回数据类型
* @template TParams 参数类型数组
* @param service 异步服务函数
* @param options 配置选项
* @param plugins 插件数组
* @returns Result 对象,包含状态和方法
*/
export function useRequest<TData, TParams extends any[] = any[]>(
service: Service<TData, TParams>,
options: Options<TData, TParams> = {},
plugins: Plugin<TData, TParams>[] = [],
): Readonly<Result<TData, TParams>> {
// 解构配置项,设置默认值
const {
manual = false,
defaultParams = [] as unknown as TParams,
cancelOnUnmount = true,
pollingInterval,
onBefore,
onSuccess,
onError,
onFinally,
} = options
// 创建响应式状态
const loading = ref<boolean>(false)
const data = ref<TData | undefined>()
const error = ref<Error | undefined>()
const params = ref<TParams | []>(defaultParams)
// 内部控制变量
let count = 0
let pollingTimer: NodeJS.Timeout | undefined
/**
* 初始化插件实例
* 传入当前实例和配置项
*/
const pluginImpls = plugins.map((p) =>
p(
{
loading,
data,
error,
params,
run,
runAsync,
refresh,
cancel,
},
options,
),
)
/**
* 取消当前请求
* 清除轮询定时器并触发插件的 onCancel 钩子
*/
function cancel(): void {
if (pollingTimer) {
clearTimeout(pollingTimer)
pollingTimer = undefined
}
count += 1
pluginImpls.forEach((plugin) => plugin.onCancel?.())
}
/**
* 使用当前参数重新发起请求
* @returns Promise<TData>
*/
async function refresh(): Promise<TData> {
return run(...(params.value as TParams))
}
/**
* 核心请求方法
* @param args 请求参数
* @returns Promise<TData>
*/
async function run(...args: TParams): Promise<TData> {
cancel()
const currentCount = count
params.value = args
loading.value = true
error.value = undefined
try {
// 执行 options 的 onBefore 回调
onBefore?.(args)
// 执行插件的 onBefore 钩子
for (const plugin of pluginImpls) {
if (plugin.onBefore) {
const newState = plugin.onBefore(args)
if (newState) {
// 更新状态
if ('loading' in newState) loading.value = newState.loading!
if ('data' in newState) data.value = newState.data
if ('error' in newState) error.value = newState.error
if ('params' in newState) params.value = newState.params!
}
}
}
// 执行请求
let serviceResult: TData
// 通过插件链处理请求
const servicePromise = (async () => {
for (const plugin of pluginImpls) {
if (plugin.onRequest) {
return await plugin.onRequest(service, args)
}
}
return await service(...args)
})()
serviceResult = await servicePromise
// 如果不是当前请求,直接返回结果
if (currentCount !== count) {
return serviceResult
}
data.value = serviceResult
// 执行成功回调
onSuccess?.(serviceResult, args)
pluginImpls.forEach((plugin) => plugin.onSuccess?.(serviceResult, args))
// 处理轮询
if (pollingInterval && pollingInterval > 0) {
pollingTimer = setTimeout(() => {
refresh()
}, pollingInterval)
}
return serviceResult
} catch (err: any) {
// 如果不是当前请求的错误,继续抛出
if (currentCount !== count) {
throw err
}
// 确保错误类型
const errorInstance = err instanceof Error ? err : new Error(String(err))
error.value = errorInstance
// 执行错误回调
onError?.(errorInstance, args)
pluginImpls.forEach((plugin) => plugin.onError?.(errorInstance, args))
throw errorInstance
} finally {
// 只处理当前请求的 finally 逻辑
if (currentCount === count) {
loading.value = false
onFinally?.(args, data.value, error.value)
pluginImpls.forEach((plugin) =>
plugin.onFinally?.(args, data.value, error.value),
)
}
}
}
// runAsync 与 run 相同,用于语义化区分
const runAsync = run
// 组件卸载时的清理工作
if (cancelOnUnmount) {
onUnmounted(() => {
cancel()
count = 0
data.value = undefined
error.value = undefined
})
}
// 非手动模式下,自动执行首次请求
if (!manual) {
run(...defaultParams)
}
// 返回只读的结果对象
return {
loading,
data,
error,
params,
run,
runAsync,
refresh,
cancel,
} as const
}
基础使用
我们基于简化后的 useRequest 方法来实战一下。
// 定义接口返回数据类型
interface UserInfo {
id: number
name: string
email: string
}
// 缓存插件:实现数据的本地缓存
const cachePlugin: Plugin<UserInfo, [number]> = (instance, options) => ({
name: 'cachePlugin',
// 请求前检查缓存
onBefore: (params) => {
const cacheKey = `user-${params[0]}`
const cached = localStorage.getItem(cacheKey)
if (cached) {
return {
loading: false,
data: JSON.parse(cached),
}
}
},
// 请求成功后更新缓存
onSuccess: (data) => {
const cacheKey = `user-${data.id}`
localStorage.setItem(cacheKey, JSON.stringify(data))
},
})
// 自动转为大写插件:自动将 name 转为大写
const responseNameToUpperCasePlugin: Plugin<UserInfo, [number]> = (
instance,
options,
) => ({
name: 'responseNameToUpperCasePlugin',
onSuccess: (data) => {
// 将 name 转为大写
data.name = data.name.toUpperCase()
},
})
// 模拟请求返回
const mockFetch = (id: number): Promise<UserInfo> => {
return new Promise<UserInfo>((resolve) => {
setTimeout(() => {
resolve({ id, name: 'John Doe' + ' ' + id, email: 'john@example.com' })
}, 1000)
})
}
let id = 1
// 使用插件
const { data, run } = useRequest(
mockFetch,
{
pollingInterval: 3000,
defaultParams: [id],
onSuccess: (data) => {
id++
},
},
// 传入插件数组
[cachePlugin, responseNameToUpperCasePlugin],
)
这个是 useRequest 的简化版,但是已经具备了插件化的能力。如果有兴趣查看完整版,可以点击查看详细的 useRequest 实现。
最后
感谢大家的阅读,谢谢。
如果感觉 vue-hooks-plus 能够帮助到你,欢迎给个 star 支持一下。如果有什么意见或者建议,可以提一个 issues,我们会及时回复与处理。