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

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

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

    写在之前

    实现 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.__lader_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     Solana     882 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 21:13 PVG 05:13 LAX 14:13 JFK 17:13
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86