前言:用戶在等待數據渲染的時候,有可能因為網絡速度慢,手機硬件等問題,造成等待時間延長,使得用戶體驗不好。
之前的做法是放個加載中的圖標,而現在是直接根據頁面原有元素繪制圖形的方式,讓用戶有種頁面就快渲染好的錯覺。
參考資料:
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>