在开发webapp的时候总是会受到首屏加载时间过长的影响,主流的解决方法是在载入完成之前显示loading图效果,而一些大公司会配置一套服务端渲染的架构来解决这个问题。考虑到ssr所要解决的一系列问题,越来越多的APP采用了“骨架屏”的方式去提升用户体验。

小米商城:

7f784639629ad7bc2c2a86622a0f50f5357ded7a

一、分析Vue页面的内容加载过程

vue项目中的入口index.html只有简单的内容:


<!DOCTYPE html><html lang="zh-CN"><head>    <  http-equiv="Content-Type" content="text/html;charset=UTF-8">    < >Document</ ></head><body>    <div id="root">            </div>    <  type="text/ " src="bundle.js"></ ></body></body></html>

当js执行完之后,会用vue渲染成的dom将div#root完全替换掉。
我们在div#root中加入模拟骨架屏,在Chrome开发者工具调整网速:


<div id="root">    这里是骨架屏</div>

87d8712abb390aedec7db99018f157c4c84f13f5

由此可知,将骨架屏内容直接插入div#root中即可实现骨架屏。

二、使用vue-server-renderer来实现骨架屏

我们需要骨架屏也是一个单独的.vue文件,因此我们需要用到vue-server-renderer。对vue服务端渲染有所了解的同学一定知道,这个插件能够将vue项目在node端打包成一个bundle,然后由bundle生成对应的html。
首先是生成项目:


.├── build│   ├── webpack.config.client.js│   └── webpack.config.server.js├── src│   └── views│        ├── index│        │   └── index.vue│        ├── skeleton│        │   └── skeleton.vue│        ├── app.vue│        ├── index.js│        └── skeleton-entry.js├── index.html└── skeleton.js└── package.json

vue的服务端渲染一般会用vue-server-renderer将整个项目在node端打包成一份bundle,而这里我们只要一份有骨架屏的html,所以会有一个单独的骨架屏入口文件skeleton-entry.js,一个骨架屏打包webpack配置webpack.config.server.js,而skeleton.js作用是将webpack打包出来的bundle写入到index.html中。


//skeleton-entry.jsimport Vue from 'vue'import Skeleton from './views/skeleton/skeleton.vue'export default new Vue({  components: {    Skeleton  },  template: '<skeleton />'})//webpack.config.server.jsconst path = require('path')const { VueLoaderPlugin } = require('vue-loader')const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')module.exports = {  mode: process.env.NODE_ENV,  target: 'node',  entry: path.join(__dirname, '../src/skeleton-entry.js'),  output: {    path: path.join(__dirname, '../server-dist'),    filename: 'server.bundle.js',    libraryTarget: 'commonjs2'  },  module: {    rules: [      {        test: /.vue$/,        loader: 'vue-loader'      },      {        test: /.css$/,        use: [          'vue-style-loader',          'css-loader'        ]      }        ]  },  externals:  .keys(require('../package.json').dependencies),  resolve: {    alias: {      'vue$': 'vue/dist/vue.esm.js'    }  },  plugins: [    new VueLoaderPlugin(),    new VueSSRServerPlugin({      filename: 'skeleton.json'    })  ]}

其中骨架屏的webpack配置因为是node端,所以需要target: 'node' libraryTarget: 'commonjs2'。在VueSSRServerPlugin中,指定了其输出的json文件名。当执行webpack会在/server-dist目录下生成一个skeleton.json文件,这个文件记载了骨架屏的内容和样式,会提供给vue-server-renderer使用。


//skeleton.jsconst fs = require('fs')const path = require('path')const createBundleRenderer = require('vue-server-renderer').createBundleRenderer// 读取`skeleton.json`,以`index.html`为模板写入内容const renderer = createBundleRenderer(path.join(__dirname, './server-dist/skeleton.json'), {  template: fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')})// 把上一步模板完成的内容写入(替换)`index.html`renderer.renderToString({}, (err, html) => {  fs.writeFileSync('index.html', html, 'utf-8')})


注意,作为模板的html文件,需要在被写入内容的位置添加<!--vue-ssr-outlet-->占位符,本例子在div#root里写入:<div id="root"> <!--vue-ssr-outlet--></div>

最后执行node skeleton就能实现vue的骨架屏。
最终的index.html:

<!DOCTYPE html><html lang="zh-CN"><head>    <  http-equiv="Content-Type" content="text/html;charset=UTF-8">    <  http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">    <  name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">    < >Document</ ><style data-vue-ssr-id="a7049cb4:0">.skeleton[data-v-61761ff8] {  position: relative;  height: 100%;  overflow: hidden;  padding: 15px;  box-sizing: border-box;  background: #fff;}.skeleton-nav[data-v-61761ff8] {  height: 45px;  background: #eee;  margin-bottom: 15px;}.skeleton-swiper[data-v-61761ff8] {  height: 160px;  background: #eee;  margin-bottom: 15px;}.skeleton-tabs[data-v-61761ff8] {  list-style: none;  padding: 0;  margin: 0 -15px;  display: flex;  flex-wrap: wrap;}.skeleton-tabs-item[data-v-61761ff8] {  width: 25%;  height: 55px;  box-sizing: border-box;  text-align: center;  margin-bottom: 15px;}.skeleton-tabs-item span[data-v-61761ff8] {  display: inline-block;  width: 55px;  height: 55px;  border-radius: 55px;  background: #eee;}.skeleton-banner[data-v-61761ff8] {  height: 60px;  background: #eee;  margin-bottom: 15px;}.skeleton-productions[data-v-61761ff8] {  height: 20px;  margin-bottom: 15px;  background: #eee;}</style></head><body>    <div id="root">        <div data-server-rendered="true" class="skeleton page" data-v-61761ff8><div class="skeleton-nav" data-v-61761ff8></div> <div class="skeleton-swiper" data-v-61761ff8></div> <ul class="skeleton-tabs" data-v-61761ff8><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li><li class="skeleton-tabs-item" data-v-61761ff8><span data-v-61761ff8></span></li></ul> <div class="skeleton-banner" data-v-61761ff8></div> <div class="skeleton-productions" data-v-61761ff8></div><div class="skeleton-productions" data-v-61761ff8></div><div class="skeleton-productions" data-v-61761ff8></div><div class="skeleton-productions" data-v-61761ff8></div><div class="skeleton-productions" data-v-61761ff8></div><div class="skeleton-productions" data-v-61761ff8></div></div>    </div></body></html>

看下效果:

1807d0974e9daed2bb194ce63cd7707516db1b0c

效果还是阔以的。

尾声

文章开头小米商城手机页面就是用的这样的方法,不同的是它的骨架屏是一个 64的图片。

更多关于vue-server-renderer内容请戳vue-ssr


原文发布时间为:2018年05月23日
原文作者:掘金
本文来源: 掘金 如需转载请联系原作者

收藏 打印