注册
web

我为什么要搓一个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

0 个评论

要回复文章请先登录注册