学习骨架屏(Skeleton Screens)技术

骨架屏

1.背景
近两年来,前、后端分离的架构得到越来越多的认可,越来越多的团队在尝试、推广这种架构。然而在带来便利的同时,也带来了一些弊端,比如首屏渲染时间(FCP)因为首屏需要请求更多内容,比原来多了更多HTTP的往返时间(RTT),这造成了白屏,如果白屏时间过长,用户体验会大打折扣。
 
 
为了优化首屏渲染时间这个指标,减少白屏时间,前端想了很多办法:
加速或减少HTTP请求损耗
延迟加载
减少请求内容的体积

优化用户等待体验

这里要介绍的就是优化用户等待体验的骨架屏,目前市面上用得比较多的是下面这几个插件:
vue-server-renderer
vue-skeleton-webpack-plugin

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加载完了才会替换为原本要展示的内容。

hzv4.gif

现在,我们对于如何在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 {
   25%;
  height: 55px;
  box-sizing: border-box;
  text-align: center;
  margin-bottom: 15px;
}
.skeleton-tabs-item span {
  display: inline-block;
   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字符串的功能,来完成骨架屏的注入,流程如下:

clipboard.png

根据流程图,我们还需要在根目录新建一个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 {
   25%;
  height: 55px;
  box-sizing: border-box;
  text-align: center;
  margin-bottom: 15px;
}
.skeleton-tabs-item span {
  display: inline-block;
   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')
})

效果:

clipboard.png

 二,vue-skeleton-webpack-plugin

我们希望在构建时渲染 skeleton 组件,将渲染 DOM 插入 html 的挂载点中,同时将使用的样式通过 style 标签内联。这样在前端 JS 渲染完成之前,用户将看到页面的大致骨架,感知到页面是正在加载的。

我们当然可以选择在开发时直接将页面骨架内容写入 html 模版中,但是这会带来两个问题:

  1. 开发 skeleton 与其他组件体验不一致。
  2. 多页应用中多个页面可能共用同一个 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;
     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

对比发现,就开发难度、灵活性和插件开源方来看,如果项目是基于vue-cli脚手架的,那么饿了么团队的 page-skeleton-webpack-plugin是你的最佳选择,如果不是,那么可以选择vue-router开源的 vue-server-renderer.

优势:

  • 支持多种加载动画
  • 针对移动端 web 页面
  • 支持多路由
  • 可定制化,可以通过配置项对骨架块形状颜色进行配置,同时也可以在预览页面直接修改骨架页面源码
  • 几乎可以零配置使用

安装:

1
2
npm install --save-dev page-skeleton-webpack-plugin
npm 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>
 
第三步:界面操作生成、写入骨架页面
  1. 在开发页面中通过 Ctrl|Cmd + enter 呼出插件交互界面,或者在在浏览器的 JavaScript 控制台内输入toggleBar 呼出交互界面。
  2. 点击交互界面中的按钮,进行骨架页面的预览,这一过程可能会花费 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;
   690rpx;
  padding: 10rpx 10rpx;
  margin-top: 20rpx;
  font-size: 26rpx;
  color: #666;
   
}
 
.row > image{
   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)
  },

 
原文地址:https://www.cnblogs.com/cfcastiel/p/14469927.html