背景 上一篇文章已经简单介绍使用了Hooks, 而Hooks带来的好处就是可以封装常用逻辑, 在前端中最常见的就是request请求了, 那么看看如何封装异步请求Hooks比较合适
开发 useAsync首先, 需要构建类似useState方法的Hooks, 只是传入值应该是一个Promise(如果非Promise, 直接当做已经完成的值就行), 返回的state也有三个属性: data/loading/error, 就叫它useAsync吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export  type  UseAsyncArgs<T> = T | (() =>  Promise <T>);export  type  UseAsyncState<T> = { loading : boolean ; error: Error ; data: T };export  type  UseAsyncPromise<T> = T | Promise <T>;export  type  UseAsyncReturn<T> = [UseAsyncState<T>, Dispatch<SetStateAction<UseAsyncPromise<T>>>];const  useAsync = <T>(_promise: UseAsyncArgs<T>): UseAsyncReturn<T> => {  const  [promise, setPromise] = useState<UseAsyncPromise<T>>(_promise);   const  [jobState, setJobState] = useState<UseAsyncState<T>>({     loading : true ,     error : null ,     data : null ,   });   return  [jobState, setPromise]; }; 
 
然后是我们需要在此Hooks里得到promise的执行情况, 并根据执行结果setState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const  useAsync = <T>(_promise: UseAsyncArgs<T>): UseAsyncReturn<T> => {  const  [promise, setPromise] = useState<UseAsyncPromise<T>>(_promise);   const  [jobState, setJobState] = useState<UseAsyncState<T>>({     loading : true ,     error : null ,     data : null ,   });   const  doJob = async  () => {     const  _jobState = { ...jobState };     try  {       if  (promise instanceof  Promise  || _.isFunction(_.get(promise, 'then' ))) {         _jobState.loading = true ;         _jobState.error = null ;         _jobState.data = null ;         setJobState({ ..._jobState });         const  result = await  promise;         _jobState.data = result;       } else  {         _jobState.loading = false ;         _jobState.error = null ;         _jobState.data = promise;       }     } catch  (error) {       console .log('useAsync error' , error);       _jobState.error = error;     } finally  {       _jobState.loading = false ;       setJobState({ ..._jobState });     }   };   useEffect(() =>  {     doJob();   }, [promise]);   return  [jobState, setPromise]; }; 
 
初步看起来这样没问题了, 但是试用后会发现, 如果第一次setPromise的返回结果比第二次慢, 那么实际上生效的data会是返回比较慢的值, 但它应该是要被覆盖的值才对. 所以需要加上promise是否最新的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 const  useAsync = <T>(_promise: UseAsyncArgs<T>): UseAsyncReturn<T> => {  const  [promise, setPromise] = useState<UseAsyncPromise<T>>(_promise);   let  isPromise = false ;   if  (promise instanceof  Promise  || _.isFunction(_.get(promise, 'then' ))) {     isPromise = true ;   }   const  [jobState, setJobState] = useState<UseAsyncState<T>>({     loading : isPromise ? true  : false ,     error : null ,     data : isPromise ? null  : (promise as  T),   });   const  currentJobRef = useRef<Promise <T>>(null );   const  doJob = async  () => {     const  _jobState = { ...jobState };     try  {       currentJobRef.current = promise as  Promise <T>;       if  (promise instanceof  Promise  || _.isFunction(_.get(promise, 'then' ))) {         _jobState.loading = true ;         _jobState.error = null ;         _jobState.data = null ;         setJobState({ ..._jobState });         const  result = await  promise;         if  (currentJobRef.current === promise) {           _jobState.data = result;         }       } else  {         _jobState.loading = false ;         _jobState.error = null ;         _jobState.data = promise;       }     } catch  (error) {       console .log('useAsync error' , error);       if  (currentJobRef.current === promise) {         _jobState.error = error;       }     } finally  {       if  (currentJobRef.current && currentJobRef.current === promise) {         _jobState.loading = false ;         setJobState({ ..._jobState });       }     }   };   if  (isPromise && currentJobRef.current !== promise) {     doJob();   }   useEffect(() =>  {     return  () =>  {       currentJobRef.current = null ;     };   }, []);   return  [jobState, setPromise]; }; 
 
