前端周刊
首发于前端周刊
Vue单页面骨架屏实践

Vue单页面骨架屏实践

github 地址: VV-UI/VV-UI

演示地址: vv-ui

文档地址:skeleton

关于骨架屏介绍

骨架屏的作用主要是在网络请求较慢时,提供基础占位,当数据加载完成,恢复数据展示。这样给用户一种很自然的过渡,不会造成页面长时间白屏或者闪烁等情况。 常见的骨架屏实现方案有ssr服务端渲染和prerender两种解决方案。这里主要通过代码为大家展示如何一步步做出这样一个骨架屏:

饿了么骨架屏

prerender 渲染骨架屏

本组件库骨架屏的实现也是基于预渲染去实现的,有关于预渲染更详细的介绍请参考这篇文章:处理 Vue 单页面 Meta SEO的另一种思路 下面我们主要介绍其实现步骤,首先我们也是需要配置webpack-plugin,不过已经有实现好的prerender-spa-plugin可用

var path = require('path')
var PrerenderSpaPlugin = require('prerender-spa-plugin')

module.exports = {
  // ...
  plugins: [
    new PrerenderSpaPlugin(
      // Absolute path to compiled SPA
      path.join(__dirname, '../dist'),
      // List of routes to prerender
      ['/']
    )
  ]
} 

然后写好我们的骨架屏文件main.skeleton.vue

 <template>
  <div class="main-skeleton">
    <w-skeleton height="80px"></w-skeleton>
    <div>
      <div class="skeleton-container">
        <div class="skeleton">
          <w-skeleton height="300px"></w-skeleton>
        </div>
        <w-skeleton height="45px"></w-skeleton>
      </div>
      <div class="skeleton-bottom">
        <w-skeleton height="45px"></w-skeleton>
      </div>
    </div>
  </div>
</template>

当初次进入页面的时候我们需要显示骨架屏,数据加载完,我们需要移除骨架屏:

 <template>
  <div id="app">
    <mainSkeleton v-if="!init"></mainSkeleton>
    <div v-else>
      <div class="body"></div>
    </div>
  </div>
</template>

 import mainSkeleton from './main.skeleton.vue'

  export default {
    name: 'app',
    data () {
      return {
        init: false
      }
    },
    mounted () {
      //  这里模拟数据请求
      setTimeout(() => {
        this.init = true
      }, 250)
    },
    components: {
      mainSkeleton
    }
  }

ssr 渲染骨架屏

下面我用我灵魂画师的笔法,画出了大致的过程:


首先创建我们的skeleton.entry.js

import Vue from 'vue';
import Skeleton from './skeleton.vue';

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

当然这里的skeleton.vue使我们事先写好的骨架屏组件,看起来可能是这样:

 <template>
    <div class="skeleton-wrapper">
        <header class="skeleton-header"></header>
        <div class="skeleton-block"></div>
    </div>
</template>

然后我们需要的是能把skeleton.entry.js编译成服务端渲染可用的bundle文件,所以我们需要有个编译骨架屏的webpack.ssr.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',
    devtool: false,
    entry: {
        app: resolve('./src/skeleton.entry.js')
    },
    output: Object.assign({}, baseWebpackConfig.output, {
        libraryTarget: 'commonjs2'
    }),
    externals: nodeExternals({
        whitelist: /\.css$/
    }),
    plugins: []
});

接下来最终的步骤,就是编写我们的webpackPlugin,我们期望我们的webpackPlugin可以帮我们把入口文件编译成bundle,然后再通过vue-server-renderer来render bundle,最终产出响应的html片段和css片段,这里贴出核心代码:

 // webpack start to work
    var serverCompiler = webpack(serverWebpackConfig);
    var mfs = new MFS();
    // output to mfs
    serverCompiler.outputFileSystem = mfs;
    serverCompiler.watch({}, function (err, stats) {

        if (err) {
            reject(err);
            return;
        }

        stats = stats.toJson();
        stats.errors.forEach(function (err) {
            console.error(err);
        });
        stats.warnings.forEach(function (err) {
            console.warn(err);
        });

        var bundle = mfs.readFileSync(outputPath, 'utf-8');
        var skeletonCss = mfs.readFileSync(outputCssPath, 'utf-8');
        // create renderer with bundle
        var renderer = createBundleRenderer(bundle);
        // use vue ssr to render skeleton
        renderer.renderToString({}, function (err, skeletonHtml) {
            if (err) {
                reject(err);
            }
            else {
                resolve({skeletonHtml: skeletonHtml, skeletonCss: skeletonCss});
            }
        });
    });

最后一步,我们对产出的html片段, css片段进行组装,产出最终的html,所以我们需要监听webpack 的编译挂载之前的事件:

compiler.plugin('compilation', function (compilation) {

    // add listener for html-webpack-plugin
    compilation.plugin('html-webpack-plugin-before-html-processing', function (htmlPluginData, callback) {
        ssr(webpackConfig).then(function (ref) {
            var skeletonHtml = ref.skeletonHtml;
            var skeletonCss = ref.skeletonCss;

            // insert inlined styles into html
            var headTagEndPos = htmlPluginData.html.lastIndexOf('</head>');
            htmlPluginData.html = insertAt(htmlPluginData.html, ("<style>" + skeletonCss + "</style>"), headTagEndPos);

            // replace mounted point with ssr result in html
            var appPos = htmlPluginData.html.lastIndexOf(insertAfter) + insertAfter.length;
            htmlPluginData.html = insertAt(htmlPluginData.html, skeletonHtml, appPos);
            callback(null, htmlPluginData);
        });
    });
 }); 

关于:

作者:monkeyWang


本文参考文章:为vue项目添加骨架屏


本文源码详见:VV-UI/VV-UI


本人主页:monkeyWang


微信公众号:会不定期推送前端技术文章,欢迎关注

weixin.qq.com/r/YiixqYj (二维码自动识别)

发布于 2017-12-12

文章被以下专栏收录

    在前端领域跟上时代的脚步,广度和深度不断精进。聚焦前端技术、框架、工具和技巧,每周会有高质量技术文章,由资深前端工程师精选精编。

    主要介绍在工作中使用vue.js的一些心得,一些分享