前后端分离以来,首屏渲染时间(FCP)因为首屏需要请求更多内容,比原来多了更多 HTTP 的往返时间(RTT),这造成了白屏,如果白屏时间过长,用户体验会大打折扣。页面交互优化方式近年来在不断发展,页面等待加载的优化大致有以下几个阶段:
》原始做法:html的div边框(约等于什么都没做);
》优化做法:loading小菊花;
》进阶做法:FCP 优化;
》最新做法:骨架屏。
以下主要介绍骨架屏。在骨架屏展开论述之前,我们先回顾以下FCP的优化。
FCP优化(First Contentful Paint)
加速或减少HTTP请求损耗
延迟加载
减少请求内容的体积
优化用户等待体验
骨架屏
相比于之前的Loading动画,骨架屏页面更容易让用户产生一种错觉,页面快加载完了。骨架屏实现原理很简单,就是通过占位线框元素,渐进式加载数据。
大厂骨架屏案例:
参见: https://zhuanlan.zhihu.com/p/96455668
1. facebook将用户固定的头像,author,日期和一小部分文字作为骨架主体
2. jira则是标题和logo对应的很整齐
3. linkedin可以说完全没有对齐,而是使用一种更加的展示骨架布局
4. slack则是使用混合的loading方式,有骨架图也有旋转圆,不仅如此,slack并没有全部使用同一种灰色值,不同的block的颜色代表的该区域的字体颜色,这又是一种切换顺滑度的提升。
这里要介绍的就是优化用户等待体验的骨架屏。
生成骨架屏的方法
生成骨架屏的方式主要有:
手写HTML、CSS的方式为目标页定制骨架屏(参考:https://segmentfault.com/a/1190000014832185),主要思路就是使用 vue-server-renderer 这个本来用于服务端渲染的插件,用来把我们写的.vue文件处理为HTML,插入到页面模板的挂载点中,完成骨架屏的注入。这种方式不甚友好,如果页面样式改变了,还得改一遍骨架屏,增加了维护成本。
使用图片作为骨架屏;简单暴力,让 UI 同学花点功夫吧哈哈;小米商城的移动端页面采用的就是这个方法,它是使用了一个 Base64 的图片来作为骨架屏。
插件自动生成并自动插入静态骨架屏;这种方法跟第一种方法类似,不过是自动生成骨架屏,可以关注下饿了么开源的插件 page-skeleton-webpack-plugin,它根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中,不过要注意的是这个插件目前只支持history方式的路由,不支持hash方式,且目前只支持首页的骨架屏,并没有组件级的局部骨架屏实现,作者说以后会有计划实现。
目前市面上用得比较多的是下面这几个插件:
相关插件配置方法参见: https://www.jianshu.com/p/eacac700630e
2. vue-skeleton-webpack-plugin
3. page-skeleton-webpack-plugin 基于vue-cli脚手架,饿了么团队出的
简单实现骨架屏的代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Skeletons</title>
<style>
img {
width: 100%;
}
.media-box-img {
width: 60px;
height: 60px;
}
/* 阻止Skeletons点击事件 */
.pointer-stop {
pointer-events: none;
}
/* Skeletons效果 */
.skeletons {
position: relative;
display: block;
overflow: hidden;
height: 100%;
min-height: 20px;
background-color: #ededed;
}
.skeletons:empty::after { // 巧妙利用了css的empty伪类选择器
display: block;
content: '';
position: absolute;
width: 100%;
height: 100%;
-webkit-transform: translateX(-100%);
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(216, 216, 216, 0.753), transparent);
-webkit-animation: loading 1.5s infinite;
animation: loading 1.5s infinite;
}
@keyframes loading {
100% {
-webkit-transform: translateX(100%);
transform: translateX(100%);
}
}
</style>
</head>
<body>
<div class="weui-panel weui-panel_access">
<div class="weui-panel__bd">
<a href="javascript:void(0);" class="weui-media-box weui-media-box_appmsg pointer-stop">
<div class="weui-media-box__hd">
<div class="media-box-img skeletons"></div>
</div>
<div class="weui-media-box__bd">
<div class="weui-media-box__title skeletons"></div>
<p class="weui-media-box__desc">
<span class="media-box-desc skeletons"></span>
</p>
</div>
</a>
</div>
</div>
<script>
function renderCard() {
var cardImage = document.querySelector('.weui-panel-title')
cardImage.textContent = '标题'
cardImage.classList.remove('skeletons')
var listData = [
{
img:xxx.jpg,
desc: '内容内容内容内容'
}
]
var html = ''
var cardImage1 = document.querySelectorAll('.media-box-img')
var cardImage2 = document.querySelectorAll('.weui-media-box')
var cardImage3 = document.querySelectorAll('.weui-media-box__title')
var cardImage4 = document.querySelectorAll('.media-box-desc')
for (var i = 0; i < listData.length; i++) {
cardImage2[i].classList.remove('pointer-stop')
cardImage1[i].classList.remove('skeletons')
cardImage3[i].classList.remove('skeletons')
cardImage4[i].classList.remove('skeletons')
cardImage1[i].innerHTML = "<img src='" + listData[i].img + "' />"
cardImage3[i].innerHTML = '一段标题'
cardImage4[i].innerHTML = '一段描述'
}
}
setTimeout(function() {
renderCard()
}, 4000)
</script>
</body>
</html>