插件化机制优雅封装你的hook请求使用方式

目录
  • 引言
  • useRequest 简介
  • 架构
  • useRequest 入口处理
  • Fetch 和 Plugins
    • state 以及 setState
    • 插件化机制的实现
    • 核心方法 —— runAsync
    • 请求前 —— onBefore
    • 进行请求——onRequest
    • 取消请求 —— onCancel
    • 最后结果处理——onSuccess/onError/onFinally
  • 思考与总结

引言

本文是深入浅出 ahooks 源码系列文章的第二篇,这个系列的目标主要有以下几点:

  • 加深对 React hooks 的理解。
  • 学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。
  • 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。

注:本系列对 ahooks 的源码解析是基于 v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见 详情

系列文章:大家都能看得懂的源码(一)ahooks 整体架构篇

本文来讲下 ahooks 的核心 hook —— useRequest。

useRequest 简介

根据官方文档的介绍,useRequest 是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 useRequest 就够了。

useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

  • 自动请求/手动请求
  • 轮询
  • 防抖
  • 节流
  • 屏幕聚焦重新请求
  • 错误重试
  • loading delay
  • SWR(stale-while-revalidate)
  • 缓存

这里可以看到 useRequest 的功能是非常强大的,如果让你来实现,你会如何实现?也可以从介绍中看到官方的答案——插件化机制。

架构

如上图所示,我把整个 useRequest 分成了几个模块。

  • 入口 useRequest。它负责的是初始化处理数据以及将结果返回。
  • Fetch。是整个 useRequest 的核心代码,它处理了整个请求的生命周期。
  • plugin。在 Fetch 中,会通过插件化机制在不同的时机触发不同的插件方法,拓展 useRequest 的功能特性。
  • utils 和 types.ts。提供工具方法以及类型定义。

useRequest 入口处理

先从入口文件开始,packages/hooks/src/useRequest/src/useRequest.ts

function useRequest<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options?: Options<TData, TParams>,
  plugins?: Plugin<TData, TParams>[],
) {
  return useRequestImplement<TData, TParams>(service, options, [
    // 插件列表,用来拓展功能,一般用户不使用。文档中没有看到暴露 API
    ...(plugins || []),
    useDebouncePlugin,
    useLoadingDelayPlugin,
    usePollingPlugin,
    useRefreshOnWindowFocusPlugin,
    useThrottlePlugin,
    useAutoRunPlugin,
    useCachePlugin,
    useRetryPlugin,
  ] as Plugin<TData, TParams>[]);
}
export default useRequest;

这里第一(service 请求实例)第二个参数(配置选项),我们比较熟悉,第三个参数文档中没有提及,其实就是插件列表,用户可以自定义插件拓展功能。

可以看到返回了 useRequestImplement 方法。主要是对 Fetch 类进行实例化。

const update = useUpdate();
// 保证请求实例都不会发生改变
const fetchInstance = useCreation(() => {
  // 目前只有 useAutoRunPlugin 这个 plugin 有这个方法
  // 初始化状态,返回 { loading: xxx },代表是否 loading
  const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
  // 返回请求实例
  return new Fetch<TData, TParams>(
    serviceRef,
    fetchOptions,
    // 可以 useRequestImplement 组件
    update,
    Object.assign({}, ...initState),
  );
}, []);
fetchInstance.options = fetchOptions;
// run all plugins hooks
// 执行所有的 plugin,拓展能力,每个 plugin 中都返回的方法,可以在特定时机执行
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));

实例化的时候,传参依次为请求实例,options 选项,父组件的更新函数,初始状态值。

这里需要非常留意的一点是最后一行,它执行了所有的 plugins 插件,传入的是 fetchInstance 实例以及 options 选项,返回的结果赋值给 fetchInstance 实例的 pluginImpls

另外这个文件做的就是将结果返回给开发者了,这点不细说。

Fetch 和 Plugins

接下来最核心的源码部分 —— Fetch 类。其代码不多,算是非常精简,先简化一下:

