React请求类hook useRequest封装思路-ahooks

本文以ahooksuseRequest源码解读介绍请求类的hooks封装思路,通过解读该请求类hooks的封装思路可以得到如下思想

  • 应该提供哪些参数,应该返回哪些参数
  • 请求类hooks该如何封装,业务层该如何实现
  • 是所有的要实现的功能逻辑都要写入主体执行函数中

如有不足欢迎指导

useRequest简介

useRequest 是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 useRequest 就够了。
useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

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

上面是官网对useRequest的介绍,从中可以看出几个信息

  • 实现了一个可以执行异步函数的hook
  • 核心代码简单,使用插件化机制扩展功能
  • 使用场景是在异步数据管理场景中
  • 要实现的功能有轮询,防抖…

所以针对于上面提出的几点要求,结合源码得到了部分的答案

  • 请求类hooks该如何封装?
    架构层该怎么理解,业务层该怎么写?
  • 应该提供哪些参数,应该返回哪些参数?
    需要提供的参数:基本的要处理的异步函数,该异步函数的配置项参数,生命周期的配置项(我把onSuccess这样的函数叫做了生命周期),插件功能的配置项…
    需要返回的参数:返回异步函数的结果,异步函数执行中的状态…
  • 是所有的要实现的功能逻辑都要写入主体执行函数中吗?
    看起来是不需要的,只需要提供最核心的功能就行了(这就是典型的微内核思想),其他的功能通过插件化机制注入
  • 在封装该hook的时候需要考虑业务场景吗?
    可以定义一个大的业务场景,比如是异步场景.不能局限于只针对某个请求库的封装,比如axios,fetch…

需要提供哪些参数

import useAutoRunPlugin from './plugins/useAutoRunPlugin';
import useCachePlugin from './plugins/useCachePlugin';
import useDebouncePlugin from './plugins/useDebouncePlugin';
import useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin';
import usePollingPlugin from './plugins/usePollingPlugin';
import useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin';
import useRetryPlugin from './plugins/useRetryPlugin';
import useThrottlePlugin from './plugins/useThrottlePlugin';
import type { Options, Plugin, Service } from './types';
import useRequestImplement from './useRequestImplement';

function useRequest<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options?: Options<TData, TParams>,
  plugins?: Plugin<TData, TParams>[],
) {
  return useRequestImplement<TData, TParams>(service, options, [
    // 用户扩展功能的插件
    ...(plugins || []),
    // 下面是系统扩展功能的插件
    useDebouncePlugin,
    useLoadingDelayPlugin,
    usePollingPlugin,
    useRefreshOnWindowFocusPlugin,
    useThrottlePlugin,
    useAutoRunPlugin,
    useCachePlugin,
    useRetryPlugin,
  ] as Plugin<TData, TParams>[]);
}

export default useRequest;

需要提供的参数有service,options,plugins

  • services:提供的异步函数,这里抽象成了服务的单词
  • options:该hook的配置项
  • plugins:提供用户用于扩展功能的插件

可以看出该库的封装思路和大多数库的封装思路类似,真正的核心能力处理并不在useRequest函数中,而是将参数下发到useRequestImplement函数中

那么为什么这样子处理呢?

  • 可以在这个入口层做一些抹平差异操作,例如将环境差异抹平,将数据差异抹平
  • 可以添加一些真正核心代码执行前的准备工作,例如这里就是将用户插件和系统插件进行合并处理,然后下发到useRequestImplement函数中
  • 如果useRequest是真正处理核心功能的地方,那么这个文件肯定会有很长的代码,并且这个文件的代码很难读懂,导致难以维护

业务层实现

import useCreation from '../../useCreation';
import useLatest from '../../useLatest';
import useMemoizedFn from '../../useMemoizedFn';
import useMount from '../../useMount';
import useUnmount from '../../useUnmount';
import useUpdate from '../../useUpdate';
import isDev from '../../utils/isDev';

import Fetch from './Fetch';
import type { Options, Plugin, Result, Service } from './types';

