🌍 背景
前后端分离项目中,我们很多场景需要通过异步请求获取数据,在此过程中会有很多的处理。例如请求的loading、错误捕获、防抖等。
开发过程中往往需要编写大量重复的代码来实现这些逻辑处理,最近vue3掀起了国内的浪潮,对ts兼容也不错,由于我是react转vue,使用vue的axios时候回调的问题,让我觉得不优雅和不适应,此文章来讲解如何封装一个类似于ahooks/useRequest的简单的vue3的异步请求hook来满足我们对这些场景的需求。也让大家直观的、易懂的了解这一过程。
🌟 功能
- 手动请求/自动请求/重新请求
- 条件请求/依赖请求
- 轮询/防抖/节流
- 请求数据缓存/数据保鲜
- 格式化数据/立即变更数据
- 状态间的回调
- 优雅的响应式数据
使用方式
const { data, run, loading } = useAsync(此处接收一个promise的异步请求函数,{...配置项})
useAsync第一个参数为promise的异步请求,可以是带参形式的()=>request(params)的,也可以是待传参形式的request需要配合run使用,run(params)。使用方法参考ahooks/useRequest
一、结果项
参数 | 说明 | 类型 | |
---|---|---|---|
data | service 返回的数据 | TData | undefined |
error | service 抛出的异常 | Error | undefined |
loading | service 是否正在执行 | boolean | |
params | 当次执行的 service 的参数数组 | TParams | [] |
run | 手动触发 service 执行,参数会传递给 service异常自动处理,通过 onError 反馈 | (...params: TParams) => void | |
refresh | 使用上一次的 params,重新调用 run | () => void | |
mutate | 直接修改 data | (data?: TData / ((oldData?: TData) => (TData / undefined))) => void | |
cancel | 取消当前正在进行的请求 | () => void |
二、配置项Options(基础功能)
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
manual | 默认 false 。 即在初始化时自动执行 service。如果设置为 true ,则需要手动调用 run 触发执行。 | boolean | false |
defaultParams | 首次默认执行时,传递给 service 的参数 | TParams | - |
onBefore | service 执行前触发 | (params: TParams) => void | - |
onSuccess | service resolve 时触发 | (data: TData, params: TParams) => void | - |
onError | service reject 时触发 | (e: Error, params: TParams) => void | - |
onFinally | service 执行完成时触发 | (params: TParams, data?: TData, e?: Error) => void | - |
更多的高级功能↓
📡 基本实现流程
vue3响应式的数据声明,从结果项中我们得知,不是函数类型的结果我们都应该声明一个响应式的数据。简单数据类型采用ref,复杂的数据类型如对象可以采用reactive。
一、首先我们需要有这些基础的数据
const params = ref<TParams>(defaultParams as TParams) as Ref<TParams>;
const lastSuccessParams = ref<TParams>(defaultParams as TParams) as Ref<TParams>;
const state: StateType<TData> = reactive({
data: initialData || null,
error: null,
loading: false,
});
二、其次我们取到传入的service,service都围绕着run进行的
const run = async (...args: TParams) => {
// ...逻辑
try {
// 请求更新数据
const result = await service(...args);
// 由于reactive解构会失去响应式,只能单个赋值
state.data = result;
state.loading = false;
} catch (err: Error) {
onError(err);
state.error = err;
state.loading = false;
}
};
三、return,至此完成了基本的流程
export function useAsync<TParams = any, TParams extends any[] = any>(
service: Service<TData, TParams>,
options: OptionsType<TData, TParams> = {}
){
const state: StateType<TData> = reactive({
data: initialData || null,
error: null,
loading: false,
});
const run = async (...args: TParams) => {
// ...逻辑
try {
// 请求更新数据
const result = await service(...args);
// 由于reactive解构会失去响应式,只能单个赋值
state.data = result;
state.loading = false;
} catch (err: Error) {
onError(err);
state.error = err;
state.loading = false;
}
};
return {
...toRefs(state),
run
}
}
上述代码就能够完成在vue3中基本的异步请求hook。定义好state,当开始异步请求的时候,进入run,异步请求状态变化的时候更新state状态,当有错误异常的时候也能及时的更新状态。拓展功能见↓
☄️ useAsync的拓展功能
-
manual自动执行
在Mounted进行判断,如果manual为true则发送请求。此处可以将manual传入一个ref,这样此处可以使用watchEffect进行依赖收集监听,个人认为此方案也优。
onMounted(() => {
if (!manual) run(...(defaultParams as TParams));
});
-
数据格式化
在onSuccess回调前formatResult变更数据。formatResult将data做为参数回调,由外部返回修改后的值,将onSuccess回调值改变和结果项的改变
state.data = formatResult?.(state.data); onSuccess?.(state.data);
-
防抖
创建一个防抖的函数做为run的中间件,需要经过防抖加工后再进行run。防抖debounce属于lodash
serviceRun做为一个待被处理的run
let serviceRun = run; if (debounceInterval) { const debounceRun = debounce(run, debounceInterval); serviceRun = (...args: TParams) => { state.loading = true; return Promise.resolve(debounceRun(...args)!); }; }
-
节流
创建一个节流的函数做为run的中间件,需要经过节流加工后再进行run。节流debounce属于lodash
serviceRun同上
if (throttleInterval) { const throttleRun = throttle(run, throttleInterval); serviceRun = (...args: TParams) => { return Promise.resolve(throttleRun(...args)!); }; }
-
轮询
创建一个轮询的函数做为run的中间件,需要轮询加工后再进行run。
serviceRun同上
let pollingTimer: Timer | undefined; if (pollingInterval) { serviceRun = (...args: P) => { if (pollingTimer) { clearInterval(pollingTimer); } pollingTimer = setInterval(() => { // 此处可添加轮询不处理的逻辑,如页面隐藏的时候不发送轮询,可自行定义字段处理 run(...args); }, pollingInterval); return run(...args); }; }
-
取消请求
取消定时器等逻辑,loading状态改变。
function cancel = () => { if (pollingTimer) { clearInterval(pollingTimer); } state.loading = false; }; // 可定义一个变量count记录当前请求次数,在run请求前累计当前次数和赋值一个currentCount时候赋值,相等的时候赋值。如果取消请求可以在此处将count加1,造成异步请求时候count不一致,从而达到取消请求的目的
-
重新请求
重新发送请求,并且携带上一次请求的params
function refresh() { return run(...params.value); }
-
立即变更数据-突变
提供函数从外部任意位置变更数据
function mutate(data: TData) { state.data = data; }
-
条件请求
ready传入一个ref响应式的数据,当为true的时候发送请求
watch(ready, (val) => { if (val === true) { run(...params.value); } });
-
依赖请求
refreshDeps为一个ref的数组,watch监听依赖的改变,如果发生改变,则会寻找其是否是params依赖项,如果是则会替换该params里属性的值发生请求,否则不改变的重新发送请求。
// 依赖更新数据 watch(refreshDeps, (value, prevValue) => { params.value = (params.value as any[])?.map((item) => { const obj = item; (prevValue as any[]).forEach((v, index) => { Object.keys(item).forEach((key) => { if (item[key] === v) { obj[key] = value[index]; } }); }); return { params.value, ...obj, }; }) as TParams; run(...params.value); });
-
loading延迟
loading延迟防止数据闪烁
// 使用loadingDelay if (loadingDelay !== undefined) { timerRef.value = setTimeout(() => { state.loading = true; }, loadingDelay); state.loading = false; }
-
请求数据缓存
需配合cacheKey一起使用,这将缓存请求数据,在下一次请求的时候优先使用缓存数据,并且会在背后偷偷发送数据,更新缓存。
可指定cacheTime设置缓存到期时间,到期会被清除。
const run = async (...args: TParams) => { // run前立即使用缓存 if (cacheKey) { state.data = getCache(cacheKey)?.data; } // ..... // 请求成功后更新缓存 if (cacheKey) { setCache(cacheKey, cacheTime, state.data, cloneDeep(args)); } onSuccess?.(state.data); }
cache.ts文件中
type Timer = ReturnType<typeof setTimeout>; type CachedKey = string | number; type CachedData = { data: any; params: any; timer: Timer | undefined; time: number; }; type Listener = (data: any) => void; // 缓存是一个Map对象 const cache = new Map<CachedKey, CachedData>(); const listeners: Record<string, Listener[]> = {}; // 设置缓存 const setCache = ( key: CachedKey, cacheTime: number, data: any, params: any ) => { const currentCache = cache.get(key); // 如果缓存有延时器,则清除延时器 if (currentCache?.timer) { clearTimeout(currentCache.timer); } let timer: Timer | undefined = undefined; // cacheTime为-1,不缓存 if (cacheTime > -1) { timer = setTimeout(() => { cache.delete(key); }, cacheTime); } if (listeners[key]) { listeners[key].forEach((item) => item(data)); } cache.set(key, { data, params, timer, time: new Date().getTime(), }); }; const getCache = (key: CachedKey) => { return cache.get(key); }; // 订阅模式,用于监听cache数据 const subscribe = (key: string, listener: Listener) => { if (!listeners[key]) { listeners[key] = []; } listeners[key].push(listener); return function unsubscribe() { const index = listeners[key].indexOf(listener); listeners[key].splice(index, 1); }; }; // 清除缓存 const clearCache = (key?: string | string[]) => { if (key) { const cacheKeys = Array.isArray(key) ? key : [key]; cacheKeys.forEach((cacheKey) => cache.delete(cacheKey)); } else { cache.clear(); } }; export { getCache, setCache, subscribe, clearCache };
-
并行请求
需配合fetchKey使用
fetches类似state的集合,存储相同的请求的状态(主要是同一请求参数不一致的状态收集)
const fetches = reactive< Record< string, StateType<any> & { params: any; } > >({}); // 比较简单粗暴直接取第一个参数,大家可以改进。将params回调出去,由外部传入唯一的key值 const fetchKeyPersist = fetchKey({ ...args }?.[0] ?? "default_key"); //.... //run里面请求前 // 收集同一请求不同key下的状态 if (fetchKeyPersist) { fetches[fetchKeyPersist as string] = { ...state, loading: true, params: cloneDeep(args), }; } // 请求成功的时候 if (fetchKeyPersist) { fetches[fetchKeyPersist as string] = { ...state, loading: false, params: { ...args }, }; } // 请求失败的时候 if (fetchKeyPersist) { fetches[fetchKeyPersist as string] = { ...state, data: null, params: cloneDeep(args), }; }
以上为全部高级拓展功能,类似与一个hook一样穿插在各个生命节点执行对应的逻辑。
💫 附上源码
👬 vue的好搭档axios
import axios, { AxiosRequestConfig } from "axios";
import { Toast } from "vant";
import { routers } from "../routers";
//post请求头
axios.defaults.headers.post["Content-Type"] =
"application/x-www-form-urlencoded;charset=UTF-8";
//设置超时
// axios.defaults.timeout = 10_000;
const axiosInstance = axios.create({
timeout: 10000,
});
axiosInstance.interceptors.request.use(
(config) => {
const accessToken = sessionStorage.getItem("access_token");
if (accessToken) {
return {
...config,
headers: {
...config.headers,
Authorization: accessToken ? `Bearer ${accessToken}` : "",
},
};
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(response) => {
if (response?.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
(error) => {
if (error?.message?.includes?.("timeout")) {
Toast.fail("请求超时");
} else {
Toast.fail("网络错误,请重试");
routers.push("/403");
}
Promise.reject(error);
}
);
const request = <ResponseType = unknown>(
url: string,
options?: AxiosRequestConfig<unknown>
): Promise<ResponseType> => {
return new Promise((resolve, reject) => {
axiosInstance({
url,
...options,
})
.then((res) => {
resolve(res.data.data);
})
.catch((err) => reject(err));
});
};
export { axiosInstance, request };
🌰 使用
- 返回一个promise请求请求,request为⬆️ 导出的
- 为非必填只作展示功能,可根据自身需求进行删减增和加逻辑
👩🏻💻 总结
这个hook可以帮助我们省去比较多的重复逻辑,并且难度不高,适合那些需要优雅请求和用于学习的使用,目前项目上使用也是没问题的。我们也能根据自己的场景进行添加和改进。总体还是借鉴了ahooks的思想。
🌚 不足
这是一个简易版的请求hook,尽管它简洁功能多,但弊端也比较明显,不可否认它确实能完成我们大部分的场景需求,随着后续的优化改进和加其他通用逻辑,整个hook代码会挤到一个文件里,代码繁多,乱,不利于管理。但其还是满足大部分的场景的,因此改动不会很大。
🤩 展望
useAsync提供插件式的逻辑功能拓展,彻底解决代码乱,易于管理。每个请求实例存在一个独立的实例。我们需要拓展功能,只需要自己根据规范,合理使用hook暴露出来的回调和实例进行书写即可。强大的插件式异步管理工具 🔧 开发中...
使用前言
全新的vue hooks开发完成,展望的useRequest插件式请求工具也在其中,见👇🏻链接
大家 star star 🌟🌟 ,感谢支持。