一塊兒寫一個即插即用的Vue Loading插件

不管最終要實現怎樣的網站,Loading狀態都是必不可少的一環,給用戶一個過渡喘息的機會也給服務器一個遞達響應的時間。html

從使用方式提及

無論從0開始寫起仍是直接下載的Loading插件,都會抽象爲一個組件,在用到的時候進行加載Loading,或者經過API手動進行show或者hidevue

<wait>
</wait>
...
this.$wait.show()
await fetch('http://example.org')
this.$wait.hide()

或者經過Loading狀態進行組件間的切換ios

<loader v-if="isLoading">
</loader>
<Main v-else>
</Main>

。要想註冊成全局狀態,還須要給axios類的網絡請求包添加攔截器,而後設置一個全局Loading狀態,每次有網絡請求或者根據已經設置好的URL將Loading狀態設置爲加載,請求完成後在設置爲完成git

註冊axios攔截器:github

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,何時顯示真正的組件。axios

<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判斷足以讓人煩躁不已...api

整理思路

Loading的核心很簡單,就是請求服務器時須要顯示Loading,請求完了再還原回來,這個思路實現起來並不費力,只不過使用方式上逃不開上面的顯式調用的方式。順着思路來看,能進行Loading設置的地方有,服務器

  1. 設置全局攔截,請求開始前設置狀態爲加載
  2. 設置全局攔截,請求結束後設置狀態爲完成
  3. 在觸發請求的函數中進行攔截,觸發前設置爲加載,觸發後設置爲完成
  4. 判斷請求後的數據是否爲非空,若是非空則設置爲完成

最終能夠實現的狀況上,進行全局攔截設置,而後局部的判斷是最容易想到也是最容易實現的方案。給每一個觸發的函數設置beforeafter看起來美好,但實現起來簡直是災難,咱們並無beforeafter這兩個函數鉤子來告訴咱們函數何時調用了和調用完了,本身實現吧坑不少,不實現吧又沒得用只能去原函數裏一個個寫上。只判斷數據侷限性很大,只有一次機會。網絡

既然是即插即用的插件,使用起來就得突出一個簡單易用,基本思路上也是使用全局攔截,但局部判斷方面與常規略有不一樣,使用數據綁定(固然也能夠再次全局響應攔截),我們實現起來吧~。async

樣式

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設置爲進度條好了。

演示地址

GIF.gif

綁定數據與URL

提供外部使用接口

上面思路中提到,這個插件是用全局攔截與數據綁定製做的:

  1. 暴露一個 source 屬性,從使用的組件中獲取出要綁定的數據。
  2. 暴露一個 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組件。

TIM截圖20191029135601.png

訂閱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就會起做用將加載狀態設置回完成

TIM截圖20191029143017.png

使用槽來適配原來的組件

寫完上面這些你可能有些疑問,怎麼將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-ifCSS選擇器且樣式是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是否是該隱藏,也不用調用showhide在請求的方法裏打補丁。

測試地址

其餘

上面的經過綁定數據來判斷是否已經響應,若是請求後的數據不會更新,那你也能夠直接在axios的response裏作攔截進行訂閱發佈模式的響應。

最後

咳咳,又到了嚴(hou)肅(yan)認(wu)真(chi)求Star環節了,附上完整的項目地址(我不會告訴你上面的測試地址裏的代碼也很完整的,毫不會!)。

相關文章
相關標籤/搜索