function useRequestImplement<TData, TParams extends any[]>(
  service: Service<TData, TParams>,
  options: Options<TData, TParams> = {},
  plugins: Plugin<TData, TParams>[] = [],
) {
// 实现层应该做的事情
// 1.根据service创建真实的请求-->fetch
// 2.针对于hooks options管控请求的参数
// 3.实现插件化的能力

  // 为什么manual设置为false? 因为大部分请求不需要手动触发
  // manual=false表示不需要手动触发
  const { manual = false, ...rest } = options;

  if (isDev) {
    if (options.defaultParams && !Array.isArray(options.defaultParams)) {
      console.warn(`expected defaultParams is array, got ${typeof options.defaultParams}`);
    }
  }
  // 对参数重新封装,这样更语义化,并且,重新设置了maual参数
  const fetchOptions = {
    manual,
    ...rest,
  };

  // 使用useLatest缓存service函数,useLatest里面是使用useRef实现的
  const serviceRef = useLatest(service);
  // useUpdate里面使用setState更新页面,我感觉叫做update不如叫做renderPage
  const update = useUpdate();

  // 创建请求实例,将serviceRef,配置项传递到Fetch上
  const fetchInstance = useCreation(() => {
    // 使用useAutoRunPlugin和用户自定义插件在插件的onInit函数去初始化返回的结果的一些状态
    // 例如初始化loading状态...
    // 这里主要是将要执行service前的一些准备工作
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);

    // 这是自己封装的Fetch处理类,不要被名字迷惑,虽然叫做Fetch,但是Fetch里面没有任何的fetch库代码
    return new Fetch<TData, TParams>(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),// 最后一个是根据useAutoRunPlugin和用户自定义插件提供的onInit函数的返回结果给到Fetch类
    );
  }, []);
 
  // 将fetchOptions参数给到fetchInstance实例
  fetchInstance.options = fetchOptions;

  // run all plugins hooks
  // 因为插件是写成函数形式,所以只要是函数形式的插件都可以,不像vue有入口规则install函数
  // 这里的插件函数是有规则的,第一个参数都是fetch实例,第二个参数是fetch的配置项
  fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
	
  // useMount就是useEffect的deps是[]的情况
  // 表示的意思是如果不需要手动触发那么通过页面初始化的时候自动执行异步
  useMount(() => {
    // manual是false的情况下(自动触发)去通过run发起请求
    if (!manual) {
      // useCachePlugin can set fetchInstance.state.params from cache when init
      // 对异步函数的参数做处理
      const params = fetchInstance.state.params || options.defaultParams || [];
      // @ts-ignore
      fetchInstance.run(...params);
    }
  });
	
  // useUnmount就是useEffect返回的销毁函数
  useUnmount(() => {
    // 取消异步执行的函数
    fetchInstance.cancel();
  });

  return {
    loading: fetchInstance.state.loading,
    data: fetchInstance.state.data,
    error: fetchInstance.state.error,
    params: fetchInstance.state.params || [],
	
	// 为什么要写成useMemoizedFn(fetchInstance.cancel.bind(fetchInstance))
    // 1.保证cancel能够bind到fetchInstance上
    // 2.缓存函数fetchInstance.cancel
    cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
    
    refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)),
    refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),
    run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)),
    runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)),
    mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)),
  } as Result<TData, TParams>;
}

export default useRequestImplement;

在业务层实现的功能

  • 根据service创建真实异步函数处理,通过fetchInstance.run(...params)去执行异步函数
  • 针对于hooks options管控请求的参数,抹平数据差异,具体的实现则下发到了Fetch类中
  • 实现插件化的能力

异步函数管理库

/* eslint-disable @typescript-eslint/no-parameter-properties */
import { isFunction } from '../../utils';
import type { MutableRefObject } from 'react';
import type { FetchState, Options, PluginReturn, Service, Subscribe } from './types';

// TData是泛型,也可以叫做TD或者T...
// 这个泛型相当于上一层透传过来的,为什么说相当于而不说等于
// 因为两个只是保持一致
// 为什么叫做TData,因为T表示泛型,Data表示数据,TData就表示Data类型的泛型T
export default class Fetch<TData, TParams extends any[]> {
  pluginImpls: PluginReturn<TData, TParams>[];

