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

typescript 复制代码
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会导致性能很差。

ini 复制代码
const result = shallowRef<IResult | null>(null);
const loading = shallowRef(false);
const error = shallowRef<Error | null>(null);

定义普通变量,在useRequest内部使用,不要在内部实现读取响应式变量(PS:踩过坑了,有个页面用watchEffect,loading状态一变就发请求,导致无线循环):

ini 复制代码
let abortController = new AbortController();
let isFetching = false;

然后定义run函数,如果有进行中的请求就取消掉:

vbnet 复制代码
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方法:

kotlin 复制代码
const cancel = () => {
  if (isFetching) {
    mergedOptions.onCancel?.();
    setLoadingState(false);
    abortController.abort('cancel request');
    return true;
  }

  return false;
};

在组件卸载时也取消掉未完成的请求:

scss 复制代码
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,体感不错,过程中暴露不少代码问题

  • 通过这个项目将以往所学的部分知识串联起来

参考

相关推荐
RainbowSea18 分钟前
NVM 切换 Node 版本工具的超详细安装说明
java·前端
读书点滴23 分钟前
笨方法学python -练习14
java·前端·python
Mintopia30 分钟前
四叉树:二维空间的 “智能分区管理员”
前端·javascript·计算机图形学
Mintopia41 分钟前
Three.js 深度冲突:当像素在 Z 轴上玩起 "挤地铁" 游戏
前端·javascript·three.js
Penk是个码农1 小时前
web前端面试-- MVC、MVP、MVVM 架构模式对比
前端·面试·mvc
MrSkye1 小时前
🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥
前端·javascript·面试
白瓷梅子汤1 小时前
跟着官方示例学习 @tanStack-form --- Linked Fields
前端·react.js
爱学习的茄子1 小时前
深入理解JavaScript闭包:从入门到精通的实战指南
前端·javascript·面试
zhanshuo1 小时前
不依赖框架,如何用 JS 实现一个完整的前端路由系统
前端·javascript·html
火柴盒zhang1 小时前
websheet在线电子表格(spreadsheet)在集团型企业财务报表中的应用
前端·html·报表·合并·spreadsheet·websheet·集团财务