骨架屏效果
在我们访问页面时,当资源未加载完成时,通常会出现空白,这种体验不是很好,常用的解决方法有添加loading页、使用骨架屏,相比于loading页,骨架屏更贴近实际内容。如下图所示:
刷新页面时资源未加载成功时,先展示骨架屏。
骨架屏原理
在使用vue框架生成的项目中,当我们访问页面时,最先加载的是一个名为index.html文件,此时它里面就只有一个id为app的div元素,只有当js资源加载完成后,vue框架才会生成页面展示需要的DOM节点并注入到页面中进行显示,js资源加载时间的长短就决定了空白时长,如果在这段时间index.html内含有内容,那么用户看到的就不再是空白,而骨架屏效果就是利用这一原理,将页面的内容占位情况在用户访问之前就写入到index.html中,这样在js资源未加载成功时,用户看到的就是页面内容占位情况。这其中需要用到服务端渲染知识,在服务端将含有骨架屏内容的页面生成好,浏览器只负责展示。vue项目服务端渲染我们使用官方比较推荐的vue-server-renderer
。
在vue项目中如何引入骨架屏
以使用vue-cli3默认方式创建的vue项目为例,介绍骨架屏的引入:
骨架屏的实现方式有多种,可以简单使用一张图片代替,也可以写一个vue组件,也可以引用一些工具在打包时自动生成,本文是手动写了一个骨架屏vue组件。
首先根据页面内容编写对应的骨架屏组件,如下:
// HelloWorldSkeleton.vue
<template>
<div class="skeleton-wrapper">
<div class="img-placeholder"></div>
<h1 class="h1-placeholder"></h1>
<p class="p-placeholder"></p>
<h3 class="h3-placeholder"></h3>
<ul class="ul-container">
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
</ul>
<h3 class="h3-placeholder"></h3>
<ul class="ul-container">
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
</ul>
<h3 class="h3-placeholder"></h3>
<ul class="ul-container">
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
<li class="li-placeholder"></li>
</ul>
</div>
</template>
<script>
export default {
name: 'hello-world-skeleton'
}
</script>
<style lang="scss" scoped>
.skeleton-wrapper{
margin-top: 60px;
text-align: center;
.img-placeholder{
display: inline-block;
width: 200px;
height: 200px;
background: #e5e5e5;
}
.h1-placeholder, .p-placeholder{
width: 400px;
height: 37px;
margin: 0 auto;
background: #e5e5e5;
}
.p-placeholder{
margin: 16px auto;
}
.h3-placeholder{
width: 200px;
height: 22px;
margin: 0 auto;
background: #e5e5e5;
}
.ul-container{
list-style-type: none;
padding: 0;
}
.li-placeholder{
display: inline-block;
width: 80px;
height: 18px;
margin: 0 10px;
background: #e5e5e5;
}
}
</style>
骨架屏编写完成后,接下来就是如何引用的问题了。vue-cli3创建的项目主要是用于客户端渲染,而骨架屏需要用到服务端渲染,即同一项目出现两种渲染方式,这就需要我们对项目的打包配置进行修改,vue-cli3对webpack的集成度比较高,有好处也有坏处,好处就是省事,帮我们免去一些基本配置,坏处就是当我们需要更改webpack配置时会不太容易且有一定限制,vue-cli3允许我们在项目根目录下创建vue.config.js
配置文件,在该文件中通过配置configureWebpack
来修改webpack相关配置。
vue-cli3创建的项目中自带的webpack配置是用于客户端渲染,我们可以不对客户端渲染的打包配置进行配置,只需要添加服务端渲染的相关配置即可,具体如下:
// vue.config.js
const path = require('path')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const nodeExternals = require('webpack-node-externals')
const ISSERVER = process.env.WEBPACK_TARGET === 'node'
module.exports = {
configureWebpack: () => {
if (ISSERVER) {
return {
target: 'node',
entry: path.join(__dirname, './src/components/skeleton-entry.js'),
devtool: 'source-map',
output: {
libraryTarget: 'commonjs2'
},
externals:nodeExternals({
whitelist: /\.css$/
}),
plugins: [
new VueSSRServerPlugin()
]
}
}
}
}
vue.config.js
文件中通过接收变量WEBPACK_TARGET
来区分是客户端渲染还是服务端渲染,如果是服务端渲染,我们需要重新配置webpack的入口文件等属性。变量WEBPACK_TARGET
是如何传入的呢?当然是通过执行package.json
中scripts
命令来传入的,因此需要对scripts
命令进行修改:
{
...
"scripts": {
"serve:client": "vue-cli-service serve",
"serve:server": "npm run build:server && node skeleton.js",
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
"build": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json",
"build:mac": "npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json",
"lint": "vue-cli-service lint",
"dev": "npm run serve:server && npm run serve:client"
},
...
}
命令解读:其中,serve:client
命令用于在本地启动服务(用于浏览客户端渲染的相关页面);serve:server
命令中串联执行了npm run build:server
和node skeleton.js
命令,npm run build:server
用于服务端渲内容的打包,利用vue-server-renderer/server-plugin
插件最终输出一个json文件,node skeleton.js
用于读取生成的json文件,并通过vue-server-renderer
的createBundleRenderer
方法将内容写入到index.html文件中;build:client
命令用于打包客户端渲染相关的内容;build:server
命令用于打包服务端渲染的相关内容,通过传入WEBPACK_TARGET=node
参数,告诉webpack使用服务端渲染相关配置进行打包,如果不带--mode server
参数,那么打包时会报错,如下:
附上地址
build
命令就是将build:server
命令与build:client
命令结合,由于在执行vue-cli-service build
命令时会删除dist文件夹,当build:server
生成的json文件在dist中时继续执行build:client
命令会将json文件删除,因此使用了move命令先将生成的json文件移出dist文件夹,当build:client
命令执行完后,再移入dist中;build:mac
命令为在mac下的构建命令,作用与build
命令相同;dev
命令就是将serve:server
与serve:client
命令结合,开启本地服务(既可以看到服务端渲染的页面也可以看到客户端渲染的页面)。
那么入口文件skeleton-entry.js
中是什么呢?类似于main.js文件,创建一个vue实例,并将骨架屏组件引入,如下:
// skeleton-entry.js
import Vue from 'vue'
import HelloWorldSkeleton from './HelloWorldSkeleton.vue'
export default new Vue({
components: {
HelloWorldSkeleton
},
template: '<hello-world-skeleton />'
})
skeleton.js用于将服务端打包生成的json文件通过vue-server-renderer
写入到index.html中,具体如下:
// skeleton.js
const fs = require('fs')
const { resolve } = require('path')
const bundle = require('./dist/vue-ssr-server-bundle.json')
const createBundleRenderer = require('vue-server-renderer').createBundleRenderer
// 读取`skeleton.json`,以`index.html`为模板写入内容
const renderer = createBundleRenderer(bundle, {
template: fs.readFileSync(resolve(__dirname, './public/index.html'), 'utf-8')
})
// 把上一步模板完成的内容写入(替换)`index.html`
renderer.renderToString({}, (err, html) => {
console.log(html)
fs.writeFileSync('./public/index.html', html, 'utf-8')
})
至此,在vue项目中引用骨架屏的demo就完成了,这只是一个简单的demo,实际项目中还需要考虑路由切换时骨架屏加载问题,以及开发过程中服务端渲染的热更新实现问题,后面将会继续深入。
demo源码可在github上获取
参考文献:
[1] Vue页面骨架屏
[2] Vue 页面骨架屏注入实践
[3] 通过vue-cli3构建一个SSR应用程序