export default class Fetch<TData, TParams extends any[]> {
  // 插件执行后返回的方法列表
  pluginImpls: PluginReturn<TData, TParams>[];
  count: number = 0;
  // 几个重要的返回值
  state: FetchState<TData, TParams> = {
    loading: false,
    params: undefined,
    data: undefined,
    error: undefined,
  };
  constructor(
    // React.MutableRefObject —— useRef创建的类型,可以修改
    public serviceRef: MutableRefObject<Service<TData, TParams>>,
    public options: Options<TData, TParams>,
    // 订阅-更新函数
    public subscribe: Subscribe,
    // 初始值
    public initState: Partial<FetchState<TData, TParams>> = {},
  ) {
    this.state = {
      ...this.state,
      loading: !options.manual, // 非手动,就loading
      ...initState,
    };
  }
  // 更新状态
  setState(s: Partial<FetchState<TData, TParams>> = {}) {
    this.state = {
      ...this.state,
      ...s,
    };
    this.subscribe();
  }
  // 执行插件中的某个事件(event),rest 为参数传入
  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    // 省略代码...
  }
  // 如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 或者 runAsync 来触发执行。
  // runAsync 是一个返回 Promise 的异步函数,如果使用 runAsync 来调用,则意味着你需要自己捕获异常。
  async runAsync(...params: TParams): Promise<TData> {
    // 省略代码...
  }
  // run 是一个普通的同步函数,其内部也是调用了 runAsync 方法
  run(...params: TParams) {
    // 省略代码...
  }
  // 取消当前正在进行的请求
  cancel() {
    // 省略代码...
  }
  // 使用上一次的 params,重新调用 run
  refresh() {
    // 省略代码...
  }
  // 使用上一次的 params,重新调用 runAsync
  refreshAsync() {
    // 省略代码...
  }
  // 修改 data。参数可以为函数,也可以是一个值
  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
    // 省略代码...
}

state 以及 setState

在 constructor 中,主要是进行了数据的初始化。其中维护的数据主要包含一下几个重要的数据以及通过 setState 方法设置数据,设置完成通过 subscribe 调用通知 useRequestImplement 组件重新渲染,从而获取最新值。

// 几个重要的返回值
state: FetchState<TData, TParams> = {
  loading: false,
  params: undefined,
  data: undefined,
  error: undefined,
};
// 更新状态
setState(s: Partial<FetchState<TData, TParams>> = {}) {
  this.state = {
    ...this.state,
    ...s,
  };
  this.subscribe();
}

插件化机制的实现

上文有提到所有的插件运行的结果都赋值给 pluginImpls。它的类型定义如下:

export interface PluginReturn<TData, TParams extends any[]> {
  onBefore?: (params: TParams) =>
    | ({
        stopNow?: boolean;
        returnNow?: boolean;
      } & Partial<FetchState<TData, TParams>>)
    | void;
  onRequest?: (
    service: Service<TData, TParams>,
    params: TParams,
  ) => {
    servicePromise?: Promise<TData>;
  };
  onSuccess?: (data: TData, params: TParams) => void;
  onError?: (e: Error, params: TParams) => void;
  onFinally?: (params: TParams, data?: TData, e?: Error) => void;
  onCancel?: () => void;
  onMutate?: (data: TData) => void;
}

除了最后一个 onMutate 之外,可以看到返回的方法都是在一个请求的生命周期中的。一个请求从开始到结束,如下图所示:

如果你比较仔细,你会发现基本所有的插件功能都是在一个请求的一个或者多个阶段中实现的,也就是说我们只需要在请求的相应阶段,执行我们的插件的逻辑,就能完成我们插件的功能

执行特定阶段插件方法的函数为 runPluginHandler,其 event 入参就是上面 PluginReturn key 值。

// 执行插件中的某个事件(event),rest 为参数传入
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
  // @ts-ignore
  const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
  return Object.assign({}, ...r);
}

通过这样的方式,Fetch 类的代码会变得非常的精简,只需要完成整体流程的功能,所有额外的功能(比如重试、轮询等等)都交给插件去实现。这么做的优点:

  • 符合职责单一原则。一个 Plugin 只做一件事,相互之间不相关。整体的可维护性更高,并且拥有更好的可测试性。
  • 符合深模块的软件设计理念。其认为最好的模块提供了强大的功能,又有着简单的接口。试想每个模块由一个长方形表示,如下图,长方形的面积大小和模块实现的功能多少成比例。顶部边代表模块的接口,边的长度代表它的复杂度。最好的模块是深的:他们有很多功能隐藏在简单的接口后。深模块是好的抽象,因为它只把自己内部的一小部分复杂度暴露给了用户。

核心方法 —— runAsync

可以看到 runAsync 是运行请求的最核心方法,其他的方法比如 run/refresh/refreshAsync 最终都是调用该方法。

