+-
在vue项目中使用骨架屏

现在的应用开发,基本上都是前后端分离的,前端主流框架有SPA、MPA等,那么解决页面渲染、白屏时间成为首要关注的点

  webpack可以按需加载,减小首屏需要加载代码的体积;

  使用CDN技术、静态代码等缓存技术,可以减小加载渲染的时长

  问题:但是首页依然存在加载、渲染等待时长的问题。那么如何从视觉效果上减小首屏白屏的时间呢?

  骨架屏:举个例子:其实就是在模版文件中id=app容器下面写想要展示的效果,在new Vue(option)之后,该id下的内容就被替换了( 这时候,可能Vue编译生成的内容还没有挂载。因为new Vue的时候会进行一系列的初始化,这也需要耗费时间的)。这样就可以从视觉上减小白屏的时间

  骨架屏的实现方式

  1、直接在模版文件id=app容器下面,写进想要展示的效果html

  2、直接在模板文件id=app容器下面,用图片展示

  3、使用vue *提供的webpack插件

  4、自动生成并且自动插入静态骨架屏

  方式1和方式2存在的缺陷:针对不同入口,展示的效果都一样,导致不能灵活的针对不同的入口,展示不同的样式

  方式3可以针对不同的入口展示不同的效果。(实质也是先通过*生成一个json文件,然后将json文件内容注入到模板文件的id=app容器下)

  方案一、直接在模版文件id=app容器下面,写进想要展示的效果html

  在根目录的模版文件内写进内容,如红色圈出来的地方

  在浏览器打开项目

  在调用new Vue之前的展示效果(只是做了个简单效果,不喜勿喷):

  可以看到elements中id=app的容器下内容,就是我们写进的骨架屏效果内容

  在看下调了new Vue之后的效果,id=app容器下的内容被vue编译生成的内容替换了

  方案二、直接在模板文件id=app容器下面,用图片展示(这个就不做展示了)

  方案三、使用vue *提供的webpack插件:即用.vue文件完成骨架屏

  在方案一的基础上,将骨架屏的代码抽离出来,不在模版文件里面书写代码,而是在vue文件里面书写游戏效果代码,这样便于维护

  1、在根目录下建一个skeleton文件夹,在该目录下创建文件App.vue文件(根组件,类似Vue项目的App.vue)、home.skeleton.vue(首页骨架屏展示效果的代码,类似Vue项目写的路由页面)、skeleton-entry.js(入口文件类似Vue项目的入口文件)、plugin/server-plugin.js(vue-server-renderer包提供了server-plugin插件,从里面将代码拷贝出来)

  home.skeleton.vue(首页骨架屏展示效果的代码)

  <template>

  <div class="skeleton-home">

  <div>加载中...</div>

  </div>

  </template>

  <style>

  .skeleton-home {

  width: 100vw;

  height: 100vh;

  

  }

  </style>

  App.vue(根组件)

  <template>

  <div id="app">

  <!-- 根组件 -->

  <home style="display:none" id="homeSkeleton"></home>

  </div>

  </template>

  <script>

  import home from './home.skeleton.vue'

  export default{

  components: {

  home

  }

  }

  </script>

  <style>

  #app {

  font-family: 'Avenir', Helvetica, Arial, sans-serif;

  -webkit-font-smoothing: antialiased;

  -moz-osx-font-smoothing: grayscale;

  text-align: center;

  color: #2c3e50;

  }

  *{

  padding: 0;

  margin: 0;

  }

  <
  var prefix = "[vue-server-renderer-webpack-plugin]"; var prefix = www.sangpi.comvar warn = exports.warn = function (msg) { return console.error(red((prefix + " " + msg + "\n"))); };

  var tip = exports.tip = function (msg) { return console.log(yellow((prefix + " " + msg + "\n"))); };

  var validate = function (compiler) {

  if (compiler.options.target !== 'node') {

  warn('webpack config target should be "node".');

  }

  if (compiler.options.output && compiler.options. !== 'commonjs2') {

  warn('webpack config `` should be "commonjs2".');

  }

  if (!compiler.options.externals) {

  tip(

  'It is recommended to externalize dependencies in the server build for ' +

  'better build performance.'

  );

  }

  };

  var VueServerPlugin = function VueServerPlugin (options) {

  if ( options === void 0 ) options = {};

  this.options = Object.assign({

  filename: 'vue-*-server-bundle.json'

  }, options);

  };

  Vue*ServerPlugin.prototype.apply = function apply (compiler) {

  var this$1 = this;

  validate(compiler);

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

  var stats = compilation.getStats().toJson();

  var entryName = Object.keys(stats.entrypoints)[0];

  var entryAssets = stats.entrypoints[entryName].assets.filter(isJS);

  if (entryAssets.length > 1) {

  throw new Error(

  "Server-side bundle should have one single entry file. " +

  "Avoid using CommonsChunkPlugin in the server config."

  )

  }

  var entry = entryAssets[0];

  if (!entry || typeof entry !== 'string') {

  throw new Error(

  ("Entry \"" + entryName + "\" not found. Did you specify the correct entry option?")

  )

  }

  var bundle = {

  entry: entry,

  files: {},

  maps: {}

  };

  stats.assets.forEach(function (asset) {

  if (asset.name.match(/.js$/)) {

  bundle.files[asset.name] = compilation.assets[asset.name].source();

  } else if (asset.name.match(/.js.map$/)) {

  bundle.maps[asset.name.replace(/.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());

  }

  // do not emit anything else for server

  delete compilation.assets[asset.name];

  });

  var json = JSON.stringify(bundle, null, 2);

  var filename = this$1.options.filename;

  compilation.assets[filename] = {

  source: function () { return json; },

  size: function () { return json.length; }

  };

  cb();

  });

  };

  module.exports = Vue*ServerPlugin;

  2、新建一个骨架屏构建配置文件:build/webpack.skeleton.conf.js,这个文件配合vue-server-renderer插件,将App.vue内容构建成单个json格式的文件

  'use strict'

  const path = require('path')

  const nodeExternals = require('webpack-node-externals')

  const Vue*ServerPlugin = require('../skeleton/plugin/server-plugin')

  module.exports = {

  // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),

  // 并且还会在编译 Vue 组件时,

  // 告知 vue-loader 输送面向服务器代码(server-oriented code)。

  

  // 对 bundle renderer 提供 source map 支持

  devtool: 'source-map',

  // 将 entry 指向应用程序的 server entry 文件

  entry: path.resolve(__dirname, '../skeleton/skeleton-entry.js'),

  output: {

  path: path.resolve(__dirname, '../skeleton'), // 生成的文件的目录

  publicPath: '/skeleton/',

  filename: '[name].js',

  

  },

  module: {

  rules: [

  {

  test: /.vue$/,

  loader: 'vue-loader',

  options: {

  compilerOptions: {

  preserveWhitespace: false

  }

  }

  },

  {

  test: /.css$/,

  use: ['vue-style-loader', 'css-loader']

  }

  ]

  },

  performance: {

  hints: false

  },

  //

  //

  // 外置化应用程序依赖模块。可以使服务器构建速度更快,

  // 并生成较小的 bundle 文件。

  externals: nodeExternals({

  // 不要外置化 webpack 需要处理的依赖模块。

  // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,

  // 你还应该将修改 global(例如 polyfill)的依赖模块列入白名单

  allowlist: /.css$/

  }),

  // 这是将服务器的整个输出

  // 构建为单个 JSON 文件的插件。

  // 不配置filename,则默认文件名为 vue-***-server-bundle.json

  plugins: [

  new Vue*ServerPlugin({

  filename: 'skeleton.json'

  })

  ]

  }

  3、使用webpack-cli运行文件webpack.skeleton.conf.js,生成skeleton.json文件,放置在文件夹skeleton下

  在package.json文件里面书写运行命令:create-skeleton

  "scripts": {

  "create-skeleton": "webpack --progress --config build/webpack.skeleton.conf.js",

  "fill-skeleton": "node ./skeleton/skeleton.js"

  }

  在控制台上运行命令:

  npm run create-skeleton

  文件夹skeleton下就会多出skelleton.json文件

  4、将生成的skeleton.json内容注入到根目录下的index.html(模版文件)

  1)在文件夹skeleton下新建skeleton.js

  // 将生成的skeleton.json的内容填充到模板文件中

  const fs = require('fs')

  const { resolve } = require('path')

  const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

  // 读取skeleton.json,以skeleton/index.html为模版写入内容

  const renderer = createBundleRenderer(resolve(__dirname, '../skeleton/skeleton.json'), {

  template: fs.readFileSync(resolve(__dirname, '../skeleton/index.html'), 'utf-8')

  })

  // 把上一步模版完成的内容写入根目录下的模版文件'index.html'

  renderer.renderToString({}, (err, html) => {

  if (err) {

  return console.log(err)

  }

  console.log('render complete!')

  fs.writeFileSync('index.html', html, 'utf-8')

  })

  2)添加运行命令:fill-skeleton

  "fill-skeleton": "node ./skeleton/skeleton.js"

  3)在控制台上运行该命令,则skeleton.json文件内容被填充至根目录下的模板文件index.html了