实际项目中使用骨架屏,试用各种方案(基于vue-cli 3.0)

6,121 阅读5分钟

前言

领导需求骨架屏,我等屌丝员工只能上了,调研试用了各种方案,记录一下坑与收获。

UI库自带的骨架屏

我们的项目使用的是vant,试用了一把vant自带的骨架屏,与领导需求不符,放弃。

饿了么开源方案

page-skeleton-webpack-plugin

安装

npm install --save-dev page-skeleton-webpack-plugin
npm install --save-dev html-webpack-plugin

vue.config.js

const { SkeletonPlugin } = require("page-skeleton-webpack-plugin");
const path = require("path");

...
  configureWebpack: config => {
    ...
    config.plugins = [
      // 使用骨架屏插件
      new SkeletonPlugin({
        pathname: path.resolve(__dirname, "./shell"), // 用来存储 shell 文件的地址
        staticDir: path.resolve(__dirname, "./dist"), // 最好和 `output.path` 相同
        routes: ["/"] // 将需要生成骨架屏的路由添加到数组中
      })
    ];
  },
  
  chainWebpack: config => {
    if (isProduction) {
      // 解决vue-cli3脚手架创建的项目压缩html 干掉<!-- shell -->导致骨架屏不生效
      config.plugin("html").tap(opts => {
        opts[0].minify.removeComments = false;
        return opts;
      });
    }
  }

进入项目根目录

cd /project-a

mkdir shell

index.html

<!--添加shell-->
<div id="app"><!-- shell --></div>

重新启动,报错

这是因为使用page-skeleton-webpack-plugin 之前需要预加载html-webpack-plugin,上面的vue.config.js写法可能导致html-webpack-plugin没有在骨架屏插件前面加载成功,改变写法:

vue.config.js

...
 configureWebpack: {
    externals: {
      vue: "Vue"
    },
    plugins: [
      new SkeletonPlugin({
        pathname: path.resolve(__dirname, "./shell"), // 用来存储 shell 文件的地址
        staticDir: path.resolve(__dirname, "./dist"), // 最好和 `output.path` 相同
        routes: ["/"] // 将需要生成骨架屏的路由添加到数组中
      })
    ]
  },
  ...

重新启动,成功

报错没有这样路径的文件,由于众所周知的原因我无法使用history方式的路由(饿了么方案必须使用history的方式),加上此方案只能首屏使用,再三考虑,放弃了使用此方案!

vue-server-renderer方案

这个方案使用下来和下面的vue-skeleton-webpack-plugin感觉差不多,感觉vue-skeleton-webpack-plugin稍微智能一点,此方案放弃

vue-skeleton-webpack-plugin方案

vue-skeleton-webpack-plugin

安装

npm install vue-skeleton-webpack-plugin

vue.config.js

  configureWebpack: config => {
    config.plugins = [
      new SkeletonWebpackPlugin({
        webpackConfig: {
          entry: {
            app: path.join(__dirname, "./src/skeleton.js")
          }
        },
        minimize: true,
        quiet: true
      })
    ];
  },

在src下新建js和vue模板

cd src
mkdir skeleton.js
vim skeleton.vue

skeleton.js

import Vue from "vue";
import Skeleton from "./Skeleton.vue";

export default new Vue({
  components: {
    Skeleton
  },
  render: h => h(Skeleton)
});

skeleton.vue

<template>
  <div class="skeleton-wrapper">
    <section class="skeleton-block">
      <!-- eslint-disable vue/max-len -->
      <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-block {
  display: flex;
  flex-direction: column;
  padding: 16px;
}
</style>

重新启动,报错:

vue、vue-server-renderer版本不匹配,执行命令更新npm包

npm update

再次重新启动,报错:

此问题,和上面遇见的一模一样,只是提示的方式不同而已,修改vue.config.js

  configureWebpack: {
    ...
    plugins: [
      new SkeletonWebpackPlugin({
        webpackConfig: {
          entry: {
            app: path.join(__dirname, "./src/skeleton.js")
          }
        },
        minimize: true,
        quiet: true
      })
    ]
  },

重新启动,ok,骨架屏已经出来了

不是我想要的效果,修改skeleton.vuevue.config.js

skeleton.vue

<template>
  <div class="skeleton">
    <div class="skeleton-head">
      <div class="head-item"></div>
      <div class="head-item"></div>
      <div class="head-item"></div>
    </div>
    <div class="skeleton-content"></div>
    <div class="skeleton-content"></div>
  </div>
</template>

<script>
export default {
  name: "skeleton"
};
</script>

<style scoped>
.skeleton {
  padding: 10px;
}

.skeleton-head {
  width: 100%;
  display: flex;
  justify-content: space-between;
}

.head-item {
  width: 30%;
  height: 50px;
}

.skeleton-content {
  width: 100%;
  height: 200px;
  margin-top: 20px;
  border-radius: 10px;
}

.skeleton .head-item,
.skeleton .skeleton-content {
  background: rgb(194, 207, 214);
  background-image: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.15) 25%,
    transparent 25%
  );
  background-size: 20rem 20rem;
  animation: skeleton-stripes 1s linear infinite;
}

@keyframes skeleton-stripes {
  from {
    background-position: 0 0;
  }
  to {
    background-position: 20rem 0;
  }
}
</style>

vue.config.js

css: {
    // 开启CSS样式分离,开发环境下,默认为false,extract默认为false的时候,vue-skeleton-webpack-plugin自定义样式不生效,一片空白的
    extract: true,
    ...
  },

现在这样的是,所有页面统一使用一个骨架屏,不好,还是改成单独每个页面使用单独的骨架屏吧!

src下新建文件夹skeleton

cd src
mkdir skeleton
// 移动骨架屏文件到skeleton文件夹下
mv skeleton.vue ./skeleton

修改 skeleton.js

import Vue from "vue";
import skeletonHome from "./skeleton/Skeleton.vue";

export default new Vue({
  components: {
    skeletonHome
  },
  template: `
        <div>
            <skeletonHome id="skeleton-home" style="display:none"/>
        </div>
    `
});

修改vue.config.js

 configureWebpack: {
    ...
    plugins: [
      // vue-skeleton-webpack-plugin 骨架屏
      new SkeletonWebpackPlugin({
        webpackConfig: {
          entry: {
            app: path.join(__dirname, "./src/skeleton.js")
          }
        },
        minimize: true,
        quiet: true,
        // 根据路由显示骨架屏
        router: {
          mode: "hash",
          routes: [
            {
              path: "/",
              skeletonId: "skeleton-home"
            }
          ]
        }
      })
    ]
  },

重新启动,ok

到这里就告一段落了,如果哪个页面需要有骨架屏,就在skeleton文件夹下新建一个.vue文件,在skeleton.js中引入,在vue.config.js中添加对应的路由即可

结尾

本来想弄一个完全自动的骨架屏方案,根据每一个页面自动生成一个骨架屏,但是我本人太菜了,暂时对此功能无能为力。市面上的方案就完全没有一个能全自动的,饿了么说是计划弄一个,遥遥无期;淘宝骨架屏方案,搜索了一下,只是有一些文章介绍,貌似木有开源;腾讯搜索更多的是小程序骨架屏方案;京东貌似有一个dps方案,我去看了下他们的git,讲的挺好,但是demo没有实际参考的意义,有很多疑问,而且还是新出,本人不是勇士,还是等等吧!

参考

给项目加一个骨架屏吧
基于vue-cli实现自动生成Skeleton Page,多页skeleton
基于 vue-skeleton-webpack-plugin 的骨架屏实战