骨架屏英文叫 Skeleton Screen,也被称为加载占位图。骨架屏是页面的一个空白版本,通常会在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。国内的淘宝、饿了么、知乎、得到,国外的 Facebook 等的网站都有应用。
为什么要使用骨架屏
在使用终端设备请求数据时都会出现一定的延迟,屏幕从而会出现一大片空白,以前对于这段等待时间的处理大多数都是采用菊花图加载动画或者类似于这种的加载动画。
这种方式请求时页面会卡住,直到请求完成,用户期间无法进行任何的操作,只能看着加载动效图,给人一种死机的感觉,体验差。为了进一步提升用户的体验,于是就出现了另一种加载方式——骨架屏( Skeleton Screen )
骨架屏的优势
- 锁屏 Loading 在一定程度上限制了用户的操作,而骨架屏不干扰用户的操作。
- 用户对于加载的内容有一个大致的框架。
- 弱网络环境下极大的优化了用户体验。
怎么实现骨架屏
目前主流的骨架屏生成技术主要包括以下三种:
- 通过设计师给出的骨架屏图片。
- 通过 HTML+CSS 编写骨架屏代码。
- 非侵入式自动生成骨架屏代码。
前两种情况由于变更成本和续维护成本高,且对业务代码有一定侵入性。业界对于自动生成骨架屏有多种实践,但是存在一些问题,有些配置较少,生成效果较差;有些操作繁琐,项目集成成本高,且难以定制。
1)通过 HTML+CSS 编写骨架屏代码
原理:在不改变页面布局下,隐藏图片和文字,通过样式的覆盖,使其展示为灰色块。等到加载完成后,将修改后的元素和样式提取出来
步骤:1、创建与显示内容相似的 html 结构
2、在需要显示内容的元素上增加背景色方式:1、简单堆砌出元素结构
2、使用背景动画,修改 css 样式
2)自动生成并自动插入静态骨架屏
饿了么开源的插件 page-skeleton-webpack-plugin ,它根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中,不过要注意的是这个插件目前只支持 history 方式的路由,不支持 hash 方式,且目前只支持首页的骨架屏,并没有组件级的局部骨架屏实现,作者说以后会有计划实现(issue9)。该方案通过一个 webpack 插件 page-skeleton-webpack-plugin 的方式与项目开发无缝集成,属于在自动生成骨架屏方面做的非常强大的了,并且可以启动 UI 界面专门调整骨架屏,但是在面对复杂的页面也会有不尽如人意的地方,而且生成的骨架屏节点是基于页面本身的结构和 CSS,存在嵌套比较深的情况,体积不会太小,并且只支持 history 模式。
插件 vue-skeleton-webpack-plugin,它将插入骨架屏的方式由手动改为自动,原理在构建时使用 Vue 预渲染功能,将骨架屏组件的渲染结果 HTML 片段插入 HTML 页面模版的挂载点中,将样式内联到 head 标签中。这个插件可以给单页面的不同路由设置不同的骨架屏,也可以给多页面设置,同时为了开发时调试方便,会将骨架屏作为路由写入 router 中,可谓是相当体贴了。但该方案与 vue 相关技术直接关联。
3)其他自动实现方式:
3-1:手写代码分析
进一步思考,这些色块基于当前页面去分析节点来生成,不如来段 JS 分析页面节点,一顿 DOM 操作生成颜色块拼成骨架屏。那么问题来了,该怎么样精确的分析页面节点,不同节点又该生成什么样的色块呢?
既然骨架屏代表了页面的大致结构,那么需要先用 js 对页面的结构进行分析。分析之前,我们需要制定一种规则,以确定需要排除哪些节点?哪些种类的节点需要生成颜色块?生成的颜色块如何定位等等。我们初步定下的规则如下:
只遍历可见区域可见的 DOM 节点,包括:
- 非隐藏元素、宽高大于 0 的元素、非透明元素、内容不是空格的元素、位于浏览窗口可见区域内的元素等;
- 针对(背景)图片、文字、表单项、音频视频、Canvas、自定义特征的块等区域来生成颜色块;
- 页面节点使用的样式不可控,所以不可取 style 的尺寸相关的值,可通过 getBoundingClientRect 获取节点宽、高、距离视口距离的绝对值,计算出与当前设备的宽高对应的百分比作为颜色块的单位,来适配不同设备;
基于这套规则,我们开始生成骨架屏:
首先,确定一个 rootNode 作为入口节点,比如 document.body,同时方便以后扩展到生成页面内局部的骨架屏,由此入口进行递归遍历和筛选,初步排除不可见节点。
function isHideStyle(node) {
return getStyle(node, 'display') === 'none' ||
getStyle(node, 'visibility') === 'hidden' ||
getStyle(node, 'opacity') == 0 ||
node.hidden;
}
接下来判断元素特征,确定是否符合生成条件,对于符合条件的区域,”一视同仁”生成相应区域的颜色块。”一视同仁”即对于符合条件的区域不区分具体元素、不考虑结构层级、不考虑样式,统一根据该区域与视口的绝对距离值生成 div 的颜色块。之所以这样是因为生成的节点是扁平的,体积比较小,同时避免额外的读取样式表、通过抽离样式维持骨架屏的外观,这种统一生成的方式使得骨架屏的节点更可控。基于那上述“走捷径”的想法,该方法生成的骨架屏是由纯 DOM 颜色块拼成的。
生成颜色块的方法:
const blocks = [];
// width,height,top,left 都是算好的百分比
function drawBlock({width, height, top, left, zIndex = 9999999, background, radius} = {}) {
const styles = [
'position: fixed',
'z-index: '+ zIndex,
'top: '+ top +'%',
'left: '+ left +'%',
'width: '+ width +'%',
'height: '+ height +'%',
'background: '+ background
];
radius && radius != '0px' && styles.push('border-radius: ' + radius);
// animation && styles.push('animation: ' + animation);
blocks.push(`<div style="${ styles.join(';') }"></div>`);
}
绘制颜色块并不难,绘制之前的分析确认才是这个方案真正的核心和难点。比如,对于页面结构比较复杂或者大图片比较多的页面,由图片拼接的区域没有边界,生成的颜色块就会紧挨着,出现不尽如人意的地方。再比如,一个包含很多符合生成条件的小块的 card 块区域,是以 card 块为准还是以里面的小块为准来生成颜色块呢?如果以小块为准,绘制结果可能给人的感觉压根就不是一个 card 块,再加上布局方式和样式的可能性太多,大大增加了不确定因素。而如果以 card 块为准生成颜色块的话还要对 card 块做专门的规则。
目前来说,对于页面结构不是特别复杂,不是满屏图片的,不是布局方式特别“飘逸“的场景,该方式已经可以生成比较理想的骨架屏了。而对于那些与预期相差较远的情况,我们提供了两个钩子函数可供微调:
- init 函数,在开始遍历节点之前执行,适合删除干扰节点等操作。
- includeElement(node, draw) 函数,可在遍历到指定节点时,调 用 draw 方法进行自定义绘制。
通过以上步骤就能够直接在浏览器中生成骨架屏代码了。
在浏览器里运行
由于我们的方案出发点是通过单纯的 DOM 操作,遍历页面上的节点,根据制定的规则生成相应区域的颜色块,最终形成页面的骨架屏,所以核心代码完全可以直接跑在浏览器端;
const createSkeletonHTML = require('draw-page-structure/evalDOM')
createSkeletonHTML({
// ...
background: 'red',
animation: 'opacity 1s linear infinite;'
}).then(skeletonHTML => {
console.log(skeletonHTML)
}).catch(e => {
console.error(e)
})
3-2 结合 Puppeteer 自动生成骨架屏
谷歌浏览器在2017年自行开发了 Chrome Headless 特性,并与之同时推出了 Puppeteer。Puppeteer 是一个 Node库,提供了一组用来操纵 Chrome 的 API,默认 Headless 也就是无界面的chrome,俗称“无头浏览器”。我们在浏览器中完成的大多数操作都可以在 Puppeteer 中完成,比如截图、爬虫、自动化测试、性能分析等。
虽然该方式已经可以生成骨架屏代码了,但是还是不够自动化,为了让生成的骨架屏代码自动加载进指定页面。第三方开发了一个配套的 CLI 工具。这个工具通过 Puppeteer 运行页面,并把 evalDOM.js 脚本注入页面自动执行,执行的结果是生成的骨架屏代码被插入到应用页面。
方案大概思路如下:
接下来看看如何使用 CLI 工具生成骨架屏,最多只需如下四步:
- 全局安装,npm i draw-page-structure – g
- dps init 生成配置文件 dps.config.js
- 修改 dps.config.js 进行相关配置
- dps start 开始生成骨架屏
只需简单几步,然而并没有繁琐的配置:
一般来说,你需要按自己的项目情况来配置 dps.config.js ,常见的配置项有:
- url: 待生成骨架屏的页面地址
- output.filepath: 生成的骨架屏节点写入的文件
- output.injectSelector: 骨架屏节点插入的位置,默认 #app
- background: 骨架屏主题色
- animation: css3 动画属性
- rootNode: 真对某个模块生成骨架屏
- device: 设备类型,默认 mobile
- extraHTTPHeaders: 添加请求头
- init: 开始生成之前的操作
- includeElement(node, draw): 定制某个节点如何生成
- writePageStructure(html, filepath): 回调的骨架屏节点
详细代码及工具的使用请移步 [Github]( famanoder/dps);