并且该方法中就可以看到整体请求的生命周期的处理。这跟上面插件返回的方法设计是保持一致的。

请求前 —— onBefore

处理请求前的状态,并执行 Plugins 返回的 onBefore 方法,并根据返回值执行相应的逻辑。比如,useCachePlugin 如果还存于新鲜时间内,则不用请求,返回 returnNow,这样就会直接返回缓存的数据。

this.count += 1;
// 主要为了 cancel 请求
const currentCount = this.count;
const {
  stopNow = false,
  returnNow = false,
  ...state
  // 先执行每个插件的前置函数
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
  return new Promise(() => {});
}
this.setState({
  // 开始 loading
  loading: true,
  // 请求参数
  params,
  ...state,
});
// return now
// 立即返回,跟缓存策略有关
if (returnNow) {
  return Promise.resolve(state.data);
}
// onBefore - 请求之前触发
// 假如有缓存数据,则直接返回
this.options.onBefore?.(params);

进行请求——onRequest

这个阶段只有 useCachePlugin 执行了 onRequest 方法,执行后返回 service Promise(有可能是缓存的结果),从而达到缓存 Promise 的效果。

// replace service
// 如果有 cache 的实例,则使用缓存的实例
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) {
  servicePromise = this.serviceRef.current(...params);
}
const res = await servicePromise;

useCachePlugin 返回的 onRequest 方法:

// 请求阶段
onRequest: (service, args) => {
  // 看 promise 有没有缓存
  let servicePromise = cachePromise.getCachePromise(cacheKey);
  // If has servicePromise, and is not trigger by self, then use it
  // 如果有servicePromise,并且不是自己触发的,那么就使用它
  if (servicePromise && servicePromise !== currentPromiseRef.current) {
    return { servicePromise };
  }
  servicePromise = service(...args);
  currentPromiseRef.current = servicePromise;
  // 设置 promise 缓存
  cachePromise.setCachePromise(cacheKey, servicePromise);
  return { servicePromise };
},

取消请求 —— onCancel

刚刚在请求开始前定义了 currentCount 变量,其实为了 cancel 请求。

this.count += 1;
// 主要为了 cancel 请求
const currentCount = this.count;

在请求过程中,开发者可以调用 Fetch 的 cancel 方法:

// 取消当前正在进行的请求
cancel() {
  // 设置 + 1,在执行 runAsync 的时候,就会发现 currentCount !== this.count,从而达到取消请求的目的
  this.count += 1;
  this.setState({
    loading: false,
  });
  // 执行 plugin 中所有的 onCancel 方法
  this.runPluginHandler('onCancel');
}

这个时候,currentCount !== this.count,就会返回空数据。

// 假如不是同一个请求,则返回空的 promise
if (currentCount !== this.count) {
  // prevent run.then when request is canceled
  return new Promise(() => {});
}

最后结果处理——onSuccess/onError/onFinally

这部分也就比较简单了,通过 try...catch...最后成功,就直接在 try 末尾加上 onSuccess 的逻辑,失败在 catch 末尾加上 onError 的逻辑,两者都加上 onFinally 的逻辑。

try {
  const res = await servicePromise;
  // 省略代码...
  this.options.onSuccess?.(res, params);
  // plugin 中 onSuccess 事件
  this.runPluginHandler('onSuccess', res, params);
  // service 执行完成时触发
  this.options.onFinally?.(params, res, undefined);
  if (currentCount === this.count) {
    // plugin 中 onFinally 事件
    this.runPluginHandler('onFinally', params, res, undefined);
  }
  return res;
  // 捕获报错
} catch (error) {
  // 省略代码...
  // service reject 时触发
  this.options.onError?.(error, params);
  // 执行 plugin 中的 onError 事件
  this.runPluginHandler('onError', error, params);
  // service 执行完成时触发
  this.options.onFinally?.(params, undefined, error);
  if (currentCount === this.count) {
    // plugin 中 onFinally 事件
    this.runPluginHandler('onFinally', params, undefined, error);
  }
  // 抛出错误。
  // 让外部捕获感知错误
  throw error;
}

思考与总结

useRequest 是 ahooks 最核心的功能之一,它的功能非常丰富,但核心代码(Fetch 类)相对简单,这得益于它的插件化机制,把特定功能交给特定的插件去实现,自己只负责主流程的设计,并暴露相应的执行时机即可。

