7

Vue应用加载过程

我们先来看看vue的入口文件index.html里面的内容,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

image

从图里看到vue页面从打开到加载完成是有一段白屏的时间的,那白屏的加载体验对于首次访问的用户来说是难以接受的,我们可以使用尺寸稳定的骨架屏,来辅助实现真实模块占位和瞬间加载。

vue应用从打开到页面展示完整大致经历了一下几步

1.先渲染index.html的内容
2.动态插入chunk-vendors.js(第三方库)
3.动态插入app.js(业务代码)
4.渲染路由对应的页面

只有当app.js文件渲染完成之后,当前路由对应的vue才会被渲染到页面里面,而app.js是最后才被插入到index.html里的,因此会导致在app.js未加载之前会有一段空白的显示。

这会引出了一个用户体验的问题:用户看到页面内容之前的空白时间过长,这样页面流失率会大大的增加,这也是我们不愿意看到的。

据统计:加载超过5秒就会有74%的用户离开页面。

骨架屏是什么

在页面完全渲染完成之前,用户会看到一个样式简单,描绘了当前页面的大致框架,感知到页面正在逐步加载,最终骨架屏中各个占位部分被完全替换,体验良好。常用于文章列表、动态列表页等相对比较规则的列表页面。
很多项目中都有应用: 饿了么h5版本,知乎,facebook等网站中都有应用。

image

这个过程中用户会觉得内容正在逐渐加载即将呈现,降低了用户的焦躁情绪,使得加载过程主观上变得流畅。

骨架屏相比于传统的loading图会在感官上觉得内容出现的流畅而不突兀,体验更加优良。

所以骨架屏的应用大大提高了用户体验。

下面我们看看具体的实现方法

骨架屏实践

从vue渲染过程动图中我们能看到index.html是最快加载的出来的
所以骨架屏需要放到index.html里面
在index.html将骨架屏Dom结构写好,然后在规定好的钩子函数里面去移除这个骨架屏就可以实现骨架屏机制。

但是这种方式有一个问题,如果每个页面的骨架屏的样式都不同,我们需要手动在index.html写好每个页面对应的结构样式,然后再去解析路由去显示对应的骨架屏。这样很麻烦,不好维护。

呢有没有一个插件帮我们去干两件事情:
1.将骨架屏vue文件提前渲染到index.html里面
2.渲染路由对应的骨架屏

这样一来就大大解放了双手,代码维护性也大大提高了。

vue-skeleton-webpack-plugin

github地址:https://github.com/lavas-proj...

这是一个基于 Vue 的 webpack 插件,为单页/多页应用生成骨架屏 skeleton,减少白屏时间,在页面完全渲染之前提升用户感知体验。

将骨架屏也看成路由组件,在构建时使用 Vue 服务端渲染功能,将骨架屏组件的渲染结果 HTML 片段插入 HTML 页面模版的挂载点中,将样式内联到 head 标签中。这样等前端渲染完成时,Vue 将使用客户端混合,把挂载点中的骨架屏内容替换成真正的页面内容。

  • 插件大致实现思路

    1. 通过vue-server-renderer将骨架屏的Vue 实例渲染为 HTML
    2. Vue webpack 项目使用了 HTML Webpack Plugin生成 HTML 文件,通过该插件的html-webpack-plugin-before-html-processing 事件的回调函数插入骨架屏的内容。

插件具体实现可参考作者文章

  • 实现步骤
  1. 新建一个骨架屏的vue文件并将骨架屏的样式写好

image

  1. 在src文件加下新建一个skeleton.js文件,将刚才创建的骨架屏文件引入,注册在components里面,并在template里面写入这个组件,注意:这里的组件一定要声明ID值
import Vue from 'vue'
import SkeletonIndex from './components/skeleton/index'
export default new Vue({
    components: {
        SkeletonIndex
    },
    template: `
        <div>
            <SkeletonIndex id="skeletonIndex" style="display:none" />
        </div>
    `
})

3.因为我的项目是vue-cli3,所以要对vue.config.js操作,这里需要引入skeleton.js,并且可以在routes里面配置路由对应的骨架屏组件。注意extract: true必须要配置,否则骨架屏vue里的样式不会被插入到style里面。~~~~其他配置项可以到作者github上查询。

const path = require('path');
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');

module.exports = {
  css: {
    extract: true
  },
  configureWebpack: {
    plugins: [
      new SkeletonWebpackPlugin({
        webpackConfig: {
          entry: {
            app: path.join(__dirname, './src/skeleton.js'),
          },
        },
        router: {
          mode: process.env.NODE_ENV === 'development' ? 'hash' : 'history',
          routes: [
            { path: /.+/, skeletonId: 'skeletonIndex' },
          ]
        }
      }),
    ],
  },
};

配置完成重启应用看一下效果
image

实际应用遇到的问题

由于这个插件使用的服务端渲染,所以当app.$mount挂载成功后,骨架屏的内容会被替换掉。
当我们的业务代码慢慢变多后,页面dom的绘制会变慢,所以会出现骨架屏消失后会有短暂白屏的情况。

解决方案:

因为我发现在app.vue里面注册的全局组件会比<router-view></router-view>里的提前渲染,所以我在app.vue里面注册一个骨架屏的全局变量,然后在router.beforeEach里面去根据路由动态渲染对应的骨架屏。

// app.vue
<template>
  <div id="app">
    <router-view></router-view>
    <Skeleton />
  </div>
</template>
<script>
import Skeleton from './components/app/skeleton/index'

export default {
  components: {
    Skeleton
  }
}
</script>

// ./components/app/skeleton/index.vue
<template>
    <div v-if="skeletonName" class="skeleton-wrapper">
        <component :is="skeletonName"></component>
    </div>
</template>
<script>
import { mapState } from 'vuex'
import allTop from './allTop'
import bless_sort from './bless_sort'
import classify_singer from './classify_singer'
import home from './home'
import loading from './loading'
import mv from './mv'
import photo from './photo'
import photov2 from './photov2'
import send_video from './send_video'
export default {
    computed: {
        ...mapState({
            skeletonName: state => state.skeletonName
        })
    },
    components: {
        allTop,
        bless_sort,
        classify_singer,
        home,
        loading,
        mv,
        photo,
        photov2,
        send_video
    }
}
</script>


// main.js
router.beforeEach((to, from, next) => {
    store.commit('SAVE_TOGGLE_SKELETON', false)
    if (util.skeletonCheckRoute(to.path)) {
      store.commit('SAVE_TOGGLE_SKELETON', util.skeletonCheckRoute(to.path, 'getPath'))
    }
    next()
}

这样就解决了短暂白屏的问题,如果有更好的方法可以提出来。

总结

单应用的一个最大的问题就是首屏加载东西太多,加载时间过长。
而骨架屏的应用只是降低了用户焦虑,我们还是要优化代码体积。

  • 按需加载
  • 异步组件
  • webpack-bundle-analyzer
  • ...

绘制骨架屏插件

vue-content-loader
在线绘制骨架屏网站:https://skeletonreact.com/

Vue Content Loader是一个基于Vue.js的SVG占位符加载,可自定义的SVG组件,用于创建占位符加载,例如Facebook加载卡。

Vue Content Loader是react-content-loader的Vue实现。


familyAboveAll
156 声望17 粉丝