Vue3 TypeScript 實現(xiàn)一個 useRequest
大廠技術??高級前端??Node進階
點擊上方?程序員成長指北,關注公眾號
回復1,加入高級Node交流群
起因
自從 Vue3 更新之后,算是投入了比較大的精力寫了一個較為完善的Vue3.2 + Vite2 + Pinia + Naive UI的 B 端模版,在做到網(wǎng)絡請求這一塊的時候,最初使用的是VueRequest的useRequest,但是因為VueRequest的useRequest的cancel關閉請求并不是真正的關閉,對我個人來說,還是比較介意,于是在參考aHooks和VueRequest的源碼之后,差不多弄了一個簡易的useRequest,使用體驗還算 ok,但是因為個人能力以及公司業(yè)務的問題,我的版本只支持axios,不支持fetch,算是作為公司私有的庫使用,沒有考慮功能的大而全,也只按VueRequest的官網(wǎng),實現(xiàn)了一部分我認為最重要的功能。
寫的比較混亂,中間是一部分思考,可以直接拖到最后看實現(xiàn),再回來看一下我為什么選擇這么做,歡迎討論。
效果展示

一個基礎的useRequest示例,支持發(fā)起請求 取消請求 請求成功信息 成功回調(diào) 錯誤捕獲

queryKey示例,單個useRequest管理多個相同請求。
其余還是依賴更新 重復請求關閉 防抖 節(jié)流等功能
Axios
既然咱們使用TypeScript和axios,為了使axios能滿足咱們的使用需求以及配合TypeScript的編寫時使用體驗,咱們對axios進行一個簡單的封裝。
interface
//?/src/hooks/useRequest/types.ts
import?{?AxiosResponse,?Canceler?}?from?'axios';
import?{?Ref?}?from?'vue';
//?后臺返回的數(shù)據(jù)類型
export?interface?Response?{
????code:?number;
????data:?T;
????msg:?string;
}
//?為了使用方便,對?AxiosResponse?默認添加我們公用的?Response?類型
export?type?AppAxiosResponseany>?=?AxiosResponse>;
//?為了?useRequest?使用封裝的類型
export?interface?RequestResponse?{
????instance:?Promise>;
????cancel:?Refundefined>;
}
復制代碼
axios 的簡單封裝
因為咱們現(xiàn)在沒有接入業(yè)務,所以axios只需要簡單的封裝能支持咱們useRequest的需求即可。
import?{?ref?}?from?'vue';
import?{?AppAxiosResponse,?RequestResponse?}?from?'./types';
import?axios,?{?AxiosRequestConfig,?Canceler?}?from?'axios';
const?instance?=?axios.create({
??timeout:?30?*?1000,
??baseURL:?'/api'
});
export?function?request<T>(config:?AxiosRequestConfig):?RequestResponse<T>?{
??const?cancel?=?ref();
??return?{
????instance:?instance({
??????...config,
??????cancelToken:?new?axios.CancelToken((c)?=>?{
????????cancel.value?=?c;
??????})
????}),
????cancel
??};
}
復制代碼
例
import?{?IUser?}?from?'@/interface/User';
export?function?getUserInfo(id:?number)?{
??return?request({
????url:?'/getUserInfo',
????method:?'get',
????params:?{
??????id
????}
??});
}
復制代碼
需要注意的是,示例中的錯誤信息經(jīng)過了統(tǒng)一性的封裝,如果希望錯誤有一致性的表現(xiàn),可以封裝一個類型接收錯誤,建議與后臺返回的數(shù)據(jù)結構一致。
現(xiàn)在,咱們使用這個request函數(shù),傳入對應的泛型,就可以享受到對應的類型提示。
useRequest
如何使用
想要設計useRequest,那現(xiàn)在思考一下,什么樣的useRequest使用起來,能讓我們感到快樂,拿上面的基礎示例和queryKey示例來看,大家可以參考一下VueRequest或者aHooks的用法,我是看了他們的用法來構思我的設計的。
比如一個普通的請求,我希望簡單的使用data、loading、err等來接受數(shù)據(jù),比如
const?{?run,?data,?loading,?cancel,?err?}?=?useRequest(getUserInfo,?{
????manual:?true
})
復制代碼
那 useRequest 的簡單模型好像是這樣的
export?function?useRequest(service,?options)?{
????return?{
????????data,
????????run,
????????loading,
????????cancel,
????????err
????}
}
復制代碼
傳入一個請求函數(shù)和配置信息,請求交由useRequest內(nèi)部接管,最后將data loading等信息返回即可。
那加上queryKey呢
const?{?run,?querise?}?=?useRequest(getUserInfo,?{
????manual:?true,
????queryKey:?(id)?=>?String(id)
})
復制代碼
似乎還要返回一個querise,于是變成了
export?function?useRequest(service,?options)?{
????return?{
????????data,
????????run,
????????loading,
????????cancel,
????????err,
????????querise
????}
}
復制代碼
對應的querise[key]選項,還要額外維護data loading等屬性,這樣對于useRequest內(nèi)部來說是不是太割裂了呢,大家可以嘗試一下,因為我就是一開始做簡單版本之后再來考慮queryKey功能的,代碼是十分難看的。
添加泛型支持
上面的偽代碼我們都沒有添加泛型支持,那我們需要添加哪些泛型,上面request的例子其實比較明顯了
import?{?IUser?}?from?'@/interface/User';
export?function?getUserInfo(id:?number)?{
??return?request({
????url:?'/getUserInfo',
????method:?'get',
????params:?{
??????id
????}
??});
}
復制代碼
對于id,作為請求參數(shù),我們每一個請求都不確定,這里肯定是需要一個泛型的,IUser作為返回類型的泛型,需要被useRequest正確識別,必然也是需要一個泛型的。
其中,請求參數(shù)的泛型,為了使用的方便,我們定義其extends any[],必須是一個數(shù)組,使用...args的形式傳入到request的instance中執(zhí)行。
service的類型需要與request類型保持一致, options的類型按需要實現(xiàn)的功能參數(shù)添加,于是,我們得到了如下一個useRequest。
//?/src/hooks/useRequest/types.ts
export?type?Serviceextends?any[]>?=?(...args:?P)?=>?RequestResponse;
//?可按對應的配置項需求擴展
export?interface?Options?{
??//?是否手動發(fā)起請求
??manual?:?boolean;
??//?當?manual?為false時,自動執(zhí)行的默認參數(shù)
??defaultParams?:?P;
??//?依賴項更新
??refreshDeps?:?WatchSource<any>[];
??refreshDepsParams?:?ComputedRef;
??//?是否關閉重復請求,當queryKey存在時,該字段無效
??repeatCancel?:?boolean;
??//?并發(fā)請求
??queryKey?:?(...args:?P)?=>?string;
??//?成功回調(diào)
??onSuccess?:?(response:?AxiosResponse>,?params:?P )?=>?void;
??//?失敗回調(diào)
??onError?:?(err:?ErrorData,?params:?P)?=>?void;
}
復制代碼
//?/src/hooks/useRequest/index.ts
export?function?useRequest<T,?P?extends?any[]>(
????service:?Service,
????options:?Options?=?{}
){
????return?{
????????data,?//?data?類型為T
????????run,
????????loading,
????????cancel,
????????err,
????????querise
????}
}
復制代碼
queryKey 的問題
上面我們提到了,queryKey請求和普通請求如果單獨維護,不僅割裂,而且代碼還很混亂,那有沒有什么辦法來解決這個問題呢,用js的思想來看這個問題,假設我現(xiàn)在有一個對象querise,我需要將不同請求參數(shù)的請求相關數(shù)據(jù)維護到querise中,比如run(1),那么querise應該為
const?querise?=?{
??1:?{
??????data:?null,
??????loading:?false
??????...
??}
}
復制代碼
這是在queryKey的情況下,那沒有queryKey呢?很簡單,維護到default對象唄,即
const?querise?=?{
??default:?{
??????data:?null,
??????loading:?false
??????...
??}
}
復制代碼
為了確保默認key值的唯一性,我們引入Symbol,即
const?defaultQuerise?=?Symbol('default');
const?querise?=?{
??[defaultQuerise]:?{
??????data:?null,
??????loading:?false
??????...
??}
}
復制代碼
因為我們會使用reactive包裹querise,所以想要滿足非queryKey請求時,使用默認導出的data loading err等數(shù)據(jù),只需要
return?{
????run,
????querise,
????...toRefs(querise[defaulrQuerise])
}
復制代碼
好了,需要討論的問題完了,我們來寫代碼
完整代碼
//?/src/hooks/useRequest/types.ts
import?{?Canceler,?AxiosResponse?}?from?'axios';
import?{?ComputedRef,?WatchSource,?Ref?}?from?'vue';
export?interface?Response?{
??code:?number;
??data:?T;
??msg:?string;
}
export?type?AppAxiosResponseany>?=?AxiosResponse>;
export?interface?RequestResponse{
??instance:?Promise>;
??cancel:?Refundefined>
}
export?type?Serviceextends?any[]>?=?(...args:?P)?=>?RequestResponse;
export?interface?Optionsextends?any[]>?{
??//?是否手動發(fā)起請求
??manual?:?boolean;
??//?當?manual?為false時,自動執(zhí)行的默認參數(shù)
??defaultParams?:?P;
??//?依賴項更新
??refreshDeps?:?WatchSource<any>[];
??refreshDepsParams?:?ComputedRef;
??//?是否關閉重復請求,當queryKey存在時,該字段無效
??repeatCancel?:?boolean;
??//?重試次數(shù)
??retryCount?:?number;
??//?重試間隔時間
??retryInterval?:?number;
??//?并發(fā)請求
??queryKey?:?(...args:?P)?=>?string;
??//?成功回調(diào)
??onSuccess?:?(response:?AxiosResponse>,?params:?P )?=>?void;
??//?失敗回調(diào)
??onError?:?(err:?ErrorData,?params:?P)?=>?void;
}
export?interface?IRequestResult?{
??data:?T?|?null;
??loading:?boolean;
??cancel:?Canceler;
??err?:?ErrorData;
}
export?interface?ErrorData?{
??code:?number?|?string;
??data:?T;
??msg:?string;
}
復制代碼
//?/src/hooks/useRequest/axios.ts
import?{?ref?}?from?'vue';
import?{?AppAxiosResponse,?RequestResponse?}?from?'./types';
import?axios,?{?AxiosRequestConfig,?Canceler?}?from?'axios';
const?instance?=?axios.create({
??timeout:?30?*?1000,
??baseURL:?'/api'
});
instance.interceptors.request.use(undefined,?(err)?=>?{
??console.log('request-error',?err);
});
instance.interceptors.response.use((res:?AppAxiosResponse)?=>?{
??if(res.data.code?!==?200)?{
????return?Promise.reject(res.data);
??}
??return?res;
},?(err)?=>?{
??if(axios.isCancel(err))?{
????return?Promise.reject({
??????code:?10000,
??????msg:?'Cancel',
??????data:?null
????});
??}
??if(err.code?===?'ECONNABORTED')?{
????return?Promise.reject({
??????code:?10001,
??????msg:?'超時',
??????data:?null
????});
??}
??console.log('response-error',?err.toJSON());
??return?Promise.reject(err);
});
export?function?request<T>(config:?AxiosRequestConfig):?RequestResponse<T>?{
??const?cancel?=?ref();
??return?{
????instance:?instance({
??????...config,
??????cancelToken:?new?axios.CancelToken((c)?=>?{
????????cancel.value?=?c;
??????})
????}),
????cancel
??};
}
復制代碼
import?{?isFunction?}?from?'lodash';
import?{?reactive,?toRefs,?watch?}?from?'vue';
import?{?IRequestResult,?Options,?Service,?ErrorData?}?from?'./types';
const?defaultQuerise?=?Symbol('default');
export?function?useRequest<T,?P?extends?any[]>(
??service:?Service,
??options:?Options?=?{}
)?{
??const?{
????manual?=?false,
????defaultParams?=?[]?as?unknown?as?P,
????repeatCancel?=?false,
????refreshDeps?=?null,
????refreshDepsParams?=?null,
????queryKey?=?null
??}?=?options;
??const?querise?=?reactivestring?|?symbol,?IRequestResult>>({
????[defaultQuerise]:?{
??????data:?null,
??????loading:?false,
??????cancel:?()?=>?null,
??????err:?undefined
????}
??});
??const?serviceFn?=?async?(...args:?P)?=>?{
????const?key?=?queryKey???queryKey(...args)?:?defaultQuerise;
????if?(!querise[key])?{
??????querise[key]?=?{}?as?any;
????}
????if?(!queryKey?&&?repeatCancel)?{
??????querise[key].cancel();
????}
????querise[key].loading?=?true;
????const?{?instance,?cancel?}?=?service(...args);
????querise[key].cancel?=?cancel?as?any;
????instance
??????.then((res)?=>?{
????????querise[key].data?=?res.data.data;
????????querise[key].err?=?undefined;
????????if?(isFunction(options.onSuccess))?{
??????????options.onSuccess(res,?args);
????????}
??????})
??????.catch((err:?ErrorData)?=>?{
????????querise[key].err?=?err;
????????if?(isFunction(options.onError))?{
??????????options.onError(err,?args);
????????}
??????})
??????.finally(()?=>?{
????????querise[key].loading?=?false;
??????});
??};
??const?run?=?serviceFn;
??//?依賴更新
??if?(refreshDeps)?{
????watch(
??????refreshDeps,
??????()?=>?{
????????run(...(refreshDepsParams?.value?||?([]?as?unknown?as?P)));
??????},
??????{?deep:?true?}
????);
??}
??if?(!manual)?{
????run(...defaultParams);
??}
??return?{
????run,
????querise,
????...toRefs(querise[defaultQuerise])
??};
}
復制代碼
需要防抖 節(jié)流 錯誤重試等功能,僅需要擴展Options類型,在useRequest中添加對應的邏輯即可,比如使用lodash包裹run函數(shù),這里只是將最基本的功能實現(xiàn)搞定了,一部分小問題以及擴展性的東西沒有過分糾結。
結語
當前的 useRequest 還是比較簡陋,希望有想法或者建議的朋友可以一起討論,有什么問題也可以問我,謝謝。
感謝
本次分享到這里就結束了,感謝您的閱讀,如果本文對您有什么幫助,別忘了動動手指點個贊 ?? 和關注。
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個小忙:
1. 點個「在看」,讓更多人也能看到這篇文章 2. 訂閱官方博客?www.inode.club?讓我們一起成長
點贊和在看就是最大的支持
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個小忙:
點贊和在看就是最大的支持
