学习骨架屏(Skeleton Screens)技术
骨架屏
优化用户等待体验
page-skeleton-webpack-plugin
一,使用vue-server-renderer
为了简单起见,我们使用vue-cli搭配webpack-simple这个模板来新建项目:
vue init webpack-simple vue-skeleton
这时我们便获得了一个最基本的Vue项目:
. ├── package.json ├── src │ ├── App.vue │ ├── assets │ └── main.js ├── index.html └── webpack.conf.js
安装完了依赖以后,便可以通过npm run dev去运行这个项目了。但是,在运行项目之前,我们先看看入口的html文件里面都写了些什么。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8">
<title>vue-skeleton</title>
</head> <body>
<div id="app"></div> <script src="/dist/build.js"></script> </body>
</html>
可以看到,DOM里面有且仅有一个div#app,当js被执行完成之后,此div#app会被整个替换掉,因此,我们可以来做一下实验,在此div里面添加一些内容:
<div id="app"> <p>Hello skeleton</p> <p>Hello skeleton</p> <p>Hello skeleton</p> </div>
打开chrome的开发者工具,在Network里面找到throttle功能,调节网速为“Slow 3G”,刷新页面,就能看到页面先是展示了三句“Hello skeleton”,待js加载完了才会替换为原本要展示的内容。

