无论最终要实现怎样的网站,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 环节了,附上完整的项目地址(我不会告诉你上面的测试地址里的代码也很完整的,绝不会!)。