Vue3?TypeScript?實(shí)現(xiàn)useRequest詳情
前言:
自從 Vue3
更新之后,算是投入了比較大的精力寫了一個(gè)較為完善的Vue3.2 + Vite2 + Pinia + Naive UI
的B端模版,在做到網(wǎng)絡(luò)請(qǐng)求這一塊的時(shí)候,最初使用的是VueRequest
的useRequest
,但是因?yàn)?code>VueRequest的useRequest
的cancel
關(guān)閉請(qǐng)求并不是真正的關(guān)閉,對(duì)我個(gè)人來說,還是比較介意,于是在參考aHooks
和VueRequest
的源碼之后,差不多弄了一個(gè)簡(jiǎn)易的useRequest
,使用體驗(yàn)還算ok,但是因?yàn)閭€(gè)人能力以及公司業(yè)務(wù)的問題,我的版本只支持axios
,不支持fetch
,算是作為公司私有的庫(kù)使用,沒有考慮功能的大而全,也只按VueRequest
的官網(wǎng),實(shí)現(xiàn)了一部分我認(rèn)為最重要的功能。
寫的比較混亂,中間是一部分思考,可以直接拖到最后看實(shí)現(xiàn),再回來看一下我為什么選擇這么做,歡迎討論。
效果展示
一個(gè)基礎(chǔ)的useRequest
示例,支持發(fā)起請(qǐng)求
取消請(qǐng)求
請(qǐng)求成功信息
成功回調(diào)
錯(cuò)誤捕獲
queryKey
示例,單個(gè)useRequest
管理多個(gè)相同請(qǐng)求。
其余還是依賴更新
重復(fù)請(qǐng)求關(guān)閉
防抖
節(jié)流
等功能
Axios
既然咱們使用TypeScript
和axios
,為了使axios
能滿足咱們的使用需求以及配合TypeScript
的編寫時(shí)使用體驗(yàn),咱們對(duì)axios
進(jìn)行一個(gè)簡(jiǎn)單的封裝。
interface
// /src/hooks/useRequest/types.ts import { AxiosResponse, Canceler } from 'axios'; import { Ref } from 'vue'; // 后臺(tái)返回的數(shù)據(jù)類型 export interface Response<T> { code: number; data: T; msg: string; } // 為了使用方便,對(duì) AxiosResponse 默認(rèn)添加我們公用的 Response 類型 export type AppAxiosResponse<T = any> = AxiosResponse<Response<T>>; // 為了 useRequest 使用封裝的類型 export interface RequestResponse<T> { instance: Promise<AppAxiosResponse<T>>; cancel: Ref<Canceler | undefined>; }
axios的簡(jiǎn)單封裝
因?yàn)樵蹅儸F(xiàn)在沒有接入業(yè)務(wù),所以axios
只需要簡(jiǎn)單的封裝能支持咱們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<Canceler>(); 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<IUser>({ url: '/getUserInfo', method: 'get', params: { id } }); }
需要注意的是,示例中的錯(cuò)誤信息經(jīng)過了統(tǒng)一性的封裝,如果希望錯(cuò)誤有一致性的表現(xiàn),可以封裝一個(gè)類型接收錯(cuò)誤,建議與后臺(tái)返回的數(shù)據(jù)結(jié)構(gòu)一致。
現(xiàn)在,咱們使用這個(gè)request
函數(shù),傳入對(duì)應(yīng)的泛型,就可以享受到對(duì)應(yīng)的類型提示
。
useRequest
如何使用
想要設(shè)計(jì)useRequest
,那現(xiàn)在思考一下,什么樣的useRequest
使用起來,能讓我們感到快樂
,拿上面的基礎(chǔ)示例
和queryKey示例
來看,大家可以參考一下VueRequest
或者aHooks
的用法,我是看了他們的用法來構(gòu)思我的設(shè)計(jì)的。
比如一個(gè)普通的請(qǐng)求,我希望簡(jiǎn)單的使用data
、loading
、err
等來接受數(shù)據(jù),比如:
const { run, data, loading, cancel, err } = useRequest(getUserInfo, { manual: true })
那 useRequest
的簡(jiǎn)單模型好像是這樣的
export function useRequest(service, options) { return { data, run, loading, cancel, err } }
傳入一個(gè)請(qǐng)求函數(shù)
和配置信息
,請(qǐng)求交由useRequest
內(nèi)部接管,最后將data
loading
等信息返回即可。
那加上queryKey
呢
const { run, querise } = useRequest(getUserInfo, { manual: true, queryKey: (id) => String(id) })
似乎還要返回一個(gè)querise
,于是變成了
export function useRequest(service, options) { return { data, run, loading, cancel, err, querise } }
對(duì)應(yīng)的querise[key]
選項(xiàng),還要額外維護(hù)data
loading
等屬性,這樣對(duì)于useRequest
內(nèi)部來說是不是太割裂了呢,大家可以嘗試一下,因?yàn)槲揖褪且婚_始做簡(jiǎn)單版本之后再來考慮queryKey
功能的,代碼是十分難看的。
添加泛型支持
上面的偽代碼我們都沒有添加泛型
支持,那我們需要添加哪些泛型
,上面request
的例子其實(shí)比較明顯了
import { IUser } from '@/interface/User'; export function getUserInfo(id: number) { return request<IUser>({ url: '/getUserInfo', method: 'get', params: { id } }); }
對(duì)于id
,作為請(qǐng)求參數(shù),我們每一個(gè)請(qǐng)求都不確定,這里肯定是需要一個(gè)泛型
的,IUser
作為返回類型的泛型,需要被useRequest
正確識(shí)別,必然也是需要一個(gè)泛型的。
其中,請(qǐng)求參數(shù)的泛型,為了使用的方便,我們定義其extends any[]
,必須是一個(gè)數(shù)組,使用...args
的形式傳入到request
的instance
中執(zhí)行。
service
的類型需要與request
類型保持一致, options
的類型按需要實(shí)現(xiàn)的功能參數(shù)添加,于是,我們得到了如下一個(gè)useRequest
。
// /src/hooks/useRequest/types.ts export type Service<T, P extends any[]> = (...args: P) => RequestResponse<T>; // 可按對(duì)應(yīng)的配置項(xiàng)需求擴(kuò)展 export interface Options<T, P extnds any> { // 是否手動(dòng)發(fā)起請(qǐng)求 manual?: boolean; // 當(dāng) manual 為false時(shí),自動(dòng)執(zhí)行的默認(rèn)參數(shù) defaultParams?: P; // 依賴項(xiàng)更新 refreshDeps?: WatchSource<any>[]; refreshDepsParams?: ComputedRef<P>; // 是否關(guān)閉重復(fù)請(qǐng)求,當(dāng)queryKey存在時(shí),該字段無效 repeatCancel?: boolean; // 并發(fā)請(qǐng)求 queryKey?: (...args: P) => string; // 成功回調(diào) onSuccess?: (response: AxiosResponse<Response<T>>, 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<T, P>, options: Options<T, P> = {} ){ return { data, // data 類型為T run, loading, cancel, err, querise } }
queryKey的問題
上面我們提到了,queryKey請(qǐng)求
和普通請(qǐng)求
如果單獨(dú)維護(hù),不僅割裂,而且代碼還很混亂,那有沒有什么辦法來解決這個(gè)問題呢,用js
的思想來看這個(gè)問題,假設(shè)我現(xiàn)在有一個(gè)對(duì)象querise
,我需要將不同請(qǐng)求參數(shù)
的請(qǐng)求相關(guān)數(shù)據(jù)維護(hù)到querise
中,比如run(1)
,那么querise
應(yīng)該為
const querise = { 1: { data: null, loading: false ... } }
這是在queryKey
的情況下,那沒有queryKey
呢?很簡(jiǎn)單,維護(hù)到default
對(duì)象唄,即
const querise = { default: { data: null, loading: false ... } }
為了確保默認(rèn)key
值的唯一性,我們引入Symbol
,即
const defaultQuerise = Symbol('default'); const querise = { [defaultQuerise]: { data: null, loading: false ... } }
因?yàn)槲覀儠?huì)使用reactive
包裹querise
,所以想要滿足非queryKey請(qǐng)求
時(shí),使用默認(rèn)導(dǎo)出的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<T> { code: number; data: T; msg: string; } export type AppAxiosResponse<T = any> = AxiosResponse<Response<T>>; export interface RequestResponse<T>{ instance: Promise<AppAxiosResponse<T>>; cancel: Ref<Canceler | undefined> } export type Service<T, P extends any[]> = (...args: P) => RequestResponse<T>; export interface Options<T, P extends any[]> { // 是否手動(dòng)發(fā)起請(qǐng)求 manual?: boolean; // 當(dāng) manual 為false時(shí),自動(dòng)執(zhí)行的默認(rèn)參數(shù) defaultParams?: P; // 依賴項(xiàng)更新 refreshDeps?: WatchSource<any>[]; refreshDepsParams?: ComputedRef<P>; // 是否關(guān)閉重復(fù)請(qǐng)求,當(dāng)queryKey存在時(shí),該字段無效 repeatCancel?: boolean; // 重試次數(shù) retryCount?: number; // 重試間隔時(shí)間 retryInterval?: number; // 并發(fā)請(qǐng)求 queryKey?: (...args: P) => string; // 成功回調(diào) onSuccess?: (response: AxiosResponse<Response<T>>, params: P) => void; // 失敗回調(diào) onError?: (err: ErrorData, params: P) => void; } export interface IRequestResult<T> { data: T | null; loading: boolean; cancel: Canceler; err?: ErrorData; } export interface ErrorData<T = any> { 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: '超時(shí)', data: null }); } console.log('response-error', err.toJSON()); return Promise.reject(err); }); export function request<T>(config: AxiosRequestConfig): RequestResponse<T> { const cancel = ref<Canceler>(); 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<T, P>, options: Options<T, P> = {} ) { const { manual = false, defaultParams = [] as unknown as P, repeatCancel = false, refreshDeps = null, refreshDepsParams = null, queryKey = null } = options; const querise = reactive<Record<string | symbol, IRequestResult<T>>>({ [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é)流
錯(cuò)誤重試
等功能,僅需要擴(kuò)展Options
類型,在useRequest
中添加對(duì)應(yīng)的邏輯即可,比如使用lodash
包裹run
函數(shù),這里只是將最基本的功能實(shí)現(xiàn)搞定了,一部分小問題以及擴(kuò)展性的東西沒有過分糾結(jié)。
結(jié)語(yǔ)
到此這篇關(guān)于Vue3 TypeScript 實(shí)現(xiàn)useRequest詳情的文章就介紹到這了,更多相關(guān)TypeScript實(shí)現(xiàn) useRequest內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue實(shí)現(xiàn)垂直無限滑動(dòng)日歷組件
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)垂直無限滑動(dòng)日歷組件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Vue整合Node.js直連Mysql數(shù)據(jù)庫(kù)進(jìn)行CURD操作過程詳解
這篇文章主要給大家分享Vue整合Node.js,直連Mysql數(shù)據(jù)庫(kù)進(jìn)行CURD操作的詳細(xì)過程,文中有詳細(xì)的代碼講解,具有一定的參考價(jià)值,需要的朋友可以參考下2023-07-07vue3項(xiàng)目vite.config.js配置代理、端口、打包名以及圖片壓縮
這篇文章主要給大家介紹了關(guān)于vue3項(xiàng)目vite.config.js配置代理、端口、打包名以及圖片壓縮的相關(guān)資料,因?yàn)?.0版本中vue已經(jīng)內(nèi)置了很多關(guān)于webpack的配置,一般情況下開箱即用,需要修改則可以在vue.config.js文件中完成,需要的朋友可以參考下2023-12-12Vue進(jìn)階之利用transition標(biāo)簽實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)動(dòng)畫
這篇文章主要為大家詳細(xì)介紹了Vue如何利用transition標(biāo)簽實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)動(dòng)畫,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起一下2023-08-08vue項(xiàng)目引入百度地圖BMapGL鼠標(biāo)繪制和BMap輔助工具
這篇文章主要為大家介紹了vue項(xiàng)目引入百度地圖BMapGL鼠標(biāo)繪制和BMap輔助工具的踩坑分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02vue3.0運(yùn)行npm run dev報(bào)錯(cuò)Cannot find module&
本文主要介紹了vue3.0運(yùn)行npm run dev報(bào)錯(cuò)Cannot find module node:url,因?yàn)槭褂玫膎ode版本是14.15.1低于15.0.0導(dǎo)致,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10Vue出現(xiàn)did you register the component 
這篇文章主要介紹了Vue出現(xiàn)did you register the component correctly?解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03vue.js刪除動(dòng)態(tài)綁定的radio的指定項(xiàng)
這篇文章主要介紹了vue.js刪除動(dòng)態(tài)綁定的radio的指定項(xiàng),需要的朋友可以參考下2017-06-06