这对于我们平时的组件/hook 封装很有帮助,我们对一个复杂功能的抽象,可以尽可能保证对外接口简单。内部实现需要遵循单一职责的原则,通过类似插件化的机制,细化拆分组件,从而提升组件可维护性、可测试性。

参考

软件设计之Deep Module(深模块)

精读 ahooks useRequest 源码

以上就是插件化机制优雅封装你的请求hook使用方式的详细内容,更多关于插件化封装请求hook的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue3中的hooks总结

    目录 vue3的hooks总结 计数器的hook vue3自定义hooks vue3的hooks总结 vue3中的hooks其实是函数的写法,就是将文件的一些单独功能的js代码进行抽离出来,放到单独的js文件中.这样其实和我们在vue2中学的mixin比较像.下面我们总结一下如何去书写hooks. 首先应该先建立一个hooks文件夹:其目的是为了存放hook文件. 建立相关的hook文件:一般使用use开头. 计数器的hook useTitle的hooks useScrollPostion用来监

  • vue3自定义hooks/可组合函数方式

    目录 自定义hooks/可组合函数 1.mixins方式的痛点 2.传统mixins方式示例 3.自定义hooks方式示例 vue3(hooks) 自定义hooks/可组合函数 vue3 composition api下mixins的替代方式(自定义hooks / 可组合函数) 传统方式下封装的mixins在引入文件里都是通过this来调用属性或方法, 而在vue3的composition API下this是undefined,实际上这两者就是冲突的, 只能封装一套全新的方式来使用类似mixin

  • vue3中的hook简单封装

    目录 vue3的hook封装 vue3的hooks总结 下面总结一下如何去书写hooks 计数器的hook vue3的hook封装 vue3最新鲜的就是组合式API了,通过组合式API我们可以把一些复杂的逻辑或一些常用的逻辑封装成一个个hook来进行调用,这样的方式也更易于维护. 使用 import useTest from "../../hooks/useTest"; export default defineComponent({   name: "vue3Test&qu

  • node使用async_hooks模块进行请求追踪

    async_hooks 模块是在 v8.0.0 版本正式加入 Node.js 的实验性 API.我们也是在 v8.x.x 版本下投入生产环境进行使用. 那么什么是 async_hooks 呢? async_hooks 提供了追踪异步资源的 API,这种异步资源是具有关联回调的对象. 简而言之,async_hooks 模块可以用来追踪异步回调.那么如何使用这种追踪能力,使用的过程中又有什么问题呢? 认识 async_hooks v8.x.x 版本下的 async_hooks 主要有两部分组成,一个

  • 详解如何使用React Hooks请求数据并渲染

    前言 在日常的开发中,从服务器端异步获取数据并渲染是相当高频的操作.在以往使用React Class组件的时候,这种操作我们已经很熟悉了,即在Class组件的componentDidMount中通过ajax来获取数据并setState,触发组件更新. 随着Hook的到来,我们可以在一些场景中使用Hook的写法来替代Class的写法.但是Hook中没有setState.componentDidMount等函数,又如何做到从服务器端异步获取数据并渲染呢?本文将会介绍如何使用React的新特性Hook

  • 插件化机制优雅封装你的hook请求使用方式

    目录 引言 useRequest 简介 架构 useRequest 入口处理 Fetch 和 Plugins state 以及 setState 插件化机制的实现 核心方法 —— runAsync 请求前 —— onBefore 进行请求——onRequest 取消请求 —— onCancel 最后结果处理——onSuccess/onError/onFinally 思考与总结 引言 本文是深入浅出 ahooks 源码系列文章的第二篇,这个系列的目标主要有以下几点: 加深对 React hooks

  • C++插件化 NDD源码的插件机制实现解析

    目录 NDD介绍 插件的优势 NDD插件机制分析 插件接口 插件实现 NDD插件加载过程 插件机制是一种框架,允许开发人员简单地在应用程序中添加或扩展功能.它使广泛使用,因为它可以作为模块被重复使用,并使它们更易于维护和扩展,因此它们在应用程序中非常有用.插件机制允许管理员在需要时轻松安装和卸载插件,而无需对基础应用程序做出更改. NDD介绍 这里再介绍推荐下优秀的国产软件开源项目 NDD(notepad--).一个支持windows/linux/mac的文本编辑器,目标是要国产替换同类软件.对

  • js轮播图的插件化封装详解

    本文实例为大家分享了js轮播图的插件化封装代码,供大家参考,具体内容如下 具体代码如下: ~function(){ function AutoBanner(curEleId,ajaxURL,interval){ //把之前存储获取元素的变量都作为当前实例的私有属性 this.banner = document.getElementById(curEleId); this.bannerInner = utils.firstChild(this.banner); this.bannerTip = u

  • 浅谈Android插件化

    目录 一.认识插件化 1.1 插件化起源 1.2 插件化优点 1.3 与组件化的区别 二.插件化的技术难点 三.ClassLoader Injection 3.1 java 中的 ClassLoader 3.2 android 中的 ClassLoader 3.3 双亲委派机制 3.4 如何加载插件中的类 3.5 执行插件类的方法 四.Runtime Container 4.1 为什么没有注册的 Activity 不能和系统交互 4.2 运行时容器技术 4.3 字节码替换 五.Resource

  • Android热修复及插件化原理示例详解

    目录 1.前言 2.类加载机制 3.Android类加载 4.Tinker原理 代码实现 5.插件化 5.1 Activity启动流程简单介绍 5.2 插件化原理 5.2.1 绕开验证 5.2.2还原插件Activity 5.3 加载插件资源 5.3.1 Resources&AssetManager 5.3.2 id冲突 1.前言 热修复一直是这几年来很热门的话题,主流方案大致有两种,一种是微信Tinker的dex文件替换,另一种是阿里的Native层的方法替换.这里重点介绍Tinker的大致原

  • 详解开源的JavaScript插件化框架MinimaJS

    本文介绍我开发的一个JavaScript编写的插件化框架--MinimaJS,完全开源,源码下载地址:https://github.com/lorry2018/minimajs.该框架参考OSGi规范,将该规范定义的三大插件化功能在Node上实现了.MinimaJS三个功能:动态插件化,服务和扩展.该框架基于VSCode开发.使用ES6编码,基于Node 8开发,代码量几千行,非常的简单.优雅.轻量.框架的代码结构划分清晰,命名优雅. 我们先简单看一下,如何来使用这个框架. 通过这几行代码就可以

  • Android插件化之资源动态加载

    Android插件化之资源动态加载 一.概述 Android插件化的一个重要问题就是插件资源访问问题,先列出会面对的问题 1.如何加载插件资源 2.如何处理插件资源与宿主资源的处突:插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找.这样能做到动态更新的效果. 3.如何确保插件和宿主使用到的是被修改过的资源. 二.原理分析 在做一件事之前必须先弄清楚原理,所以,这里先要弄清楚Android的资源体系原理. 1.资源链 Contex

  • JavaScript插件化开发教程 (二)

    一,开篇分析 Hi,大家好!还记得前面的那篇文章吗------这个系列的开篇(JavaScript插件化开发教程一).主要讲述了以"jQuery的方式如何开发插件", 那么今天我们带着昨天的疑问来继续我们的插件开发之旅.之前的问题如下: (1),如果项目技术选型换了这些插件又是强依赖"jQuery"机制,我们以前写的插件将会不能用(假设不用jQuery的情况),如何做重构那? (2),重构插件的关键逻辑,我们将如何组织那? 好了,带着问题去学习今天的文章吧. 首先我

  • 亲自动手实现Android App插件化

    Android插件化目前国内已经有很多开源的工程了,不过如果不实际开发一遍,很难掌握的很好. 下面是自己从0开始,结合目前开源的项目和博客,动手开发插件化方案. 按照需要插件化主要解决下面的几种问题: 1. 代码的加载 (1) 要解决纯Java代码的加载 (2) Android组件加载,如Activity.Service.Broadcast Receiver.ContentProvider,因为它们是有生命周期的,所以要特殊处理 (3) Android Native代码的加载 (4) Andro

  • jQuery插件原来如此简单 jQuery插件的机制及实战

    jQuery插件的种类 1.封装对象方法 这种插件是将对象方法封装起来,用于对通过选择器获取的jQuery对象进行操作,是最常见的一种插件.此类插件可以发挥出jQuery选择器的强大优势,有相当一部分的jQuery的方法,都是在jQuery脚本库内部通过这种形式"插"在内核上的,例如parent()方法,appendTo()方法等. 2.封装全局函数 可以将独立的函数加到jQuery命名空间下.如常用的jQuery.ajax()方法.去首尾空格的jQuery.trim()方法,都是jQ

随机推荐