乐趣区

关于前端:如何使用插件化机制优雅的封装你的请求hook

本文是深入浅出 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 源码

本文由 mdnice 多平台公布

退出移动版