骨架屏用途
- 作为spa中路由切换的 loading, 结合组件的生命周期和ajax请求返回的时机来使用.( 作为loading 使用)。作为与用户联系最为密切的前端开发者,用户体验是最值得关注的问题。关于页面loading状态的展示,主流的主要有loading图和进度条两种。除此之外,越来越多的APP采用了“骨架屏”的方式去展示未加载内容,给予了用户焕然一新的体验。
- 作为首屏渲染的优化
实现思路
- 定义一个抽象组件,在抽象组件的render函数里获取插槽
- 深度循环遍历插槽,将每个元素都添加上gm-skeleton的类名
- 将vnode textContent预暂后清空保证骨架屏出现时不会出现默认文字
- 返回slots
定义一个抽象组件
什么是抽象组件? 在渲染时会被跳过,只做运行时的操作的组件
1 2 3 4
| export default { name: 'YKSkeleton', abstract: true }
|
获取插槽并初始化操作骨架屏
1 2 3 4 5 6 7 8 9 10
| render(h) { const slots = this.$slots.default || [h('')] this.$nextTick().then(() => { this.handlerPrefix(slots, this.showSpin ? this.addSkeletPrefix : this.removeSkeletPrefix) })
return slots.length > 1 ? h('div', { staticClass: this.showSpin ? 'g-spinner' : '' }, slots) : slots }
|
这里我们将处理slots的方法放置在nextTick里面, 因为handlerPrefix里需要获取真实的DOM
,nextTick是用来执行排序后
的更新队列里的所有方法, 在执行render前, YKSkeleton组件的renderWatcher已被收集到更新队列里
,所以此时定义nextTick CallBack函数里能获取到渲染后对应插槽里所有真实DOM
循环slots操作类名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| handlerComponent(slot, handler, init) { const originchildren = (((slot.componentInstance || {})._vnode || {}).componentOptions || {}).children const compchildren = ((slot.componentInstance || {})._vnode || {}).children !init && handler(slot) if (compchildren) this.handlerPrefix(compchildren, handler, false) if (originchildren) this.handlerPrefix(originchildren, handler, false) }, handlerPrefix(slots, handler, init = true) { slots.forEach(slot => { var children = slot.children || (slot.componentOptions || {}).children || ((slot.componentInstance || {})._vnode || {}).children if (slot.data) { if (!slot.componentOptions) { !init && handler(slot) } else if (!this.$hoc_utils.getAbstractComponent(slot)) { ;(function(slot) { const handlerComponent = this.handlerComponent.bind(this, slot, handler, init) const insert = (slot.data.hook || {}).insert ;(slot.data.hook || {}).insert = () => { insert(slot) handlerComponent() } ;(slot.data.hook || {}).postpatch = handlerComponent }).call(this, slot) } } if (slot && slot.elm && slot.elm.nodeType === 3) { if (this.showSpin) { slot.memorizedtextContent = slot.elm.textContent slot.elm.textContent = '' } else { slot.elm.textContent = slot.memorizedtextContent || slot.elm.textContent || slot.text } } children && this.handlerPrefix(children, handler, false) }) },
|
逐步分析:
- 我们遍历slots插槽
- 获取当前vnode下的children集合以备做下一次循环
- 判断是否是原生HTML元素,只有组件vnode才会具备componentOptions属性
- 判断是否抽象组件,我们知道抽象组件是不会渲染到真实DOMTree上的,例如keep-alive、transition,每个组件的vnode拥有独有的hooks生命周期: init(初始化)、insert(插入)、prepatch(更新)、destroy(销毁),每个生命周期会在不同阶段触发, 劫持insert,保留原有的insert方法,随后重构vnode的insert方法在里面调用handlerComponent方法进行添加类名,这里与上面的mounted的nextTick用法理念类似,由于handlerComponent需要知道子组件的实例,所以必须在实例化后去调用,而组件的init方法会实例组件并且直接调用watcher.update(watcher.render()), 也就是我们在调用insert方法的时候其实是在update(render())后,所以这里能够获取到实例化后子组件
- 判断nodeType是否是文本节点,若是的话需要先将textContent保存后进行删除,保证在骨架屏出现时不会显示默认文字,在骨架屏消失时,将原先保留的默认文字返回给vnode,这样就能自由在骨架屏的显示隐藏期间自由切换
操作vnode的静态类名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| addSkeletPrefix(slot) { const rootVnode = slot.componentOptions ? (slot.componentInstance || {})._vnode || {} : slot; if (rootVnode.elm) { rootVnode.elm.classList.add(this.skeletPrefix) } else { ;(rootVnode.data || {}).staticClass += ` ${this.skeletPrefix}` } }, removeSkeletPrefix(slot) { const rootVnode = slot.componentOptions ? (slot.componentInstance || {})._vnode || {} : slot; if (rootVnode.elm) { rootVnode.elm.classList && rootVnode.elm.classList.remove(this.skeletPrefix) } else if (rootVnode.data.staticClass) { rootVnode.data.staticClass = rootVnode.data.staticClass.replace(` ${this.skeletPrefix}`, '') } }
|
addSkeletePrefix用于添加gm-skeleton类名,而removeSkeletonPrefix则是用于删除gm-skeleton类名
使用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import Vue from 'vue' import YKSkeleton from 'path/to/YKSkeleton' Vue.use(YKSkeleton)
<template> <yk-skeleton> <div style="margin-top: 20px;margin-left: 10px;"> <b>测试标题</b> </div> <p style="margin-top: 20px;"> <span> 测试</span> </p> <p class="content"> <i> 我也是有内容的我也是有内容的我也是有内容的我也是有内容的</i> </p> </yk-skeleton> </template> <script> import Vue from 'vue' import YKSkeleton from './YKSkeleton.js' Vue.component(YKSkeleton.name, YKSkeleton) export default {
} </script> <style lang="scss"> .content{width: 3rem; margin-top: 50px; margin-left: 60px;} .yk-skeleton { background-image: linear-gradient(90deg,#f2f2f2 25%,#e6e6e6 37%,#f2f2f2 63%) !important; background-color: transparent !important; background-size: 400% 100%; animation: skeleton-loading 1.5s ease-in infinite; min-height: .3rem; min-width: 1rem; display: inline-block; } @keyframes skeleton-loading { from { background-position: 100% 50%; } to { background-position: 0 50%; } } </style>
|
属性
showSpin
Boolean 判断是否显示骨架屏(默认为true)
className
String 骨架屏ClassName(默认为yk-skeleton)
init
Boolean 默认第一层的子元素不添加骨架屏名字,因为背景色一般都会设置在根元素上,所以这里不做骨架屏处理,默认为true