现在,我们对于如何在Vue页面实现骨架屏,已经有了一个很清晰的思路——在div#app内直接插入骨架屏相关内容即可。
显然,手动在 div#app 里面写入骨架屏内容是不科学的,我们需要一个扩展性强且自动化的易维护方案。既然是在Vue项目里,我们当然希望所谓的骨架屏也是一个 .vue 文件,它能够在构建时由工具自动注入到 div#app 里面。
首先,我们在/src目录下新建一个Skeleton.vue文件,其内容如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
<template> <div class="skeleton page"> <div class="skeleton-nav"></div> <div class="skeleton-swiper"></div> <ul class="skeleton-tabs"> <li v-for="i in 8" class="skeleton-tabs-item"><span></span></li> </ul> <div class="skeleton-banner"></div> <div v-for="i in 6" class="skeleton-productions"></div> </div></template><style>.skeleton { position: relative; height: 100%; overflow: hidden; padding: 15px; box-sizing: border-box; background: #fff;}.skeleton-nav { height: 45px; background: #eee; margin-bottom: 15px;}.skeleton-swiper { height: 160px; background: #eee; margin-bottom: 15px;}.skeleton-tabs { list-style: none; padding: 0; margin: 0 -15px; display: flex; flex-wrap: wrap;}.skeleton-tabs-item { width: 25%; height: 55px; box-sizing: border-box; text-align: center; margin-bottom: 15px;}.skeleton-tabs-item span { display: inline-block; width: 55px; height: 55px; border-radius: 55px; background: #eee;}.skeleton-banner { height: 60px; background: #eee; margin-bottom: 15px;}.skeleton-productions { height: 20px; margin-bottom: 15px; background: #eee;}</style> |
接下来,再新建一个 skeleton.entry.js 入口文件:
|
1
2
3
4
5
6
7
8
9
|
import Vue from 'vue'import Skeleton from './Skeleton.vue'export default new Vue({ components: { Skeleton }, template: '<skeleton />'}) |
在完成了骨架屏的准备之后,就轮到一个关键插件vue-server-renderer登场了。该插件本用于服务端渲染,但是在这个例子里,我们主要利用它能够把.vue文件处理成html和css字符串的功能,来完成骨架屏的注入,流程如下:

根据流程图,我们还需要在根目录新建一个webpack.skeleton.conf.js文件,以专门用来进行骨架屏的构建。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
const path = require('path')const webpack = require('webpack')const nodeExternals = require('webpack-node-externals')const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')module.exports = { target: 'node', entry: { skeleton: './src/skeleton.entry.js' }, output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: '[name].js', libraryTarget: 'commonjs2' }, module: { rules: [ { test: /\.css$/, use: [ 'vue-style-loader', 'css-loader' ] }, { test: /\.vue$/, loader: 'vue-loader' } ] }, externals: nodeExternals({ whitelist: /\.css$/ }), resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' }, extensions: ['*', '.js', '.vue', '.json'] }, plugins: [ new VueSSRServerPlugin({ filename: 'skeleton.json' }) ]} |
可以看到,该配置文件和普通的配置文件基本完全一致,主要的区别在于其 target: 'node' ,配置了 externals ,以及在 plugins 里面加入了 VueSSRServerPlugin 。在 VueSSRServerPlugin 中,指定了其输出的json文件名。我们可以通过运行下列指令,在 /dist 目录下生成一个 skeleton.json 文件:
|
1
|
webpack --config ./webpack.skeleton.conf.js |
这个文件在记载了骨架屏的内容和样式,会提供给vue-server-renderer使用。
接下来,在根目录下新建一个skeleton.js,该文件即将被用于往index.html内插入骨架屏。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const fs = require('fs')const { resolve } = require('path')const createBundleRenderer = require('vue-server-renderer').createBundleRenderer// 读取`skeleton.json`,以`index.html`为模板写入内容const renderer = createBundleRenderer(resolve(__dirname, './dist/skeleton.json'), { template: fs.readFileSync(resolve(__dirname, './index.html'), 'utf-8')})// 把上一步模板完成的内容写入(替换)`index.html`renderer.renderToString({}, (err, html) => { fs.writeFileSync('index.html', html, 'utf-8')}) |
注意,作为模板的html文件,需要在被写入内容的位置添加<!--vue-ssr-outlet-->占位符,本例子在div#app里写入:
<div id="app"> <!--vue-ssr-outlet--> </div>
接下来,只要运行 node skeleton.js ,就可以完成骨架屏的注入了。运行效果如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
<html lang="en"> <head> <meta charset="utf-8"> <title>vue-skeleton</title> <style data-vue-ssr-id="742d88be:0">.skeleton { position: relative; height: 100%; overflow: hidden; padding: 15px; box-sizing: border-box; background: #fff;}.skeleton-nav { height: 45px; background: #eee; margin-bottom: 15px;}.skeleton-swiper { height: 160px; background: #eee; margin-bottom: 15px;}.skeleton-tabs { list-style: none; padding: 0; margin: 0 -15px; display: flex; flex-wrap: wrap;}.skeleton-tabs-item { width: 25%; height: 55px; box-sizing: border-box; text-align: center; margin-bottom: 15px;}.skeleton-tabs-item span { display: inline-block; width: 55px; height: 55px; border-radius: 55px; background: #eee;}.skeleton-banner { height: 60px; background: #eee; margin-bottom: 15px;}.skeleton-productions { height: 20px; margin-bottom: 15px; background: #eee;}</style></head> <body> <div id="app"> <div data-server-rendered="true" class="skeleton page"><div class="skeleton-nav"></div> <div class="skeleton-swiper"></div> <ul class="skeleton-tabs"><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li></ul> <div class="skeleton-banner"></div> <div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div></div> </div> <script src="/dist/build.js"></script> </body></html> |
可以看到,骨架屏的样式通过 <style></style> 标签直接被插入,而骨架屏的内容也被放置在 div#app 之间。当然,我们还可以进一步处理,把这些内容都压缩一下。改写 skeleton.js ,在里面添加 html-minifier :
...
+ const htmlMinifier = require('html-minifier')
...
renderer.renderToString({}, (err, html) => {
+ html = htmlMinifier.minify(html, {
+ collapseWhitespace: true,
+ minifyCSS: true + })
fs.writeFileSync('index.html', html, 'utf-8')
})
效果:

二,vue-skeleton-webpack-plugin
我们希望在构建时渲染 skeleton 组件,将渲染 DOM 插入 html 的挂载点中,同时将使用的样式通过 style 标签内联。这样在前端 JS 渲染完成之前,用户将看到页面的大致骨架,感知到页面是正在加载的。
我们当然可以选择在开发时直接将页面骨架内容写入 html 模版中,但是这会带来两个问题:
- 开发 skeleton 与其他组件体验不一致。
- 多页应用中多个页面可能共用同一个 html 模版,而又有独立的 skeleton。
下面我们将看看插件在具体实现中是如何解决这两个问题的:
具体实现步骤:
1、我们用vue-cli 直接构建一下项目跑起来(具体怎么构建就不说了)
2、进去当前项目,执行命令 : npm install vue-skeleton-webpack-plugin
3、我们在src目录下创建 Skeleton.vue
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<template> <div class="skeleton-wrapper"> <header class="skeleton-header"></header> <section class="skeleton-block"> <img src="data:image/svg+xml;base64..."> <img src="data:image/svg+xml;base64..."> </section> </div></template><script> export default { name: 'skeleton' }</script><style scoped> .skeleton-header { height: 40px; background: #1976d2; padding:0; margin: 0; width: 100%; } .skeleton-block { display: flex; flex-direction: column; padding-top: 8px; }</style> |
4、创建入口文件:entry-skeleton.js
|
1
2
3
4
5
6
7
8
|
import Vue from 'vue'import Skeleton from './Skeleton'export default new Vue({ components: { Skeleton }, template: '<Skeleton />'}) |

5、我们在build 目录下创建 webpack.skeleton.conf.js
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
'use strict';const path = require('path')const merge = require('webpack-merge')const baseWebpackConfig = require('./webpack.base.conf')const nodeExternals = require('webpack-node-externals')function resolve(dir) { return path.join(__dirname, dir)}module.exports = merge(baseWebpackConfig, { target: 'node', devtool: false, entry: { app: resolve('../src/entry-skeleton.js') }, output: Object.assign({}, baseWebpackConfig.output, { libraryTarget: 'commonjs2' }), externals: nodeExternals({ whitelist: /\.css$/ }), plugins: []}) |
然后在webpack.dev.conf.js和webpack.prod.conf.js分别加入
|
1
2
3
4
5
6
|
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')// inject skeleton content(DOM & CSS) into HTML new SkeletonWebpackPlugin({ webpackConfig: require('./webpack.skeleton.conf'), quiet: true }) |
然后就完成了。
延伸:
1、vue-skeleton-webpack-plugin 可以 使用多个 骨架屏 ,具体的可以查看官网地址:https://github.com/lavas-project/vue-skeleton-webpack-plugin
三,page-skeleton-webpack-plugin
优势:
- 支持多种加载动画
- 针对移动端 web 页面
- 支持多路由
- 可定制化,可以通过配置项对骨架块形状颜色进行配置,同时也可以在预览页面直接修改骨架页面源码
- 几乎可以零配置使用
安装:
|
1
2
|
npm install --save-dev page-skeleton-webpack-pluginnpm install --save-dev html-webpack-plugin |
配置:
第一步:配置插件,详细配置可参考官方文档
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
const HtmlWebpackPlugin = require('html-webpack-plugin')const { SkeletonPlugin } = require('page-skeleton-webpack-plugin')const path = require('path')const webpackConfig = { entry: 'index.js', output: { path: __dirname + '/dist', filename: 'index.bundle.js' }, plugin: [ new HtmlWebpackPlugin({ // Your HtmlWebpackPlugin config }), new SkeletonPlugin({ pathname: path.resolve(__dirname, `${customPath}`), // 用来存储 shell 文件的地址 staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同 routes: ['/', '/search'], // 将需要生成骨架屏的路由添加到数组中 }) ]} |
第二步:修改 HTML Webpack Plugin 插件的模板
在你启动 App 的根元素内部添加
|
1
2
3
4
5
6
7
8
9
10
11
12
|
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Document</title></head><body> <div id="app"> <!-- shell --> </div></body></html> |
第三步:界面操作生成、写入骨架页面
- 在开发页面中通过 Ctrl|Cmd + enter 呼出插件交互界面,或者在在浏览器的 JavaScript 控制台内输入toggleBar 呼出交互界面。
-
点击交互界面中的按钮,进行骨架页面的预览,这一过程可能会花费 20s 左右时间,当插件准备好骨架页面后,会自动通过浏览器打开预览页面,如下图。
uniapp中使用骨架屏
插件:
https://ext.dcloud.net.cn/plugin?id=256#detail
小程序使用骨架屏
地址:https://github.com/jayZOU/skeleton
基于skeleton组件的骨架屏生成及其简单,主要有以下几个步骤。
- 1、下载组件到项目中
- 2、配置json文件,允许使用组件
- 3、构建基本页面骨骼
- 4、在wxml中引入组件并设置相关类
实践
1、这一点简单,跳过。
2、这一点也简单,在json文件中,添加如下代码:
|
1
2
3
|
"usingComponents": { "skeleton": "/component/skeleton/skeleton" }, |
3、构建基本页面骨骼,也就是填充默认数据(模拟数据)。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
<view class='container skeleton'> <view class='row' wx:for="{{studentList}}" wx:key="{{index}}"> <image class='skeleton-radius' src='{{item.avatarUrl}}' mode='widthFix'></image> <view> <text class='nickName skeleton-rect'>{{item.class}}-{{item.nickName}}</text> <text class='skeleton-rect'>{{item.detailInfo}}</text> </view> </view></view>/* pages/student/student.wxss */page{ border-top: solid 2rpx #999;}.container{ padding: 10rpx 20rpx;}.row{ display: flex; flex-direction: row; justify-content: flex-start; align-items: center; height: 100rpx; width: 690rpx; padding: 10rpx 10rpx; margin-top: 20rpx; font-size: 26rpx; color: #666; }.row > image{ width: 100rpx; height: 100rpx; border-radius: 50%; margin-right: 20rpx;}.nickName{ display: block; color: #333; font-size: 30rpx; margin-bottom: 16rpx;} |
这样就完成了页面骨骼的构架,接下来就是在wxml中引入组件并设置相关类了。
4、在wxml中引入,可以在头部或者底部引入,如下:
|
1
2
3
4
|
<skeleton selector="skeleton" loading="spin" bgcolor="#FFF" wx:if="{{showSkeleton}}"></skeleton> |
接下来就是设置相关类了,主要要设置3个类,分别是:skeleton、skeleton-radius和skeleton-rect。
skeleton就是作用范围,相当于vue中的el:“#app” 的范围,一般设置给最底部的view即可。
skeleton-radius:设置为圆形的骨架
skeleton-rect:设置为长方形的骨架
注意:这里的skeleton-radius和skeleton-rect渲染出来的骨架大小,是受默认的填充的元素大小影响的。
完整的wxml代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<!--pages/student/student.wxml--><skeleton selector="skeleton" loading="spin" bgcolor="#FFF" wx:if="{{showSkeleton}}"></skeleton><view class='container skeleton'> <view class='row' wx:for="{{studentList}}" wx:key="{{index}}"> <image class='skeleton-radius' src='{{item.avatarUrl}}' mode='widthFix'></image> <view> <text class='nickName skeleton-rect'>{{item.class}}-{{item.nickName}}</text> <text class='skeleton-rect'>{{item.detailInfo}}</text> </view> </view></view> |
然后在js中的data中加入showSkeleton变量,设置一个定时器结束(用来模拟网络请求的等待过程)。
然后运行就可以看见效果了。
js中的data 和 onLoad
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
data:{ //... showSkeleton:true, //...},onLoad: function (options) { const that = this; setTimeout(() => { that.setData({ showSkeleton: false }) }, 5000) }, |

浙公网安备 33010602011771号