Vue骨架屏实现方案

vue作为现在前端最主流的框架之一,拥有者大量的使用者,从学生的练习项目到企业的大型项目,vue都有值得称道的地方。但是一旦项目变得庞大起来,任何项目都会变得卡顿,这里就整理了关于优化vue用户体验的方案之一——骨架屏的实现方案。严格来说骨架屏的也算是优化首屏体验的一种方式。

骨架屏介绍

骨架屏可以理解为是当数据还未加载进来前,页面的一个空白版本,一个简单的关键渲染路径。可以看一下下面Facebook的骨架屏实现,可以看到在页面完全渲染完成之前,用户会看到一个样式简单,描绘了当前页面的大致框架的骨架屏页面,然后骨架屏中各个占位部分被实际资源完全替换,这个过程中用户会觉得内容正在逐渐加载即将呈现,降低了用户的焦躁情绪,使得加载过程主观上变得流畅。

骨架屏的生成方法

Vue-server-renderer

手写HTML、CSS的方式为目标页定制骨架屏,主要思路就是使用 vue-server-renderer这个本来用于服务端渲染的插件,用来把我们写的.vue文件处理为HTML,插入到页面模板的挂载点中,完成骨架屏的注入。这种方式不甚文明,如果页面样式改变了,还得改一遍骨架屏,增加了维护成本。

原理

使用过vue的都清楚,vue项目打包过后都有一个入口的html文件,在那个文件中只有一个id为app的div,而其他所有的js文件其实都是动态地计算和生成HTML标签插入到id为app的div中。

而我们去请求vue页面的时候都是先将index.html文件加载进来之后通过html文件中的script标签将对应的js代码引入进来,如果这些js文件过大导致下载过慢或者js运行时间太长都会导致页面长时间白屏。

所以自然而然就会想到这种解决方案,先修改index.html文件,在id为app的div中加入原生的html作为骨架屏先展示,当js运算完成之后在替换div中的内容

实现流程

但是手动在div#app中写入骨架屏明显是不合理的。

既然我们是一个vue项目,骨架屏当然也需要是一个vue文件,它能够在构件时自动注入到div#app中

新建一个用于存储骨架屏文件的vue,如Skeleton.vue
新建一个入口文件,如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插件

该插件本来是用于服务端渲染(服务端根据不同的请求生成不同的html页面返回前端,浏览器直接进行渲染,与之相对的是前端渲染,前端首先拿到部分页面,再从后台拿到数据,然后在前端组装页面),但是我们在这里使用它将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
47
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'
})
]
}

运行以下命令后会在dist文件夹下生成skeleton.json,该文件记录了skeleton.vue以及其引用的样式。这个文件会提供给vue-server-renderer使用

在根目录下新建skeleton.js,用于向index.html中插入骨架屏

使用图片作为骨架屏

这种方案比第一种还简单,只需要UI人员绘制一张图片即可,但是这种方式比第一种更加难以维护。

自动生成骨架屏

自动生成并自动插入静态骨架屏 这种方法跟第一种方法类似,不过是自动生成骨架屏,可以关注下饿了么开源的插件 page-skeleton-webpack-plugin,它根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中,不过要注意的是这个插件目前只支持history方式的路由,不支持hash方式,且目前只支持首页的骨架屏,并没有组件级的局部骨架屏实现。

特点

Page Skeleton 是一款 webpack 插件,该插件的目的是根据你项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中。

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

安装

npm install --save-dev page-skeleton-webpack-plugin

npm install --save-dev html-webpack-plugin

使用

修改build/webpack.base.config.js

这个文件配置的是webpack打包时的配置,既webpack打包的loader,插件,生成文件的路径等,这个文件会被webpack.dev.config.js和webpack.prod.config.js文件引用,用于开发环境和生产环境的打包。

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'], // 将需要生成骨架屏的路由添加到数组中
})
]
}

由于插件是根据process.env.NODE_ENV 环境变量来选择不同的操作,因此需要在package.json 文件中 scrpt选项显示配置环境变量如下

1
2
3
4
"scripts": {
"dev": "cross-env NODE_ENV=development node server.js",
"build": "rm -rf dist && cross-env NODE_ENV=production webpack --progress --hide-modules"
}

