這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 loading的展示和取消可以說是每個前端對介面的時候都要關心的一個問題。這篇文章將要幫你解決的就是如何結合axios更加簡潔的處理loading展示與取消的邏輯。 首先在我們平時處理業務的時候loading一般分為三種:按鈕loadin ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
loading的展示和取消可以說是每個前端對介面的時候都要關心的一個問題。這篇文章將要幫你解決的就是如何結合axios更加簡潔的處理loading展示與取消的邏輯。
首先在我們平時處理業務的時候loading一般分為三種:按鈕loading,局部loading,還有全局loading。
按鈕loading
其實想寫這篇博客的誘因也是因為這個按鈕loading ,在大多數時候我們寫按鈕loading業務的時候是這樣寫的。
const loading = ref(false) try { loading.value = true const data = await axios.post(`/api/data`) } finally { loading.value = false }
或者這樣的寫的
const loading = ref(false) loading.value = true axios.post(`/api/data`) .then(data => { //do something }) .finally(() => { loading.value = false })
可以看到 我們總要處理loading的開始與結束狀態。而且好多介面都要這麼寫。這樣太繁瑣了,那我們可不可以這樣呢?
vue3版本
const loading = ref(false) const data = await axios.post(`/api/data`,{loading:loading})
把loading的狀態給axios統一處理。這樣代碼是不是就簡潔多了呢?處理方式也很簡單。
// 請求攔截器 axios.interceptors.request.use(config = >{ if (config.loading) { config.loading.value = true } }) // 響應攔截器 axios.interceptors.response.use( response => { if (response.config.loading) { res.config.loading.value = false } }, error => { if (error.config.loading) { config.loading.value = false } } )
我們只需要在axios的攔截器中改變loading的值就可以,註意一定要傳入一個ref類的值。這種寫法也僅適用於vue3。vue2是不行的。
vue2版本
在vue2裡面我們可能會想到這樣寫。
<template> <a-button loading="loading.value"> 保存 </a-button> </template> <script> export default { data () { return { loading: { value: false }, } }, mounted () { const data = await axios.post(`/api/data`,{loading:this.loading}) }, } </script> //攔截器和vue3寫法一樣
但是很遺憾這樣是無法生效的。原因如下
//介面調用 axios.post(介面地址,配置項) //攔截器 axios.interceptors.request.use(配置項 => {})
在axios中我們介面調用傳入的配置項 和 攔截器返回的配置項 並不是同一個記憶體地址。axios做了深拷貝處理。所以傳入的loading對象和返回的loading對象並不是同一個對象。所以我們在攔截器中修改是完全沒有用的。
可是vue3為什麼可以呢?因為ref返回的對象是RefImpl類的實例 並不是一個普通的對象,axios在做深拷貝的時候沒有處理這種實例對象。 所以我們就可以從這裡出發來改造一下我們的axios寫法。代碼如下:
axios代碼:
const _axios = axios.create({ method: `post`, baseURL: process.env.VUE_APP_BASE_URL, }) //註意:攔截器中比vue3多了個loading!!! // 請求攔截器 _axios.interceptors.request.use(config = >{ if (config.loading) { config.loading.loading.value = true } }) // 響應攔截器 _axios.interceptors.response.use( response => { if (response.config.loading) { res.config.loading.loading.value = false } }, error => { if (error.config.loading) { config.loading.loading.value = false } } ) export const post = (url, params, config) => { if (config?.loading) { class Loading { loading = config.loading } config.loading = new Loading() } return _axios.post(url, params, config) }
使用方式:
<template> <a-button loading="loading.value"> 保存 </a-button> </template> <script> import { post } from '@api/axios' export default { data () { return { //這裡的loading可以取任意名字。但是裡面必須有value loading: { value: false }, } }, mounted () { const data = await post(`/api/data`,{loading:this.loading}) }, } </script>
可以看到實現的原理也很簡單。我們在axios裡面把出傳入的config中的loading對象也變成一個實例對象就好了。在實例對象中記錄我們傳入的對象,也是以為這裡我們會比vue3的寫法多一個loading,從而實現響應式。
高階函數版本
以上的方案看起來還是很不友好。如果我們不拘泥於在攔截器中封裝呢?
axios代碼如下:
const _axios = axios.create({ method: `post`, baseURL: import.meta.env.VITE_BASE_URL, }) // 請求攔截器 _axios.interceptors.request.use() // 響應攔截器 _axios.interceptors.response.use() async setRequest (callBack, url, params, config) { //添加按鈕的loading if (config.loading) { config.loading.value = true } try { return await callBack(url, params, config) } catch (error) { return Promise.reject(error) } finally { //關閉按鈕的loading狀態 if (config.loading) { config.loading.value = false } } } export const post = (url, params, config) => { return setRequest(_axios.post,url, params, config) }
以上代碼僅僅是個代碼思路。我們可以使用高階函數二次封裝 axios的post函數。get等函數也是一樣的,這裡就不一一舉例了。
局部loading
局部loading的添加有兩種方式:
- 使用自定義指令 傳入true和false 。這樣的缺陷是不夠靈活,組件內的元素就很難局部添加了, 只能全組件添加。值得一提的是,改變true和false的邏輯就可以用我們上述的按鈕loading方法。具體的實現方式這裡就不再講述了,如果需要的話可以評論區留言。
- 在axios中封裝。每次調用介面的時候傳入需要添加loading的dom。介面調用完畢刪除dom。實現方法如下。
這裡是vue3 + antdV3 技術棧的一個封裝。這裡用hooks把設置刪除loading的邏輯給拆了出去。
axios代碼:
const _axios = axios.create({ method: `post`, baseURL: import.meta.env.VITE_BASE_URL, }) const { setLoading, deleteLoading } = useAxiosConfig() // 請求攔截器 _axios.interceptors.request.use(config = >{ setLoading(config) }) // 響應攔截器 _axios.interceptors.response.use( response => { deleteLoading(res.config) }, error => { deleteLoading(res.config) } ) //這裡也可以 不拘泥於攔截器。使用上述高階函數的方式。 export const post = (url, params, config) => { return _axios.post(url, params, config) }
hooks代碼
import { createApp } from 'vue' import QSpin from '@/components/qSpin/QSpin.vue' import type { RequestConfig, AxiosError } from '@/types/services/http' export default function () { /** 使用WeakMap類型的數據 鍵名所指向的對象可以被垃圾回收 避免dom對象的鍵名記憶體泄漏 */ const loadingDom = new WeakMap() /** * 添加局部loading * @param config */ const setLoading = (config: RequestConfig) => { const loadingTarget = config.dom if (loadingTarget === undefined) return const loadingDomInfo = loadingDom.get(loadingTarget) if (loadingDomInfo) { loadingDomInfo.count++ } else { const appExample = createApp(QSpin) const loadingExample = appExample.mount(document.createElement(`div`)) as InstanceType<typeof QSpin> loadingTarget.appendChild(loadingExample.$el) loadingExample.show(loadingTarget) loadingDom.set(loadingTarget, { count: 1, //記錄當前dom的loading次數 appExample, }) } } /** * 刪除局部loading * @param config */ const deleteLoading = (config: RequestConfig) => { const loadingTarget = config.dom if (loadingTarget === undefined) return const loadingDomInfo = loadingDom.get(loadingTarget) if (loadingDomInfo) { if (--loadingDomInfo.count === 0) { loadingDom.delete(loadingTarget) loadingDomInfo.appExample.unmount() } } } return { setLoading, deleteLoading } }
基礎邏輯,很簡單。只需要介面請求的時候的添加loading ,介面響應完成的時候刪除loading。但是隨之而來的就有一個問題,如果多個介面同時請求 或者 一個介面頻繁請求需要覆蓋的都是同一個dom,這樣我們添加的loading就會有很多個相同的,相互覆蓋。因此上述代碼定義了一個loadingDom 記錄當前正在loading的dom有哪些,如果有一樣的進來的 就把count加一 ,結束後就把count減一。如果count為零則刪除loading。
使用實例代碼:
<template> <div> <div ref="head_dom">我是頭部數據</div> <a-card ref="card_dom">我是卡片內容</a-card> </div> </template> <script setup lang="ts"> import { post } from '@api/axios' import { ref, onMounted } from 'vue' const head_dom = ref() const card_dom = ref() //這邊寫了兩個是為了演示下 直接在html標簽上面綁定ref拿到的就是dom。在組件上面拿到的是組件實例要$el一下 onMounted(async () => { const data1 = await post(`/api/head`, { dom: head_dom.value }) const data2 = await post(`/api/card`, { dom: card_dom.value.$el }) }) </script>
下麵簡單解釋下hooks代碼中QSpin組件的代碼。
<template> <div v-show="visible" class="q-spin"> <spin tip="載入中" /> </div> </template> <script setup lang="ts"> import { Spin } from 'ant-design-vue' import { ref } from 'vue' const visible = ref(false) const show = (dom: HTMLElement) => { visible.value = true dom.style.transform = dom.style.transform || `translate(0)` } defineExpose({ show }) </script> <style scoped lang="less"> .q-spin { position: fixed; z-index: 999; top: 0; bottom: 0; left: 0; right: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: rgb(0 0 0 / 10%); } </style>
這裡是對antdv3的Spin組件做了一個簡單的二次封裝。主要講解的就是一個loading覆蓋傳入dom的方法。
大多數地方使用的方式都是 relative 和 absolute 定位組合的方式,但是這裡採用了transform 和 fixed定位組合的方式。因為我們的項目中可能出現這樣一種情況
<div style="position: relative"> <div ref="div_dom"> <div style="position: absolute">我是內容</div> </div> </div>
假如 我們要給中間的的div添加loading, 使用relative 和 absolute 定位組合的方式。那麼中間的div就會在樣式表種添加一個position: relative的屬性,這樣代碼就會變成這樣
<div style="position: relative"> <div style="position: relative" ref="div_dom"> <div style="position: absolute">我是內容</div> </div> </div>
很明顯 我們第三層div定位的根節點就從第一層變成了第二層,這樣就會有可能導致我們樣式的錯亂。因此筆者採用了transform 和 fixed定位組合的方式。雖然上述的情況可能還會出現 但是會大大減少出現的可能性。
全局loading
這個就很簡單了。如果你封裝好了局部的loading 直接在配置項的dom中傳入document.body即可!