V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
cyrbuzz
V2EX  ›  分享创造

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

  •  2
     
  •   cyrbuzz ·
    HuberTRoy · 2019-10-29 21:01:27 +08:00 · 3450 次点击
    这是一个创建于 1633 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写在之前

    实现 Loading 思路上并不困难,只不过是根据请求前后进行设置而已,可当要设置的状态越来越多又不能全局统一设置时,就又变得十分繁琐重复。在 Github 和各个社区站里搜 Loading 的插件,每个插件尽管样式上天差地别,但使用起来都没有太大的差别,要么给定 API,手动showhide,要么封装了一个样式组件,还是需要手动判断什么时候该显示什么时候不该显示。

    这个插件所想要解决的问题也是相对容易的自动进行 Loading 状态切换

    经过几天的改进,这个版本的插件已经基本符合预期,感谢 @TomVista @shintendo @lllllliu @luoway 在上一个帖子的指教~。

    代码中有任何问题 /可以改进的地方都希望能指教一下~(*^▽^*)。

    从使用方式说起

    不管从 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. 判断请求后的数据是否为非空,如果非空则设置为完成

    最终可以实现的情况上,进行全局拦截设置,然后局部的判断是最容易想到也是最容易实现的方案。给每个触发的函数设置beforeafter看起来美好,但实现起来简直是灾难,我们并没有beforeafter这两个函数钩子来告诉我们函数什么时候调用了和调用完了,自己实现吧坑很多,不实现吧又没得用只能去原函数里一个个写上。只判断数据局限性很大,只有一次机会。

    既然是即插即用的插件,使用起来就得突出一个简单易用,基本思路上也是使用全局拦截,但局部判断方面与常规略有不同,使用数据绑定(当然也可以再次全局响应拦截),咱们实现起来吧~。

    样式

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

    16 条回复    2019-11-01 22:43:42 +08:00
    lllllliu
        1
    lllllliu  
       2019-10-30 09:30:08 +08:00
    鼓掌鼓掌
    missnote
        2
    missnote  
       2019-10-30 10:18:02 +08:00
    先 mark 再鼓掌!
    cyrbuzz
        3
    cyrbuzz  
    OP
       2019-10-30 12:43:53 +08:00
    @lllllliu
    再次感谢,嘿嘿~。
    cyrbuzz
        4
    cyrbuzz  
    OP
       2019-10-30 12:44:07 +08:00
    @missnote
    谢谢支持~。
    royluo
        5
    royluo  
       2019-10-30 14:10:09 +08:00
    大佬 666 ,是不是可以考虑将 loading 的圈圈做成 props,这样 可以自定义 loading 样式
    cyrbuzz
        6
    cyrbuzz  
    OP
       2019-10-30 18:52:20 +08:00   ❤️ 1
    @royluo
    嗯嗯,现在还没有做成独立的插件,完善后的最终结果应该是如你所说自定义样式。
    Wichna
        7
    Wichna  
       2019-10-31 00:36:13 +08:00 via Android
    非常不错!
    royluo
        8
    royluo  
       2019-10-31 10:10:12 +08:00   ❤️ 1
    @cyrbuzz 强强强 期待独立组件!
    cyrbuzz
        9
    cyrbuzz  
    OP
       2019-10-31 14:11:04 +08:00
    @Wichna
    能得到肯定真是太好了~。
    cyrbuzz
        10
    cyrbuzz  
    OP
       2019-10-31 19:22:33 +08:00
    @royluo
    Hi~,发布了一个 0.0.11 版本,可以试试喔,再次感谢支持~~。
    安装
    ```
    npm install v-loader-helper
    ```
    Github:
    https://github.com/HuberTRoy/v-loader-helper
    royluo
        11
    royluo  
       2019-11-01 13:59:02 +08:00
    @cyrbuzz 大佬🐂 等会就试试~
    cyrbuzz
        12
    cyrbuzz  
    OP
       2019-11-01 16:05:18 +08:00
    @royluo
    非大佬,一起进步~。(*^▽^*)
    royluo
        13
    royluo  
       2019-11-01 17:51:24 +08:00
    @cyrbuzz 刚仔细看了下 loading 的实现, 就觉得这个 Proxy 用的妙啊
    royluo
        14
    royluo  
       2019-11-01 17:56:34 +08:00
    @cyrbuzz 还有就是如果有多个 loading 的组件 那是不是就会造成 __loader_checks 这个数组过于大, 如果在 loader 卸载的时候把自身注册的 handler 去掉感觉会好点?
    cyrbuzz
        15
    cyrbuzz  
    OP
       2019-11-01 19:06:03 +08:00
    @royluo
    一般不会造成性能问题。
    不过 loader 卸载时去掉自身注册的 handler 确实是应该具有的功能。
    另外你可以直接提一个 PR~,共同改进~。
    royluo
        16
    royluo  
       2019-11-01 22:43:42 +08:00
    @cyrbuzz 哈哈好的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5135 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 01:18 · PVG 09:18 · LAX 18:18 · JFK 21:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.