其中的cross-env也是一个插件,可以动态修改NODE_ENV变量的值

修改 HTML Webpack Plugin 插件的模板

在根元素内部添加

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 左右时间,当插件准备好骨架页面后,会自动通过浏览器打开预览页面

  3. 扫描预览页面中的二维码,可在手机端预览骨架页面,可以在预览页面直接编辑源码,通过点击右上角写入按钮,将骨架页面写入到 shell.html 文件中。

  4. 通过 webpack 重新打包应用,当页面重新启动后,就能够在获取到数据前看到应用的骨架结构了。

注意事项

这里可能会遇到一个问题,那就是开发环境可以显示,但是生产环境没有生成插入骨架屏,原因可能是在webpack.prod.config,js中存在一个配置会消除所有注释,这个配置会将标签删除,所以找不到骨架屏的插入点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
plugins: [
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
// removeComments: true, 移除注释
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
]

使用vue-skeleton-webpack-plugin插件

它将插入骨架屏的方式由手动改为自动,原理在构建时使用 Vue 预渲染功能,将骨架屏组件的渲染结果 HTML 片段插入 HTML 页面模版的挂载点中,将样式内联到 head 标签中。这个插件可以给单页面的不同路由设置不同的骨架屏,也可以给多页面设置,同时为了开发时调试方便,会将骨架屏作为路由写入router中

安装

npm install vue-skeleton-webpack-plugin

创建骨架屏组件 src/keleton.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
<template>
<div>
<div class="skeleton">
<div class="skeleton-head"></div>
<div class="skeleton-body">
<div class="skeleton-name"></div>
<div class="skeleton-title"></div>
<div class="skeleton-content"></div>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'skeleton'
};
</script>

<style scoped>
.skeleton {
padding: 15px;
}
.skeleton .skeleton-head,
.skeleton .skeleton-name,
.skeleton .skeleton-title,
.skeleton .skeleton-content,
.skeleton .skeleton-content {
background: rgb(194, 207, 214);
}
.skeleton-head {
width: 33px;
height: 33px;
border-radius: 50%;
float: left;
}
.skeleton-body {
margin-left: 50px;
}
.skeleton-name{
width: 150px;
height: 30px;
margin-bottom: 10px;
}
.skeleton-title {
width: 100%;
height: 30px;
}
.skeleton-content {
width: 100%;
height: 30px;
margin-top: 10px;
}
</style>
创建骨架屏的入口文件 src/entry-skeleton.js
1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue'
import Skeleton from './Skeleton'
export default new Vue({
components: {
Skeleton
},
template: `
<div>
<skeleton id="skeleton1" style="display:none"/>
</div>`
})
创建骨架屏的webpack配置文件
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
'use strict';
const path = require('path')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const nodeExternals = require('webpack-node-externals')
const config = require('../config')
const utils = require('./utils')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap

function resolve(dir) {
return path.join(__dirname, dir)
}

let skeletonWebpackConfig = 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: []
})

// important: enable extract-text-webpack-plugin
// 重点配置
skeletonWebpackConfig.module.rules[0].options.loaders = utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: true
})

module.exports = skeletonWebpackConfig
分别在webpack.prod.conf.js和webpack.dev.conf.js plugins中引入插件

首先需要引入vue-skeleton-webpack-plugin插件

1
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')

使用插件,在plugins数组中添加一项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new SkeletonWebpackPlugin({
webpackConfig: require('./webpack.skeleton.conf'),
quiet: true,
minimize: true,
router: {
mode: 'history',
routes: [
{
path: '/client/a/Quiksns/comment', //对应使用路由
skeletonId: 'skeleton1' // 所用骨架屏的id标识
},
]
}
}),
验证

启动vue项目后在浏览器输入 http://localhost:8080/client/a/Quiksns/comment

我们可以看到在页面加载出来之前会有一个骨架屏出现,如果页面加载太快看不清楚也可以使用chrome Devtools 的 performance选项卡去录制操作(注意勾选screenshot选项),我们就可以看到每一帧的页面情况。

参考文章

https://zhuanlan.zhihu.com/p/35505062

https://www.cnblogs.com/FarmanKKK/p/9712913.html