useRequest的拓展功能

1,520 阅读5分钟

useRequest的拓展功能

上一篇文章我们介绍了useRequest()的基本用法,这篇文章我们将讲述,useRequest是如何进行拓展的,如果我们自己以后封装hooks 的时候,应该怎么在基本功能上拓展。

拓展功能

  1. 延时展示loading
  2. 轮询请求
  3. 延时请求
  4. ready状态之前请求不触发
  5. 依赖刷新

useRequest拓展功能的机制

useRequest()的拓展功能都是通过类似于插件的机制,在基本功能的情况下,通过一定的钩子函数来覆盖参数进行操作。每一个插件都是一个函数,本身可以挂载一个 init()函数,也可以调用函数返回一些钩子,在请求不同的时期进行调用。

function useXXX(fetchInstance, options) {}

useXXX.onInit = (options) => {}

插件的钩子

onInit()

从上一篇文章中,我们知道useRequest的核心内容都是在Fetch中, onInit()方法主要是修改Fetch的初始化的state, 它在Fetch实例化 之前的时候调用

  const fetchInstance = useCreation(() => {
    const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
    return new Fetch(
      serviceRef,
      fetchOptions,
      update,
      Object.assign({}, ...initState),
    );
  }, []);
onBefore()

onBefore() 会在请求之前被调用,用于修改请求发送之前的状态

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

onFinally() 会在请求结束后被调用,用于在请求后做一些清理工作

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

onCancel()

onCancel() 会在点击调用取消的时候执行

  class Fetch {
    cancel() {
      this.count += 1;
      this.setState({
        loading: false,
      });
      this.runPluginHandler('onCancel');
    }
  }

延时展示loading

在业务中,正常的情况下,我们发送请求的时候,需要展示给用户一个加载中的状态。但是,当我们请求时间比较短的时候,就会有场景不需要展示给用户加载中的状态, 直接展示请求后的数据状态,这样就可以防止从 加载中变成请求后的闪烁,useRequest()通过配置options的loadingDelay参数,来控制多长时间我们需要展示loading状态(返回的loading为true)

const { loading, data } = useRequest(getUsername, {
  loadingDelay: 300
});

return <div>{ loading ? 'Loading...' : data }</div>

内部实现(loadingDelay)

主要是通过onBefore()钩子,在请求发生前的loadingDelay时间内,将loading的状态设置为false, 不展示在请求中的状态,过了loadingDelay时间后,将 loading的状态设置为true。

我们知道插件是一个函数, 接受2个参数,一个是Fetch实例,一个是options选项。由于在请求之前会执行每一个插件返回的onBefore, 默认情况,在执行请求的之前,会将loading 设置为true, 所以useLoadingDelayPlugin的主要作用,就是在loadingDelay之前,将loading返回为false,覆盖掉本来的值。

const useLoadingDelayPlugin = (fetchInstance, {loadingDelay}) => {
  const timerRef =  useRef()
  if(!loadingDelay) {
    return {}
  }
  const cancelTimeout = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
  };
  return {
    onBefore: (p) => {
      cancelTimeout();
      timerRef.current = setTimeout(() => {
        fetchInstance.setState({
          loading: true,
        });
      }, loadingDelay);

      return {
        loading: false,
      };
    },
    onFinally: () => {
      cancelTimeout();
    },
    onCancel: () => {
      cancelTimeout();
    },
  };
}

Fetch中覆盖掉原本的loading值:

    const {
      ...state
    } = this.runPluginHandler('onBefore', params);
    this.setState({
      loading: true,
      ...state,
    });

轮询请求(pollingInterval)

在实际的业务中,我们可能会有一些场景需要轮询某一个接口请求,useRequest()通过传递参数pollingInterval来指定轮询时间,我们可以通过cancel来停止轮询。

const { data, run, cancel } = useRequest(getUsername, {
  pollingInterval: 3000, // 轮询时间
  pollingWhenHidden: true  // 页面隐藏的时候是否轮询
});

内部实现

轮询的内部实现主要是通过onBefore()onFinally()onCancel()来实现。

主要思路是,在每次请求结束后,新增一个定时器,在指定pollingInterval时间后,调用Fetch实例的refresh重新请求。每一次请求之前,清空上一次请求的定时器和订阅

