Skip to content

Vue 骨架屏

什么是骨架屏

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

骨架屏效果

安装骨架屏插件

shell
npm install vue-skeleton-webpack-plugin

Skeleton 组件

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

html
<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 文件用来导出骨架组件

js
import Vue from 'vue';
import Skeleton from './components/Skeleton.vue';

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

单页骨架屏

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

js
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 组件:

html
<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 中进行改造:

js
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 中骨架屏配置:

js
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

js
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'
      //       },
      //     ]
      //   },
      // })
    ]
  }
};