  count: number = 0;// 保证和其它的请求互斥,这个count很重要

  // 聚合所有的state,外部实例可以直接.state访问所有的state
  // 这个就是将要返回的state
  state: FetchState<TData, TParams> = {
    loading: false,
    params: undefined,
    data: undefined,
    error: undefined,
  };

  constructor(
    public serviceRef: MutableRefObject<Service<TData, TParams>>,// MutableRefObject是useRef返回的类型
    public options: Options<TData, TParams>,
    public subscribe: Subscribe,// 发布订阅模式,这里主要是传入页面的更新函数,就是上一个页面的update函数,我把他叫做renderPage函数
    public initState: Partial<FetchState<TData, TParams>> = {},// 这个是根据插件的初始化函数onInit初始化状态的结果
  ) {
    this.state = {
      ...this.state,
      // 因为默认是手动触发,options.manual = false就代表是手动触发
      // 当manual是true的时候就代表手动触发,所以此时loading就是false
      // 当manual是false的时候就代表自动触发,所以此时loading就是true,loading=true就代表处理异步操作开始了
      loading: !options.manual,
      ...initState,
    };
  }
  // state就是useRequest返回的结果
  setState(s: Partial<FetchState<TData, TParams>> = {}) {
    this.state = {
      ...this.state,
      ...s,// 相同的属性会覆盖state的属性
    };
    this.subscribe();// 更新页面
  }
	
  // 在做每次操作(runAsync,cancel,mutate)后都会执行插件里面对应的生命周期,让插件的生命周期也做对应的操作
  // 例如执行cancel函数后就会执行runPluginHandler('onCancel')
  runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
    // @ts-ignore
    // event是插件提供的生命周期方法名称例如onRequest,onBefore,onCancel...
    const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
    return Object.assign({}, ...r);
  }

  async runAsync(...params: TParams): Promise<TData> {
    this.count += 1;
    const currentCount = this.count;// 保存本次的count

    const {
      stopNow = false,
      returnNow = false,
      ...state
    } = this.runPluginHandler('onBefore', params);

    // stop request
    if (stopNow) {
      return new Promise(() => {});
    }

    // 因为params重新传入了,所以重新setState
    this.setState({
      loading: true,
      params,
      ...state,
    });

    // return now
    if (returnNow) {
      return Promise.resolve(state.data);
    }

    this.options.onBefore?.(params);

    try {
      // replace service
      // 创建service请求,返回的是一个Promise
      // 执行所有插件的onRequest方法
      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);

      if (!servicePromise) {
        servicePromise = this.serviceRef.current(...params);
      }

      // 这里使用await处理请求调用的结果,这里不包含任何的请求业务逻辑
      // 整个的思路基于请求的功能做扩展,而不会限制死某个请求(比如限制为axios,fetch...)
      // 整个的useRequest功能就是下面的代码执行
      const res = await servicePromise;
		
	  // 对下面if (currentCount !== this.count)代码的解释
      // 接入此时页面连续发送好几个请求,也就是连续调用好几次的runAsync方法
      // 假如此时第一个runAsync代码还在运行中,只不过异步事件的处理还在事件循环队列里面,所以,当前的currentCount是0,
      // 此时第二个请求来了,那么此时的count就是1,然后将这个请求也会放入到事件循环中
      // 此时的第一个请走到了下面这句话的代码,这时候就会发现两个并不相等(currentCount = 0,count = 1),所以直接将第一次的请求取消掉
      // 其实也不是取消,请求的结果依然有,只不过不要这一次的请求结果res,要后面的请求结果
      // 简单描述:下一次的请求的结果比前面的请求结果提前到达,就不要前面的请求结果,而是要后面的请求结果
      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }

      // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;

      // 将最后的结果替换掉state
      this.setState({
        data: res,
        error: undefined,
        loading: false,// loading设置为false,表示请求结束
      });

      // 成功的话执行成功的回调
      this.options.onSuccess?.(res, params);
      // 执行所有插件的onSuccess方法
      this.runPluginHandler('onSuccess', res, params);
     
      // 不管成功失败都执行finally
      this.options.onFinally?.(params, res, undefined);

      if (currentCount === this.count) {
        this.runPluginHandler('onFinally', params, res, undefined);
      }

      return res;
    } catch (error) {
      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }

      this.setState({
        error,
        loading: false,
      });

      // 错误执行错误的回调
      this.options.onError?.(error, params);
      this.runPluginHandler('onError', error, params);

      this.options.onFinally?.(params, undefined, error);

      if (currentCount === this.count) {
        this.runPluginHandler('onFinally', params, undefined, error);
      }

      throw error;
    }
  }
  // run就是执行的runAsync函数
  run(...params: TParams) {
    this.runAsync(...params).catch((error) => {
      if (!this.options.onError) {
        console.error(error);
      }
    });
  }

  cancel() {
    // 请求期间点击cancel
    this.count += 1;// cancel就是让currentCount和count不相等
    this.setState({
      loading: false,
    });

    this.runPluginHandler('onCancel');
  }

  refresh() {
    // @ts-ignore
    this.run(...(this.state.params || []));
  }

  refreshAsync() {
    // @ts-ignore
    return this.runAsync(...(this.state.params || []));
  }

  mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
    const targetData = isFunction(data) ? data(this.state.data) : data;
    this.runPluginHandler('onMutate', targetData);
    this.setState({
      data: targetData,
    });
  }
}

