骨架屏注入实践一:vue-skeleton-webpack-plugin
骨架屏是在页面数据尚未加载前先给用户展示出页面的大致结构,直到请求数据返回后再渲染页面,补充进需要显示的数据内容。
骨架屏是预渲染机制中一种增强用户体验的方式。
生成骨架屏的技术方案有很多,大概方案有:
1、手动编写骨架屏代码
2、通过预渲染手动书写的代码生成相应的骨架屏, 比如:vue-skeleton-webpack-plugin
3、饿了么内部的生成骨架页面的工具:page-skeleton-webpack-plugin
4、JavaScript操作DOM 的方式结合 Puppeteer 自动生成网页骨架屏
其他... ...
本文主要介绍其中的一种:vue-skeleton-webpack-plugin
(一)单页面骨架屏具体操作
1、在项目中安装 vue-skeleton-webpack-plugin
npm i vue-skeleton-webpack-plugin -D
2、创建一个仅使用骨架屏组件的入口文件:
/src/components/Skeleton/entry-skeleton.js
import Vue from 'vue';
import Skeleton from './Skeleton';
export default new Vue({
components: {
Skeleton
},
template: '<Skeleton />'
});
/src/components/Skeleton/Skeleton.vue
<template>
<div class="skeleton-wrapper">
<header class="skeleton-header"></header>
<section class="skeleton-block">
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==">
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==">
</section>
</div>
</template>
<script>
export default {
name: 'skeleton'
};
</script>
<style scoped>
.skeleton-header {
height: 152px;
background: grey;
margin-top: 60px;
width: 152px;
margin: 60px auto;
}
.skeleton-block {
display: flex;
flex-direction: column;
padding-top: 8px;
}
</style>
3、创建一个用于服务端渲染的 webpack 配置对象,将刚创建的入口文件指定为 entry 依赖入口:build/webpack.skeleton.conf.js
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', // 区别默认的‘web’
devtool: false,
entry: {
app: resolve('../src/components/Skeleton/entry-skeleton.js') // 多页传入对象
},
output: Object.assign({}, baseWebpackConfig.output, {
libraryTarget: 'commonjs2'
}),
externals: nodeExternals({
whitelist: /\.css$/
}),
plugins: []
})
4、创建一个用于服务端渲染的 webpack 配置对象,将刚创建的入口文件指定为 entry 依赖入口:build/webpack.dev.conf.js和build/webpack.prod.conf.js
... ...
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
module.exports = merge(baseWebpackConfig, {
... ...
plugins: [
... ....
// inject skeleton content(DOM & CSS) into HTML
new SkeletonWebpackPlugin({
webpackConfig: require('./webpack.skeleton.conf'),
quiet: true
}),
]
})
5、安装后运行,npm run dev 运行,报错:
throw new Error(
^
Error:
Vue packages version mismatch:
- vue@2.5.16
- vue-server-renderer@2.6.12
This may cause things to work incorrectly. Make sure to use the same version for both.
原因:vue的版本与vue-server-renderer版本不一致导致的,因此需要vue与vue-server-renderer安装同一个版本。
注:
原项目的vue版本是vue@2.3.3,但是为了统一,尝试了vue-server-renderer@2.3.3及vue-server-renderer@2.3.4+vue@2.3.4,无效,升级为vue@2.5.16+vue-server-renderer@2.5.16
npm i vue@2.5.16 vue-server-renderer@2.5.16 -D
再次npm run dev 即ok
(二)多页面骨架屏具体操作
1、再新建Skeleton2.vue文件:
/src/components/Skeleton2.vue
注解:
要是多个页面不同的,则需要编写对应页面的不同的骨架屏(css样式编写的页面),这样才能一一匹配, 因此需要写对应页面骨架屏的样式或者是ui的设计(页面采用图片或base64);
总结一句话:这样交互虽好,但是工作量是比较大的,要是页面后期改动较大的话,则整个骨架屏都需要改动。
<template>
<div class="skeleton-wrapper">Skeleton For Home</div>
</template>
<script>
export default {
name: 'skeleton2'
};
</script>
<style scoped>
.skeleton-header {
height: 152px;
background: grey;
margin-top: 60px;
width: 152px;
margin: 60px auto;
}
.skeleton-block {
display: flex;
flex-direction: column;
padding-top: 8px;
}
</style>
2、修改入口文件entry-skeleton.js
import Vue from 'vue';
import Skeleton from './Skeleton';
import Skeleton2 from './Skeleton2'; // 新增
export default new Vue({
components: {
Skeleton,
Skeleton2 // 新增
},
// 注意:id="skeleton"或id="skeleton2"中的id是关键,是控制对应是否显示的关键
template: `
<div>
<Skeleton
id="skeleton" style="display:none"/>
<Skeleton2 id="skeleton2" style="display:none"/>
</div>
`
});
3、修改build/webpack.dev.conf.js和build/webpack.prod.conf.js
注解:build/webpack.prod.conf.js一定要配置,因为打包上线的时候需要注入的。
... ...
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
... ...
plugins: [
// inject skeleton content(DOM & CSS) into HTML
new SkeletonWebpackPlugin({
webpackConfig: require('./webpack.skeleton.conf'),
quiet: true,
minimize: true, // 选填 SPA 下是否需要压缩注入 HTML 的 JS 代码
router: {
mode: 'hash',
routes: [
{ // 插入骨架屏路由1
path: /\/comboHelper/, // 路由是采用正则,哪个页面需要加入骨架屏则写对应页面的页面路由
skeletonId: 'skeleton' // 此处就是对应是否显示入口文件entry-skeleton.js中template那个id的html显示的
},
{ // 插入骨架屏路由1
path: /\/commercialActivity\?/,
skeletonId: 'skeleton'
},
{
path: '/',
skeletonId: 'skeleton2',
},
]
}
}),
]
注意事项:
用于匹配每个 Skeleton 的 path选项可以填写字符串或者正则。如果想匹配 /page1?key=value 这样的路由路径,可以直接写正则 path: /^/page1/。
实际中本项目若是动态参数的路由,路由写成:
path: '/play/:type/:id', //如路由为动态参数,必须跟router的配置一样
无效,改为正则的即ok
path: /\/commercialActivity\?/,
总结
-
手写HTML、CSS的方式为目标页定制骨架屏
-
骨架屏可以理解为是当数据还未加载进来前,页面的一个空白版本,一个简单的关键渲染路径
-
主要思路就是使用
vue-server-renderer这个本来用于服务端渲染的插件,用来把我们写的.vue文件处理为HTML,插入到页面模板的挂载点中,完成骨架屏的注入。缺点:这种方式不甚文明,如果页面样式改变了,还得改一遍骨架屏,增加了维护成本。 -
使用图片作为骨架屏:简单暴力,让UI同学多花点功夫;小米商城的移动端页面采用的就是这个方法,它是使用了一个Base64的图片来作为骨架屏。
-
交互问题:当dom节点数据加载回来后,骨架屏与真正页面会有一个空白屏的过渡,当网速很慢的情况下,这个白屏现象就会很明显!(
首屏骨架->然后挂载vue白屏直->页面)