Vue2.x项目骨架屏注入(上)

5,299 阅读4分钟

骨架屏注入实践一:vue-skeleton-webpack-plugin

骨架屏是在页面数据尚未加载前先给用户展示出页面的大致结构,直到请求数据返回后再渲染页面,补充进需要显示的数据内容。

骨架屏是预渲染机制中一种增强用户体验的方式。

02.png

生成骨架屏的技术方案有很多,大概方案有:

  • 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、创建一个仅使用骨架屏组件的入口文件:

07.png

/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

08.png

注解:

要是多个页面不同的,则需要编写对应页面的不同的骨架屏(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.jsbuild/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 时无法匹配当前路由路径的?

用于匹配每个 Skeletonpath选项可以填写字符串或者正则。如果想匹配 /page1?key=value 这样的路由路径,可以直接写正则 path: /^/page1/

实际中本项目若是动态参数的路由,路由写成:

path: '/play/:type/:id',    //如路由为动态参数,必须跟router的配置一样

无效,改为正则的即ok

path: /\/commercialActivity\?/,

总结

  • 手写HTML、CSS的方式为目标页定制骨架屏

  • 骨架屏可以理解为是当数据还未加载进来前,页面的一个空白版本,一个简单的关键渲染路径

  • 主要思路就是使用 vue-server-renderer 这个本来用于服务端渲染的插件,用来把我们写的.vue文件处理为HTML,插入到页面模板的挂载点中,完成骨架屏的注入。缺点:这种方式不甚文明,如果页面样式改变了,还得改一遍骨架屏,增加了维护成本。

  • 使用图片作为骨架屏:简单暴力,让UI同学多花点功夫;小米商城的移动端页面采用的就是这个方法,它是使用了一个Base64的图片来作为骨架屏。

  • 交互问题:当dom节点数据加载回来后,骨架屏与真正页面会有一个空白屏的过渡,当网速很慢的情况下,这个白屏现象就会很明显!(首屏骨架->然后挂载vue白屏直->页面