一起写一个即插即用的Vue Loading插件

无论最终要实现怎样的网站,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设置的地方有,

1. 设置全局拦截,请求开始前设置状态为加载

2. 设置全局拦截,请求结束后设置状态为完成

3. 在触发请求的函数中进行拦截,触发前设置为加载,触发后设置为完成

4. 判断请求后的数据是否为非空,如果非空则设置为完成。

最终可以实现的情况上,进行全局拦截设置,然后局部的判断是最容易想到也是最容易实现的方案。给每个触发的函数设置`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

提供外部使用接口

上面思路中提到,这个插件是用全局拦截与数据绑定制作的:

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组件。


订阅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环节了,附上完整的项目地址(我不会告诉你上面的测试地址里的代码也很完整的,绝不会!)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容