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