uniapp实现骨架屏
前言:用户在等待数据渲染的时候,有可能因为网络速度慢,手机硬件等问题,造成等待时间延长,使得用户体验不好。
之前的做法是放个加载中的图标,而现在是直接根据页面原有元素绘制图形的方式,让用户有种页面就快渲染好的错觉。
参考资料:
https://ext.dcloud.net.cn/plugin?id=256
备注:我是准备应用到项目中,从uniapp的插件市场下载了demo,结果出现一些小问题,在下载下来的demo做了些小修改
加载过程效果图:如图,从图一到图二,最底部多出了一个动态加载的骨架,模拟同一页面多个数据请求(每个请求所需时间不同),
我这边的处理是在每个请求的回调中,先赋值渲染的动态数据,再重新抓取需要绘制的动态元素(因为绘制的元素需要先有数据给它撑开),
最后页面中的请求基本完成的时候,隐藏骨架屏,显示原先的页面
问题:对demo有更好建议的可以提出来哈,相互学习一下
代码如下:
组件
1 <template> 2 <view v-if="show" :style="{width: systemInfo.width + 'px', height: systemInfo.height + 'px', backgroundColor: bgcolor, position: 'absolute', left: 0, top: 0, zIndex: 9998, overflow: 'hidden'}"> 3 <view v-for="(item,rect_idx) in skeletonRectLists" :key="rect_idx + 'rect'" :class="[loading == 'chiaroscuro' ? 'chiaroscuro' : '']" 4 :style="{width: item.width + 'px', height: item.height + 'px', backgroundColor: 'rgb(194, 207, 214)', position: 'absolute', left: item.left + 'px', top: item.top + 'px'}"></view> 5 <view v-for="(item,circle_idx) in skeletonCircleLists" :key="circle_idx + 'circle'" :class="loading == 'chiaroscuro' ? 'chiaroscuro' : ''" 6 :style="{width: item.width + 'px', height: item.height + 'px', backgroundColor: 'rgb(194, 207, 214)', borderRadius: item.width + 'px', position: 'absolute', left: item.left + 'px', top: item.top + 'px'}"></view> 7 <view class="spinbox" v-if="loading == 'spin'"> 8 <view class="spin"></view> 9 </view> 10 </view> 11 </template> 12 13 <script> 14 export default { 15 name: "skeleton", 16 props: { 17 bgcolor: { 18 type: String, 19 value: '#FFF' 20 }, 21 selector: { 22 type: String, 23 value: 'skeleton' 24 }, 25 loading: { 26 type: String, 27 value: 'spin' 28 }, 29 show: { 30 type: Boolean, 31 value: false 32 }, 33 isNodes: { 34 type: Number, 35 value: false 36 } //控制什么时候开始抓取元素节点,只要数值改变就重新抓取 37 }, 38 data() { 39 return { 40 loadingAni: ['spin', 'chiaroscuro'], 41 systemInfo: {}, 42 skeletonRectLists: [], 43 skeletonCircleLists: [] 44 } 45 }, 46 watch: { 47 isNodes (val) { 48 this.readyAction(); 49 } 50 }, 51 mounted() { 52 this.attachedAction(); 53 }, 54 methods: { 55 attachedAction: function(){ 56 //默认的首屏宽高,防止内容闪现 57 const systemInfo = uni.getSystemInfoSync(); 58 this.systemInfo = { 59 width: systemInfo.windowWidth, 60 height: systemInfo.windowHeight 61 }; 62 this.loading = this.loadingAni.includes(this.loading) ? this.loading : 'spin'; 63 }, 64 readyAction: function(){ 65 console.log('子组件readyAction') 66 const that = this; 67 //绘制背景 68 uni.createSelectorQuery().selectAll(`.${this.selector}`).boundingClientRect().exec(function(res){ 69 that.systemInfo.height = res[0][0].height + res[0][0].top; 70 }); 71 72 //绘制矩形 73 this.rectHandle(); 74 75 //绘制圆形 76 this.radiusHandle(); 77 }, 78 rectHandle: function(){ 79 const that = this; 80 81 //绘制不带样式的节点 82 uni.createSelectorQuery().selectAll(`.${this.selector}-rect`).boundingClientRect().exec(function(res){ 83 that.skeletonRectLists = res[0]; 84 }); 85 86 }, 87 radiusHandle(){ 88 const that = this; 89 90 uni.createSelectorQuery().selectAll(`.${this.selector}-radius`).boundingClientRect().exec(function(res){ 91 that.skeletonCircleLists = res[0]; 92 }); 93 } 94 } 95 } 96 </script> 97 98 <style> 99 .spinbox{ 100 position: fixed; 101 display: flex; 102 justify-content: center; 103 align-items: center; 104 height: 100%; 105 width: 100%; 106 z-index: 9999 107 } 108 .spin { 109 display: inline-block; 110 width: 64rpx; 111 height: 64rpx; 112 } 113 .spin:after { 114 content: " "; 115 display: block; 116 width: 46rpx; 117 height: 46rpx; 118 margin: 1rpx; 119 border-radius: 50%; 120 border: 5rpx solid #409eff; 121 border-color: #409eff transparent #409eff transparent; 122 animation: spin 1.2s linear infinite; 123 } 124 @keyframes spin { 125 0% { 126 transform: rotate(0deg); 127 } 128 100% { 129 transform: rotate(360deg); 130 } 131 } 132 133 .chiaroscuro{ 134 width: 100%; 135 height: 100%; 136 background: rgb(194, 207, 214); 137 animation-duration: 2s; 138 animation-name: blink; 139 animation-iteration-count: infinite; 140 } 141 142 @keyframes blink { 143 0% { 144 opacity: .4; 145 } 146 50% { 147 opacity: 1; 148 } 149 100% { 150 opacity: .4; 151 } 152 } 153 154 @keyframes flush { 155 0% { 156 left: -100%; 157 } 158 50% { 159 left: 0; 160 } 161 100% { 162 left: 100%; 163 } 164 } 165 .shine { 166 animation: flush 2s linear infinite; 167 position: absolute; 168 top: 0; 169 bottom: 0; 170 width: 100%; 171 background: linear-gradient(to left, 172 rgba(255, 255, 255, 0) 0%, 173 rgba(255, 255, 255, .85) 50%, 174 rgba(255, 255, 255, 0) 100% 175 ) 176 } 177 </style>
页面demo
1 <template> 2 <view class="controller"> 3 <view class="container skeleton" :style="{visibility: showSkeleton ? 'hidden' : 'visible'}"> 4 <view class="userinfo"> 5 <block> 6 <!--skeleton-radius 绘制圆形--> 7 <image class="userinfo-avatar skeleton-radius" :src="userInfo.avatarUrl" mode="cover"></image> 8 <!--skeleton-rect 绘制矩形--> 9 <text class="userinfo-nickname skeleton-rect">{{userInfo.nickName}}</text> 10 </block> 11 </view> 12 <view style="margin: 20px 0"> 13 <view v-for="(item,index) in lists" :key="index" class="lists"> 14 <text class="skeleton-rect">{{item}}</text> 15 </view> 16 </view> 17 <view class="usermotto"> 18 <text class="user-motto skeleton-rect">{{motto}}</text> 19 </view> 20 </view> 21 <!--引用组件--> 22 <skeleton :show="showSkeleton" :isNodes="isNodes" ref="skeleton" loading="chiaroscuro" selector="skeleton" bgcolor="#FFF"></skeleton> 23 </view> 24 </template> 25 26 <script> 27 //引入骨架屏组件(以我本地地址为例,具体地址由自身引用位置决定) 28 import skeleton from "@/components/quick-skeleton/quick-skeleton.vue"; 29 export default { 30 data() { 31 return { 32 motto: '', 33 userInfo: { 34 avatarUrl: 'https://wx.qlogo.cn/mmopen/vi_32/s4RzXCAQsVNliaJXtHBvdpAkeRwnK7Jhiaf9mzuVqEhZza3zSYM7tJ1xZCQE9SCoOR8qjVEjDKltw1SQnxyicWq6A/132', 35 nickName: 'jayzou' 36 }, 37 // lists: [ 38 // '第1行数据', 39 // '第2行数据', 40 // '第3行数据', 41 // '第4行数据', 42 // '第5行数据', 43 // '第6行数据' 44 // ], 45 lists: [], //如果没有默认数据 46 showSkeleton: true, //骨架屏显示隐藏 47 isNodes: 0 //控制什么时候开始抓取元素节点,只要数值改变就重新抓取 48 }; 49 }, 50 components: { 51 skeleton 52 }, 53 onLoad: function () { 54 const that = this; 55 56 //问题:骨架屏出现的时间段,部分已经渲染完毕,但还是得等骨架屏隐藏才一起出现 57 58 setTimeout(() => { 59 this.lists = [ 60 '第1行数据', 61 '第2行数据', 62 '第3行数据', 63 '第4行数据', 64 '第5行数据', 65 '第6行数据' 66 ] 67 that.isNodes ++; 68 }, 182); 69 70 setTimeout(() => { 71 that.motto = 'Hello World' 72 that.isNodes ++; 73 }, 500); 74 75 setTimeout(() => { 76 that.showSkeleton = false; 77 }, 2000); 78 }, 79 /** 80 * 页面载入完成后调用子组件的方法生成预加载效果 81 */ 82 onReady:function(){ 83 84 } 85 } 86 </script> 87 88 <style> 89 .container { 90 padding: 20upx 60upx; 91 } 92 /**index.wxss**/ 93 .userinfo { 94 display: flex; 95 flex-direction: column; 96 align-items: center; 97 } 98 .userinfo-avatar { 99 width: 128rpx; 100 height: 128rpx; 101 margin: 20rpx; 102 border-radius: 50%; 103 } 104 .userinfo-nickname { 105 color: #aaa; 106 } 107 .usermotto { 108 margin-top: 200px; 109 } 110 .lists{ 111 margin: 10px 0; 112 } 113 .list{ 114 margin-right: 10px; 115 } 116 </style>