useFetch然后再实现useFetch, 实现接口的自动请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const  isAxiosResponse = <T>(result: unknown): result is AxiosResponse<T> => (result as  AxiosResponse)?.request instanceof  XMLHttpRequest;export  type  UseFetchReturn<R, T> = [UseAsyncState<R>, (promise: UseAsyncPromise<T> ) =>  UseAsyncPromise<T>];const  defaultGetData = <T, R>(result: T) => {  if (isAxiosResponse<R>(result)) {     return result.data;   }   return result as unknown as R; }; const useFetch = <T, R = T extends AxiosResponse ? T['data'] : T>(   promise: UseAsyncArgs<T> = null,   getData: (result: T) => R = defaultGetData, ): UseFetchReturn<R, T> => {   const [asyncState, setPromise] = useAsync<T>(promise);   const login = useLogin();   if (asyncState.error instanceof UTILS.FetchError.AuthError) {     login();   }   const resolveAsyncState: { loading: boolean; error: Error; data: R } = {     ...asyncState,     data: useMemo<R>(       () => (_.isArray(asyncState.data) ? (asyncState.data.map(getData) as any) : getData(asyncState.data)),       [asyncState.data, getData],     ),   };   return [resolveAsyncState, setPromise]; }; 
 
完善流程 以上两个Hooks完成后, 已经实现了请求的完整流程控制, 使用起来也比较简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  [pageData, setPagePromise] = useFetch(null );const  clickRefreshData = useCallback(  async  (id: string ) => {     setPagePromise(       UTILS.post<{         data : string ;       }>(api.webApi.api(), { id }),     );   },   [setPagePromise], ); useEffect(() =>  {    }, [pageData]); 
 
但是这样使用起来实际上是会打断流程的, 甚至比以前的回调地狱还难用, 因为闭包之间是没有互相包含的, 如果有个异步串行请求, 那么实现起来会非常痛苦
普通异步请求流程 flowchart LR
  subgraph A[闭包空间Event]
    direction LR
    E1[事件触发] --> F1[执行异步Job]
    F1 -. 完成 .-> F2[异步回调]
    F2 --> F3[处理结果]
  end
Hooks请求流程flowchart LR
  subgraph AA[闭包空间Event]
    direction LR
    EE1[事件触发]-->FF1[执行异步Job]
  end
  subgraph BB[闭包空间 useEffect]
    direction LR
    FF1 -. 完成 .-> FF2[异步回调]
    FF2 --> FF3[处理结果]
  end
那么有办法解决这个问题吗? 如果能将setPagePromise这个方法也能直接添加异步返回, 就能直接变成普通异步请求的流程. 对于无所不能的js来说, 好像也是可以实现的
useFetchWithErrorHandler按上面所描述的, 我们只需要在useFetch的时候, 将setPromise的返回值修改为与原始promise返回值同步的操作就行, 这里把项目中常用的error-modal弹窗也直接加入进来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 const  getErrorMessage = (error: Error  ) =>   React.createElement(React.Fragment, null , React.createElement('div' , null , _.get(error, 'text' )), React.createElement('div' , null , error.message)); export  type  UseFetchWithErrorHandlerReturn<T> = [  UseAsyncState<T>,   <TT, RR = TT extends AxiosResponse ? TT['data'] : TT>(     promise?: Promise<TT> | Promise<any>,     _showError?: boolean,     _throwError?: boolean,   ) => Promise<RR>, ]; const useFetchWithErrorHandler = <T>(promise: UseAsyncArgs<T> = null, showError: boolean = true): UseFetchWithErrorHandlerReturn<T> => {   const [resolveAsyncState, setPromise] = useFetch<T | any>(promise);   const setPromiseWithErrorHandler = useCallback(     <TT, RR = TT extends AxiosResponse ? TT['data'] : TT>(       _promise: Promise<TT> | Promise<any> = null,       _showError: boolean = showError,       _throwError: boolean = false,     ): Promise<RR> => {       return new Promise<RR>((resolve, reject) => {         const isPromise = _promise instanceof Promise || _.isFunction(_.get(_promise, 'then'));         if (isPromise) {           setPromise(             (_promise as Promise<RR>).then(               (data: any) => {                 resolve(defaultGetData(data));                 return data;               },               (error: Error) => {                 if (_showError) {                   Modal.error({                     centered: true,                     title: error.name,                     content: getErrorMessage(error),                   });                 }                 if (_throwError) {                   reject(error);                 }                 return Promise.reject(error);               },             ),           );         } else {           resolve(setPromise(_promise));         }       });     },     [setPromise, showError],   );   return [resolveAsyncState, setPromiseWithErrorHandler]; }; 
 
