vue骨架屏

什么是骨架屏

骨架屏就是在页面尚未加载之前先给用户展示出页面的大致结构,直到页面请求数据后渲染页面。
骨架屏和 loading 相比较还是骨架屏用户体验感更好。
原理其实非常简单,就是在页面还没加载完成时展示一张图片(一般是 Base64 编码),类似下方效果:

安装骨架屏插件

1
npm install vue-skeleton-webpack-plugin

Skeleton 组件

src/components 下新建 Skeleton.vue 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="skeleton">
<!-- 我是首页骨架屏图片 -->
<div class="home-skeleton">
<img src="../assets/skeleton.jpg" alt="skeleton" />
</div>
</div>
</template>

<script>
export default {};
</script>

<style lang="css" scoped>
.home-skeleton {
width: 100%;
}
</style>

导出 js

根目录下新建 skeleton.js 文件用来导出骨架组件

1
2
3
4
5
6
7
8
9
import Vue from 'vue';
import Skeleton from './components/Skeleton.vue';

export default new Vue({
components: {
Skeleton
},
template: `<Skeleton></Skeleton>`
});

单页骨架屏

在根目录下新建 vue.config.js 配置骨架屏插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');
const path = require('path');

module.exports = {
configureWebpack: {
plugins: [
new SkeletonWebpackPlugin({
webpackConfig: {
entry: {
app: path.resolve(__dirname, 'src/skeleton.js')
}
}
})
]
}
};

搞定 OK~

多页骨架屏

假如现在我们希望首页加载时出现骨架屏(上面单页的例子),还要在切换到 about 页面后也有个骨架屏。就需要再新建一个 Skeleton 组件了。

src/components/ 下新建 SkeletonAbout.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
<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 {};
</script>

<style lang="css" scoped>
.skeleton-header {
height: 40px;
background: #1976d2;
padding: 0;
margin: 0;
width: 100%;
}
.skeleton-block {
display: flex;
flex-direction: column;
padding-top: 8px;
}
</style>

然后回到 src/skeleton.js 中进行改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vue from 'vue';
import Skeleton from './components/Skeleton.vue';
import SkeletonAbout from './components/SkeletonAbout.vue';

export default new Vue({
components: {
Skeleton,
SkeletonAbout
},
// 默认都隐藏,通过在 vue.config.js 中的插件配置中根据id进行判断
template: `<div>
<Skeleton style="display: none;" id="skeleton-home"></Skeleton>
<SkeletonAbout style="display: none;" id = "skeleton-about"></SkeletonAbout>
</div>
`
});

修改 vue.config.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
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');
const path = require('path');

module.exports = {
configureWebpack: {
// 给webpack新增配置
plugins: [
new SkeletonWebpackPlugin({
webpackConfig: {
entry: {
app: path.resolve(__dirname, 'src/skeleton.js')
}
},
router: {
mode: 'history',
routes: [
{
path: '/',
skeletonId: 'skeleton-home'
},
{
path: '/about',
skeletonId: 'skeleton-about'
}
]
}
})
]
}
};

现在刷新首页,就会先显示首页骨架屏,切换到 about 页面刷新,会首先显示 about 页面的骨架屏。

骨架屏插件原理

骨架屏插件的原理也非常简单,我们只要在 webpack 的 html-webpack-plugin 插件处理 html 之前对 app 根节点下的内容进行替换就 OK 了,下面是一个简化版插件写法:

vue.config.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
63
64
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');
const path = require('path');

// 骨架屏插件原理
class MyPlugin {
apply(compiler) {
// compiler 表示当前整个webpack对象
compiler.plugin('compilation', compilation => {
// compilation 表示当前这次webpack打包的对象(也就是说多次打包会产生多个)
compilation.plugin(
// 当前webpack插件在html处理之前,把html替换
'html-webpack-plugin-before-html-processing',
data => {
data.html = data.html.replace(
`<div id="app"></div>`,
`
<div id="app">
<div id="home" style="display:none">首页骨架屏</div>
<div id="about" style="display:none">about页骨架屏</div>
</div>
<script>
if(window.hash == '#/about' || location.pathname=='/about'){
document.getElementById('about').style.display="block"
}else{
document.getElementById('home').style.display="block"
}
</script>
`
);
return data;
}
);
});
}
}

module.exports = {
configureWebpack: {
// 给webpack新增配置
plugins: [
// 使用自己的插件
new MyPlugin()
// new SkeletonWebpackPlugin({
// webpackConfig: {
// entry: {
// app: path.resolve(__dirname, 'src/skeleton.js')
// }
// },
// router: {
// mode: 'history',
// routes: [{
// path: '/',
// skeletonId: 'skeleton-home'
// },
// {
// path: '/about',
// skeletonId: 'skeleton-about'
// },
// ]
// },
// })
]
}
};