
骨架屏简介
页面未加载完的时候,为了给用户更好的体验,大家最容易想到的方案就是在页面上展示一个进度条或者loading,用来告诉用户页面没有死,只是还在加载,起到安抚作用。这个事情即使看起来是很小的一件事,但是却相当重要,尤其在中国,也许是因为选择太多,好像我们的用户对页面体验的要求更高。今天给大家介绍一种相对于进度条和loading更好的提升体验的方案——骨架屏
进度条和loading的作用是安抚用户,但是页面该慢还是慢,你无法让用户感觉这个页面加载更快。所以要想让你的页面在众多类似产品中脱颖而出,就需要让你的页面加载的更快。骨架屏就是解决这一问题的,不过,骨架屏方案的原理并没有真正让页面加载速度变得更快,而是让用户主观上感觉到页面的速度变得更快了。
Luke Wroblewski(Google的产品总监) 早在2013年就已经提出了骨架屏的概念,他是这样定义骨架屏的——骨架屏是一个页面的空白版本,它包含文本或者元素基本的轮廓,通过这个空白版本传递信息,我们的页面正在渐进式的加载过程中。2015年,Facebook也在其移动端App中使用了骨架屏方案,随后,Twitter,Medium,YouTube也都在其产品设计中添加了骨架屏,骨架屏一时成为了首屏加载的新趋势,你现在就可以打开京东小程序看一眼首页是不是这样的效果——这就是骨架屏

实现方案
实现方案众多,其目标都只有一个:就是在页面的某个根节点下插入骨架屏的HTML片段,然后页面加载完之后替换掉骨架屏。以下介绍一下几种方案
1. 使用图片、svg 或者手动编写骨架屏HTML片段
这种方式很直接,但是很明显每次改版之后我们又要重新设计一版骨架屏,不管是让设计给图片还是我们手写HTML,这种方案都显得有些笨拙了。
2. 使用Vue服务端渲染生成骨架屏
这种方式相对于第一种好一些,本质上和第一种区别不大。它主要思路就是使用 vue-server-renderer这个插件,它的本来用处是服务端渲染,其基本使用参考文档。这个方案就是利用这个插件将.vue文件渲染成为.html文件,然后将其手动复制到根节点#app中,以此来生成一个骨架屏,当页面渲染完之后#app就会被替换掉。但是这种方案依然需要我们来实现这个.vue文件,存在和方案一同样的问题,只不过我们可以用.vue文件来写骨架屏,相对来说方便一些,但问题也很明显,就是这种方案只适用于Vue项目。
3.骨架屏自动注入
在方案2的基础上利用了webpack的能力,实现了骨架屏的自动注入,其简单的实现可以参考Vue页面骨架屏注入实践,更成熟的方案可以参考为vue项目添加骨架屏。但该方案依然需要我们手动实现相应的骨架屏页面,而且只能用在Vue项目中,要是能够根据页面自动生成骨架屏就好了。
4. 自动生成骨架屏的方案
饿了么团队曾经实现过这样的方案,并开源了一个插件,但不知为何这个插件好像已经很久没有人维护了,详情可参考 page-skeleton-webpack-plugin 。笔者尝试了一波,感觉坑也是比较多,也懒得解决。后来找到比较巧妙的设计方案——网页骨架屏自动生成方案(dps)。这种方案借鉴了饿了么的实现,也实现了一个插件,虽然说是精简了很多,但是用起来完全满足需求。其基本原理简单来讲就是:遍历(这个遍历其实只需要遍历一层即可)可见区域可见的 DOM节点并获取它们的宽高和位置,然后用这些数据生成骨架屏,最后利用Puppeteer——谷歌的headLesschrome工具来完成完全的自动化生成骨架屏,目前唯一的缺点就是还不支持给单页面的不同路由都设置骨架屏,只能设置其中一个(我的项目是多页面的,所以完美适用)。以下以一个案例介绍该插件的使用。
案例介绍
我这里以一个Vue项目为例。
- 首先全局安装插件 draw-page-structure,这个插件要依赖Puppeteer,所以可能会有些慢。
npm i draw-page-structure -g
- 安装完之后,进入到项目目录下运行命令
-
dps init
运行之后会让你输入要生成的骨架屏的页面的地址和最终生成的骨架屏HTML片段要插入的.html文件的地址,这里的.html文件的地址要写成绝对路径。 命令运行成功就会在当前目录下生成dps.config.js文件,这个文件就是用于生成骨架屏的基本配置。 -
dps start
这个命令会先启动无头浏览器,无头浏览器执行相应脚本,读取dps.config.js配置,然后就会生成一个html片段并自动插入到相应的.html文件。运行这个项目并在浏览器打开,就会发现页面加载完之前出现了骨架屏。
- 介绍下dps.config.js配置文件
const dpsConfig = {
url: 'http://www.baidu.com', // 待生成骨架屏页面的地址,用百度(https://baidu.com)试试也可以
output: {
filepath: '', // 生成骨架屏的存放页面,一般为项目的入口页面
injectSelector: '#app' // 生成的骨架屏插入页面的节点
},
// header: {
// height: 40,
// background: '#1b9af4'
// },
// background: '#eee', // 生成的骨架图的各个节点的背景色
// animation: 'opacity 1s linear infinite;',
// includeElement: function(node, draw) {
// 定制某个节点画出来的样子,带上return false
// if(node.id == 'ui-alert') {
// 跳过该节点及其子节点
// return false;
// }
// if(node.tagName.toLowerCase() === 'img') {
// draw({
// width: 100,
// height: 8,
// left: 0,
// top: 0,
// zIndex: 99999999,
// background: 'red'
// });
// return false;
// }
// },
// writePageStructure: function(html) {
// 自己处理生成的骨架屏
// fs.writeFileSync(filepath, html);
// console.log(html)
// },
init: function() {
// 生成骨架屏之前的操作,比如删除干扰节点
}
}
module.exports = dpsConfig;
这个配置基本上就已经能生成一个比较理想的骨架屏了,如果我们对生成的骨架图屏还有特殊的要求,可以利用includeElement,它的第一个参数是一个dom对象,第二个参数是一个方法,这个方法指向插件内部的drawBlock方法,用于生成一个色块。同时,插件暴露出了以下方法(其实文档没说,这个是笔者看源码知道的),可以直接在includeElement方法中调用(init、writePageStructure中也可调用)
- getRect(node)
返回: { w,h,t,l} ,分别表示width、height、top、left - wPercent(x)
返回:节点宽度占总宽度的百分比 - hPercent(y)
返回:节点高度占总高度的百分比 - getStyle(node,attr)
返回:节点的属性值
比如要将所有的className为img-icon的节点的border-radius置为50%
includeElement: function(node, ) {
if (node.className.indexOf('img-icon')!==-1) {
const {t, l, w, h} = getRect(node);
draw({
width: wPercent(w),
height: hPercent(h),
top: hPercent(t),
left: wPercent(l),
radius:'50%' // 注意,这里是radius而不是boderRadius,我是看了源码才知道的
});
return false;
}
}
如果你想要自己实现骨架图的HTML片段,可以使用writePageStructure方法,其实我觉得有点多余,基本不会用上的
某些节点可能是干扰项,可以编写init函数,删除这些节点,这个函数会在遍历节点之前执行。
总结
骨架屏的方案现在已经比较主流了。我已经在项目中尝试了该方案,对于某些产品,前端的用户体验还是相当重要的,大家赶快用起来吧!有坑的话欢迎一起留言交流。
附录
源码流程梳理
