我为什么要搓一个useRequest
背景
- 在日常开发网络请求过程中,为了维护loading和error状态开发大量重复代码
- 对于竞态问题,要么不处理,要么每个需要请求的地方都要写重复逻辑
- 图表接口数据量大,甚至单接口响应就足以达到数十兆字节,而一个页面有数十个这样的请求,响应时间长,需要能够取消网络请求
以上逻辑,每个人的解法各不相同。为了解决上述问题,统一处理逻辑,需要一个能够统一管理网络请求状态的工具。
调研
首先想到的当然不是自己搓轮子,而是在社区上寻找是否已有解决方案。果不其然,找到了一些方案。
对于React,有像react-query这样的老前辈,功能全面,大而重;有像SWR这样的中流砥柱,受到社区广泛追捧;有像ahooks的useRequest这样的小清新,功能够用,小而美。
而对于Vue,一开始还真没让我找到类似的解决方案,后续进一步查找,发现有一个外国哥们仿造react-qeury仿写了一个vue-query,同时了解到雷达团队正是用的这一套解决方案,便又更深入了解了一下,发现这个库已经不维护了......进而了解到@tanstack/query,好家伙,这玩意胃口大得把react-query和vue-query都吃进去了,甚至svelte也不放过。继续找,发现有个哥们写了一个vue-request库,差不多类似于ahooks的useRequest,不错。然后经典的vue-use库也看了下,有一个useFetch方法,比较鸡肋,只适用于Fetch请求。
上述的社区库都相当不错,但对于我来说都太重了,功能繁多,而且在使用上,几个query都需要花费大量心智在缓存key上,太难用了。而ahooks和vue-request提供的useRequest的高阶函数,是比较符合我的胃口的,但是我还是嫌他们功能太多了。最关键的是,上述所有方案都没有达到我最主要的目的,能够真正取消网络请求。
因此,自己动手,丰衣足食。
动手
说干就干,搓一个咱自己的useRequest。
首先,定义useRequest的接口:
export declare const useRequest: <P extends unknown[], R>(request: (signal: AbortSignal, ...args: P) => Promise<R>, options?: IUseRequestOptions<R> | undefined) => {
result: ShallowRef<R | null>;
loading: ShallowRef<boolean>;
error: ShallowRef<Error | null>;
run: (...args: P) => Promise<Error | R>;
forceRun: (...args: P) => Promise<Error | R>;
cancel: () => boolean;
};
然后定义三个响应式状态,这里之所以用shallowRef,是考虑到部分请求结果可能很深,如果用ref会导致性能很差。
const result = shallowRef<IResult | null>(null);
const loading = shallowRef(false);
const error = shallowRef<Error | null>(null);
定义普通变量,在useRequest内部使用,不要在内部实现读取响应式变量(PS:踩过坑了,有个页面用watchEffect,loading状态一变就发请求,导致无线循环):
let abortController = new AbortController();
let isFetching = false;
然后定义run函数,如果有进行中的请求就取消掉:
const run = async (...args: IParams) => {
if (mergedOptions.cancelLastRequest && isFetching) {
cancel();
}
abortController = new AbortController();
setLoadingState(true);
const res = await runRequest(...args);
return res;
};
const runRequest = async (...args: IParams) => {
const currentAbortController = abortController;
try {
const res = await request(currentAbortController.signal, ...args);
if (currentAbortController.signal.aborted) {
return new Error('canceled');
}
handleSuccess(res);
return res;
} catch (error) {
if (currentAbortController.signal.aborted) {
return new Error('canceled');
}
handleError(error as Error);
return error as Error;
}
};
另外暴露出cancel方法:
const cancel = () => {
if (isFetching) {
mergedOptions.onCancel?.();
setLoadingState(false);
abortController.abort('cancel request');
return true;
}
return false;
};
在组件卸载时也取消掉未完成的请求:
onScopeDispose(() => {
if (mergedOptions.cancelOnDispose && isFetching) {
cancel();
}
});
以上,就是最基础版的useRequest实现,想要了解更多,欢迎直接阅读useRequest源码,核心代码一共也就一百来行。看完再把star一点,诶嘿,美滋滋。
产出
收益
业务贡献
- 提供响应式的result、loading、error状态
- 内置缓存逻辑
- 内置错误重试逻辑
- 内置竞态处理逻辑
- 兼容 Vue 2 & 3
- 兼容 Axios & Fetch
- 取消网络请求
个人成长
- 学会如何编写一个基本的Vue工具库
- 了解如何用vite打包,并且带上类型文件
- 学会如何使用vue-demi兼容Vue2 & Vue3
- 学会如何用VuePress编写文档,过程中没少看源码
- 学会如何在npm上发包并维护
- 之前用jest写过测试,这次尝试了一下vitest,体感不错,过程中暴露不少代码问题
- 通过这个项目将以往所学的部分知识串联起来
参考
来源:juejin.cn/post/7293786784126255131