实现的功能有:

  • 管理异步函数的参数
  • 管理异步函数执行的结果
  • 提供操作异步函数的方法,执行对应的方法顺便执行对应的插件内对应的方法

总结

  • 应该提供哪些参数,应该返回哪些参数
    应该提供:基本的异步函数,异步函数的配置项,异步函数的生命周期配置项,以及用户自定义插件的配置项
    应该返回:加载状态,异步函数的操作(cancel,mutate,refresh…),异步函数的结果
  • 请求类hooks该如何封装,业务层该如何实现
    不应当专注与异步函数的业务逻辑,也就是不关心异步函数长什么样子,我应当只关心我会执行你
  • 是所有的要实现的功能逻辑都要写入主体执行函数中
    不应当,主体应该只有核心功能的实现,其余的功能通过插件化机制实现
  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个React触底加载的Hook封装的示例代码: ```javascript import { useState, useEffect } from 'react'; function useOnScreen(ref) { const [isIntersecting, setIntersecting] = useState(false); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { setIntersecting(entry.isIntersecting); }, { rootMargin: '0px 0px 100px 0px' } // 设置触发时机 ); if (ref.current) { observer.observe(ref.current); } return () => { observer.unobserve(ref.current); }; }, [ref]); return isIntersecting; } function useScroll() { const [scrollPosition, setScrollPosition] = useState(0); useEffect(() => { const updateScrollPosition = () => { setScrollPosition(window.pageYOffset); }; window.addEventListener('scroll', updateScrollPosition); return () => window.removeEventListener('scroll', updateScrollPosition); }, []); return scrollPosition; } function useInfiniteScroll(callback) { const [isFetching, setIsFetching] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const ref = useRef(); const isIntersecting = useOnScreen(ref); const scrollPosition = useScroll(); useEffect(() => { if (isIntersecting && !isFetching && hasMore) { setIsFetching(true); callback(page).then((res) => { if (res.length === 0) { setHasMore(false); } else { setPage(page + 1); } setIsFetching(false); }); } }, [isIntersecting, isFetching, hasMore, page, callback]); return [ref, scrollPosition]; } ``` 这个Hook封装了一个无限滚动的功能,当用户滚动到页面底部时,会自动触发回调函数,从而实现无限滚动的效果。其中,`useOnScreen`是用来判断元素是否在可视区域内的Hook,`useScroll`是用来获取滚动条位置的Hook,`useInfiniteScroll`是用来封装无限滚动功能的Hook。 使用示例: ```javascript function App() { const [list, setList] = useState([]); const [ref, scrollPosition] = useInfiniteScroll(fetchData); function fetchData(page

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值