Vue 全域 / 非全域 Loading 的等待特效 ( 支援多個併發請求 )
解決問題
在前端工程中,常常會透過 ajax 向後端做非同步資料交換,但api 可能會因各種因素延遲回應,為提昇更好的使用者體驗,通常都會做一個過場的動畫,讓使用者得知正在加載資料。
而大多數套件設計的相當簡易,背後可能只是一個 boolean 值,在多個併發的非同步的ajax request ,只要有任一個 request,提前回來就被關掉 loading 的過場動畫。
設計原理
在 vuex 中 做個計數器功能, ajax 發出 request 時,就呼叫 store 中的increment方法,讓count 就加1,完成或失敗則 count 減1,並提供 isLoading 的 getter 給畫面判斷, 透過封裝 axios 來由外部傳入參數決定需不需要 show loading 轉場動畫。
- request 進來 就 count + 1
- request 失敗 count - 1 ( 參數錯誤,不會往下走,也不會走到 reponse )
- response 成功 count - 1
- response 失敗 count - 1 ( time out 、 server error、 cancel )
Vuex 的程式碼
import { generatorUUID } from '@/utils'
var _ = require('lodash')
const state = {
count: []
}
// getters
const getters = {
isLoading: (state) => {
return state.count.length > 0
},
removeByItems: (state) => {
return state.count.filter(x => x.isRemoveByRequestId === true)
}
}
// mutations
const mutations = {
increment(state, requestId) {
const isRemoveByRequestId = requestId !== '' && requestId !== undefined
requestId = requestId || generatorUUID()
var newLoading = { requestId: requestId, isRemoveByRequestId: isRemoveByRequestId }
state.count.push(newLoading)
},
decrement(state, requestId) {
if (requestId) {
var index = state.count.findIndex(item => item.requestId === requestId)
state.cart.splice(index, 1)
} else {
const newCount = _.cloneDeep(state.count).filter(x => x.isRemoveByRequestId === false)
if (newCount.length > 0) {
newCount.shift()
state.count = newCount
}
}
}
}
// actions
const actions = {
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
封裝 axios
function apiAxios(method, url, params, hideLoading, requestId) {
// 顯示loading
const appParams = {}
params = { ...appParams, ...params }
const httpDefault = {
method: method,
baseURL: baseURL,
url: url,
hideLoading: hideLoading,
requestId: requestId,
// `params` 是即將與請求一起發送的 URL 參數
// `data` 是作為請求主體被發送的數據
params: method === 'GET' || method === 'DELETE' ? params : null,
data: method === 'POST' || method === 'PUT' ? params : null,
timeout: 100000
}
if (httpDefault['hideLoading'] === false) {
store.commit('loading/increment', httpDefault['requestId'])
}
return new Promise((resolve, reject) => {
axios(httpDefault)
.then((res) => {
if (httpDefault['hideLoading'] === false) {
store.commit('loading/decrement', requestId)
}
resolve(successState(res))
})
.catch((response) => {
if (httpDefault['hideLoading'] === false) {
store.commit('loading/decrement', requestId)
}
errorState(response)
})
})
}
在 Layout 加上 Loading box 的組件,並 監聽 getter 中的 isLoading 屬性,來顯示 Loading box 。
倘若 Call Api 時不需要顯示 loading 時,則多傳遞一個 true 的參數
Vue.prototype.$getAxios(${process.env.VUE_APP_BASE_API}/api/v1/home/query
, null, true)
為了之後擴充,預留一個彈性,各別元件若需要控制,可以由外面把 request id 傳入,元件在去 watch loading count 物件中 isRemoveByRequestId 屬性的資料
export function getHomeQuery(requestId = ‘’) {
return Vue.prototype.$getAxios(${process.env.VUE_APP_BASE_API}/api/v1/home/query
, null, true, requestId)
}
延伸議題 - Skeleton
現今越來越多網站 採用 Skeleton 提昇使用者的使用體驗,可以之後列成優化項目。