無論最終要實現怎樣的網站,Loading狀態都是必不可少的一環,給用戶一個過渡喘息的機會也給服務器一個遞達響應的時間。
從使用方式說起
不管從0開始寫起還是直接下載的Loading插件,都會抽象爲一個組件,在用到的時候進行加載Loading,或者通過API手動進行show或者hide
<wait>
</wait>
...
this.$wait.show()
await fetch('http://example.org')
this.$wait.hide()
或者通過Loading狀態進行組件間的切換
<loader v-if="isLoading">
</loader>
<Main v-else>
</Main>
。要想註冊成全局狀態,還需要給axios類的網絡請求包添加攔截器,然後設置一個全局Loading狀態,每次有網絡請求或者根據已經設置好的URL將Loading狀態設置爲加載,請求完成後在設置爲完成。
註冊axios攔截器:
let loadingUrls = [
`${apiUrl}/loading/`,
`${apiUrl}/index/`,
`${apiUrl}/comments/`,
...
]
axios.interceptors.request.use((config) => {
let url = config.url
if (loadingUrls.indexOf('url') !== -1) {
store.loading.isLoading = true
}
})
axios.interceptors.response.use((response) => {
let url = response.config.url
if (loadingUrls.indexOf('url') !== -1) {
store.loading.isLoading = false
}
})
使用時在每個組件下獲取出loading狀態,然後判斷什麼時候顯示loading,什麼時候顯示真正的組件。
<template>
<div>
<loader v-if="isLoading">
</loader>
<Main v-else>
</Main>
</div>
</template>
<script>
...
components: {
loader
},
computed: {
isLoading: this.$store.loading.isLoading
},
async getMainContent () {
// 實際情況下State僅能通過mutations改變.
this.$sotre.loading.isLoading = false
await axios.get('...')
this.$sotre.loading.isLoading = false
},
async getMain () {
await getMainContent()
}
...
</script>
在當前頁面下只有一個需要Loading的狀態時使用良好,但如果在同一個頁面下有多個不同的組件都需要Loading,你還需要根據不同組件進行標記,好讓已經加載完的組件不重複進入Loading狀態...隨着業務不斷增加,重複進行的Loading判斷足以讓人煩躁不已...
整理思路
Loading的核心很簡單,就是請求服務器時需要顯示Loading,請求完了再還原回來,這個思路實現起來並不費力,只不過使用方式上逃不開上面的顯式調用的方式。順着思路來看,能進行Loading設置的地方有,
- 設置全局攔截,請求開始前設置狀態爲加載。
- 設置全局攔截,請求結束後設置狀態爲完成。
- 在觸發請求的函數中進行攔截,觸發前設置爲加載,觸發後設置爲完成。
- 判斷請求後的數據是否爲非空,如果非空則設置爲完成
最終可以實現的情況上,進行全局攔截設置,然後局部的判斷是最容易想到也是最容易實現的方案。給每個觸發的函數設置before
和after
看起來美好,但實現起來簡直是災難,我們並沒有before
和after
這兩個函數鉤子來告訴我們函數什麼時候調用了和調用完了,自己實現吧坑很多,不實現吧又沒得用只能去原函數裏一個個寫上。只判斷數據侷限性很大,只有一次機會。
既然是即插即用的插件,使用起來就得突出一個簡單易用,基本思路上也是使用全局攔截,但局部判斷方面與常規略有不同,使用數據綁定(當然也可以再次全局響應攔截),咱們實現起來吧~。
樣式
Loading嘛,必須得有一個轉圈圈才能叫Loading,樣式並不是這個插件的最主要的,這裏直接用CSS實現一個容易實現又不顯得很糙的:
<template>
<div class="loading">
</div>
</template>
...
<style scoped>
.loading {
width: 50px;
height: 50px;
border: 4px solid rgba(0,0,0,0.1);
border-radius: 50%;
border-left-color: red;
animation: loading 1s infinite linear;
}
@keyframes loading {
0% { transform: rotate(0deg) }
100% { transform: rotate(360deg) }
}
</style>
固定大小50px的正方形,使用border-radius
把它盤得圓潤一些,border
設置個進度條底座,border-left-color
設置爲進度條好了。
綁定數據與URL
提供外部使用接口
上面思路中提到,這個插件是用全局攔截與數據綁定製作的:
- 暴露一個 source 屬性,從使用的組件中獲取出要綁定的數據。
- 暴露一個 urls 屬性,從使用的組件中獲取出要攔截的URL。
<template>
...
</template>
<script>
export default {
props: {
source: {
require: true
},
urls: {
type: Array,
default: () => { new Array() }
}
},
data () {
return { isLoading: true }
},
watch: {
source: function () {
if (this.source) {
this.isLoading = false
}
}
}
}
</script>
<style scoped>
....
</style>
不用關心source是什麼類型的數據,我們只需要監控它,每次變化時都將Loading狀態設置爲完成即可,urls我們稍後再來完善它。
設置請求攔截器
攔截器中需要的操作是將請求時的每個URL壓入一個容器內,請求完再把它刪掉。
Vue.prototype.__loader_checks = []
Vue.prototype.$__loadingHTTP = new Proxy({}, {
set: function (target, key, value, receiver) {
let oldValue = target[key]
if (!oldValue) {
Vue.prototype.__loader_checks.forEach((func) => {
func(key, value)
})
}
return Reflect.set(target, key, value, receiver)
}
})
axios.interceptors.request.use(config => {
Vue.prototype.$__loadingHTTP[config.url] = config
return config
})
axios.interceptors.response.use(response => {
delete Vue.prototype.$__loadingHTTP[response.config.url]
return response
})
將其掛載在Vue實例上,方便我們之後進行調用,當然還可以用Vuex,但此次插件要突出一個依賴少,所以Vuex還是不用啦。
直接掛載在Vue上的數據不能通過computed
或者watch
來監控數據變化,咱們用Proxy
代理攔截set
方法,每當有請求URL壓入時就做點什麼事。Vue.prototype.__loader_checks
用來存放哪些實例化出來的組件訂閱了請求URL時做加載的事件,這樣每次有URL壓入時,通過Proxy
來分發給訂閱過得實例化Loading組件。
訂閱URL事件
<template>
...
</template>
<script>
export default {
props: {
source: {
require: true
},
urls: {
type: Array,
default: () => { new Array() }
}
},
data () {
return { isLoading: true }
},
watch: {
source: function () {
if (this.source) {
this.isLoading = false
}
}
},
mounted: function () {
if (this.urls) {
this.__loader_checks.push((url, config) => {
if (this.urls.indexOf(url) !== -1) {
this.isLoading = true
}
})
}
}
}
</script>
<style scoped>
....
</style>
每一個都是一個嶄新的實例,所以直接在mounted裏訂閱URL事件即可,只要有傳入urls
,就對__loader_checks
裏每一個訂閱的對象進行發佈,Loader實例接受到發佈後會判斷這個URL是否與自己註冊的對應,對應的話會將自己的狀態設置回加載,URL請求後勢必會引起數據的更新,這時我們上面監控的source
就會起作用將加載狀態設置回完成。
使用槽來適配原來的組件
寫完上面這些你可能有些疑問,怎麼將Loading時不應該顯示的部分隱藏呢?答案是使用槽來適配,
<template>
<div>
<div class="loading" v-if="isLoading" :key="'loading'">
</div>
<slot v-else>
</slot>
</div>
</template>
<script>
export default {
props: {
source: {
require: true
},
urls: {
type: Array,
default: () => { new Array() }
}
},
data () {
return { isLoading: true }
},
watch: {
source: function () {
if (this.source) {
this.isLoading = false
}
}
},
mounted: function () {
if (this.urls) {
this.__loader_checks.push((url, config) => {
if (this.urls.indexOf(url) !== -1) {
this.isLoading = true
}
})
}
}
}
</script>
<style scoped>
....
</style>
還是通過isLoading
判斷,如果處於加載那顯示轉圈圈,否則顯示的是父組件裏傳入的槽,
這裏寫的要注意,Vue這裏有一個奇怪的BUG,
<div class="loading" v-if="isLoading" :key="'loading'">
</div>
<slot v-else>
</slot>
在有<slot>
時,如果同級的標籤同時出現v-if
與CSS選擇器
且樣式是scoped
,那用CSS選擇器
設置的樣式將會丟失,<div class="loading" v-if="isLoading" :key="'loading'">
如果沒有設置key
那.loading
的樣式會丟失,除了設置key
還可以把它變成嵌套的<div v-if="isLoading"> <div class="loading"></div> </div>
。
註冊成插件
Vue中的插件有四種註冊方式,這裏用mixin來混入到每個實例中,方便使用,同時我們也把上面的axios攔截器也註冊在這裏。
import axios
import Loader from './loader.vue'
export default {
install (Vue, options) {
Vue.prototype.__loader_checks = []
Vue.prototype.$__loadingHTTP = new Proxy({}, {
set: function (target, key, value, receiver) {
let oldValue = target[key]
if (!oldValue) {
Vue.prototype.__loader_checks.forEach((func) => {
func(key, value)
})
}
return Reflect.set(target, key, value, receiver)
}
})
axios.interceptors.request.use(config => {
Vue.prototype.$__loadingHTTP[config.url] = config
return config
})
axios.interceptors.response.use(response => {
delete Vue.prototype.$__loadingHTTP[response.config.url]
return response
})
Vue.mixin({
beforeCreate () {
Vue.component('v-loader', Loader)
}
})
}
}
使用
在入口文件中使用插件
import Loader from './plugins/loader/index.js'
...
Vue.use(Loader)
...
任意組件中無需導入即可使用
<v-loader :source="msg" :urls="['/']">
<div @click="getRoot">{{ msg }}</div>
</v-loader>
根據綁定的數據和綁定的URL自動進行Loading的顯示與隱藏,無需手動設置isLoading
是不是該隱藏,也不用調用show
與hide
在請求的方法裏打補丁。
其他
上面的通過綁定數據來判斷是否已經響應,如果請求後的數據不會更新,那你也可以直接在axios的response裏做攔截進行訂閱發佈模式的響應。
最後
咳咳,又到了嚴(hou)肅(yan)認(wu)真(chi)求Star環節了,附上完整的項目地址(我不會告訴你上面的測試地址裏的代碼也很完整的,絕不會!)。