const usePollingPlugin = ( fetchInstance,
  { pollingInterval, pollingWhenHidden = true },
) => {
  const timerRef = useRef();
  const unsubscribeRef = useRef();
  const stopPolling = () => { 
    // 如果存在定时器,就清掉原来的定时器
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
    // 删除lister的函数
    unsubscribeRef.current?.();
  };

  useUpdateEffect(() => {
    if (!pollingInterval) {
      stopPolling();
    }
  }, [pollingInterval]);

  if (!pollingInterval) {
    return {};
  }

  return {
    onBefore: () => {
      stopPolling();
    },
    onFinally: () => {
      // 当隐藏的时候,我们需要记入当页面展示的时候请求函数,等当页面展示的时候重新的请求函数
      if (!pollingWhenHidden && !isDocumentVisible()) {
        unsubscribeRef.current = subscribeReVisible(() => {
          // 页面展示的时候重新请求
          fetchInstance.refresh();
        });
        return;
      }
      
      // 每次请求完成后,重新设置一个新的定时器
      timerRef.current = setTimeout(() => {
        fetchInstance.refresh();
      }, pollingInterval);
    },
    onCancel: () => {
      stopPolling();
    },
  };
};

等待请求(ready)和依赖刷新(refreshDeps)

有时候,我们需要等待一些准备工作,才去发送请求, 或者当一些数据发生变化后,重新的发送请求。useRequest()提供ready参数,让我们自动去控制请求的时机、refreshDeps 参数当依赖数据发送变化后,重新发送请求。

  const [ready, { toggle }] = useToggle(false);
  const { data, run } = useRequest(() => getUserSchool(userId), {
    refreshDeps: [userId],
    ready
  });

内部实现

默认情况下,useRequest()不需要等待时间,在manual为true的时候,自动发送请求,那如果我们当ready为false的时候,不发送请求,就需要在 执行请求之前,提前返回。

const useAutoRunPlugin = (fetchInstance, {manual, ready, defaultParams }) => {
  const hasAutoRun = useRef(false)
  // 每次运行都将其设置为false
  hasAutoRun.current = false
  useUpdateEffect(() => {
    if (!manual && ready) {
      hasAutoRun.current = true;
      fetchInstance.run(...defaultParams);
    }
  }, [ready]);
  
  useUpdateEffect(() => {
    if(hasAutoRun.current) return
    if(!manual) {
      hasAutoRun.current = true
      fetchInstance.refresh();

    }
  }, [...refreshDeps])
  return {
    onBefore: () => {
      if(!ready) {
        return { stopNow: true }
      }
    }    
 }
}

// 在初始化的时候,根据ready的值,是否展示loading状态
useAutoRunPlugin.init = (ready = true, manual) => {
  return {
    loading: !manual && ready,
  };
}

从上面的代码中,我们可以看出,useAutoRunPlugin插件主要是,当ready为true 的时候,重新执行了实例的run()方法,在onBefore阶段, 如果ready为false 的时候,返回stopNow为true。 而在Fetch实例的时候,当stopNow为true的时候,请求会立即返回,这样就可以做到ready为false的时候,不发出请求

  class Fetch {
    async runAsync(...params) {
      this.count += 1;
      const currentCount = this.count;
      const {
        stopNow = false,
        returnNow = false,
        ...state
      } = this.runPluginHandler('onBefore', params);
      // stop request
      if (stopNow) {
        return new Promise(() => {});
      }
    }
  }

防抖和节流(debounceWaitthrottleWait)

useRequest()中的节流和防抖都是通过lodash中对应的库,通过对Fetch实例的runAsync进行包装,从而实现防抖和节流

const { data, run } = useRequest(getUsername, {
  throttleWait: 300,
  manual: true
});

内部实现

从上一篇文章中,我们可以看出useRequest的请求都是通过Fetch实例的runAsync方法,所以防抖和节流都是基于runAsync进行的改装。

import throttle from 'lodash/throttle';

const useThrottlePlugin = (
  fetchInstance,
  { throttleWait, throttleLeading, throttleTrailing },
) => {
  const throttledRef = useRef();

  const options = {};
  // 请求的相关配置
  if (throttleLeading !== undefined) {
    options.leading = throttleLeading;
  }
  if (throttleTrailing !== undefined) {
    options.trailing = throttleTrailing;
  }

  useEffect(() => {
    if (throttleWait) {
      // 保留原有的发送请求方法
      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
      
      // 调用lodash的节流函数,记录返回的新函数
      throttledRef.current = throttle(
        (callback) => {
          callback();
        },
        throttleWait,
        options,
      );

      // throttle runAsync should be promise
      // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
      fetchInstance.runAsync = (...args) => {
        return new Promise((resolve, reject) => {
          throttledRef.current?.(() => {
            _originRunAsync(...args)
              .then(resolve)
              .catch(reject);
          });
        });
      };

      return () => {
        fetchInstance.runAsync = _originRunAsync;
        throttledRef.current?.cancel();
      };
    }
  }, [throttleWait, throttleLeading, throttleTrailing]);
  
  if (!throttleWait) {
    return {};
  }

  return {
    onCancel: () => {
      throttledRef.current?.cancel();
    },
  };
};