使用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const  [pageData, setPagePromise] = useFetchWithErrorHandler(null );const  clickRefreshData = useCallback(  async  (id: string ) => {     try  {       const  result = await  setPagePromise(         UTILS.post<{           data : string ;         }>(api.webApi.api(), { id }),       );            } catch  (e) {            }   },   [setPagePromise], ); 
 
全局进度条 既然实现了全局统一的useFetch以及error-msg, 那么顺便把请求进度条也一起加上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 type ProgressConfig = {   start : number;   progress: number;   intervalId?: number; }; type RequestConfig = FetchRequestConfig & {   requestId : string; }; const  LoadingProgress: React.FC = () =>  {  const  loadingRef = useRef<{     [key: string]: ProgressConfig;   }>({});   const  [progressState, setProgressState] = useState({     status : 'none' ,     progress : 0 ,   });   const  calcProgressState = () =>  {     let  currentProgress = 0 ;     _.map(loadingRef.current, ({ progress } ) =>  {       currentProgress += progress;     });     const  totalProgress = _.size(loadingRef.current) * 100 ;     if  (currentProgress === totalProgress) {       _.map(_.keys(loadingRef.current), (key ) =>  Reflect .deleteProperty(loadingRef.current, key));       setTimeout (         () =>            setProgressState({             status : 'none' ,             progress : 0 ,           }),         300 ,       );     }     setProgressState({       status : currentProgress === totalProgress ? 'done'  : 'progress' ,       progress : (currentProgress / totalProgress) * 100 ,     });   };   useEffect(() =>  {     const  reqOff = addFetchEventListener('request' , (config: RequestConfig ) =>  {       if  (config._hasProgress) {         config.requestId = _.uniqueId(`axios_request_` );         const  progressConfig: ProgressConfig = {           start : new  Date ().getTime(),           progress : 20 ,         };         loadingRef.current[config.requestId] = progressConfig;         calcProgressState();         progressConfig.intervalId = window .setInterval(() =>  {           const  spendTime = new  Date ().getTime() - progressConfig.start;                      progressConfig.progress = 20  + (spendTime / (spendTime + 500 )) * 0.7  * 100 ;           calcProgressState();         }, 100 );       }       return  config;     });     const  resOff = addFetchEventListener(       'response' ,       (response: AxiosResponse ) =>  {         const  { config } = response;         if  ((config as  RequestConfig)._hasProgress) {           const  progressConfig = loadingRef.current[(config as  RequestConfig).requestId];           progressConfig.progress = 100 ;           clearInterval (progressConfig.intervalId);           calcProgressState();         }         return  response;       },       (error: AxiosError ) =>  {         const  { config } = error;         if  ((config as  RequestConfig)._hasProgress) {           const  progressConfig = loadingRef.current[(config as  RequestConfig).requestId];           progressConfig.progress = 100 ;           clearInterval (progressConfig.intervalId);           calcProgressState();         }         return  Promise .reject(error);       },     );     return  () =>  {       reqOff();       resOff();     };   }, []);   return  (     <div  className ={classNames( 'loading-progress-wrapper ', { hidden:  progressState.status  === 'none'  })}>        <Progress           className ='loading-progress'          type ='line'          showInfo ={false}          status ={progressState.status  === 'progress'  && progressState.progress  >  80 ? 'active' : 'normal'}        percent={progressState.progress}         strokeColor={{           '0%': '#108ee9',           '100%': '#87d068',         }}         strokeWidth={5}       />       <style  jsx  global >          {`           .loading-progress-wrapper  {             z-index : 2000 ;             height : 3px ;             width : 100% ;             position : absolute;             .loading-progress  {               display : block;               line-height : 0px ;               height : 3px ;             }           }         `}        </style >     </div >    ); }; export  default  LoadingProgress;