zoukankan      html  css  js  c++  java
  • 骨架屏

    写在前面

    现在的前端开发领域,都是前后端分离,前端框架主流的都是 SPAMPA;这就意味着,页面渲染以及等待的白屏时间,成为我们需要解决的问题点;而且大项目,这个问题尤为突出。

    webpack 可以实现按需加载,减小我们首屏需要加载的代码体积;再配合上 CDN 以及一些静态代码(框架,组件库等等…)缓存技术,可以很好的缓解这个加载渲染的时间过长的问题。

    但即便如此,首屏的加载依然还是存在这个加载以及渲染的等待时间问题;

    骨架屏是什么

    目前主流,常见的解决方案是使用骨架屏技术,包括很多原生的APP,在页面渲染时,也会使用骨架屏。(下图中,红圈中的部分,即为骨架屏在内容还没有出现之前的页面骨架填充,以免留白

    如何实现(原理分析)

    在 Vue 中,我们是通过 $mount 实例方法去挂载 vm 的;我们来简单看一下 Vue 代码里面关于 $mount 方法的实现:

    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(el)
    
      /* istanbul ignore if */
      if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
      }
    
      const options = this.$options
      // resolve template/el and convert to render function
      if (!options.render) {
      	...
      }
      return mount.call(this, el, hydrating)
    }
    

      

    我们可以看到:这段代码首先缓存了原型上的 $mount 方法,再重新定义该方法,我们先来分析这段代码。首先,它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。为什么??

    因为render生成的vNode,通过 $mount 方法,挂载在我们的定义的 DOM 元素上;这里的挂载是【替换】的意思。

    默认情况下我们的模版 index.html 里面有一个 id 为 app 的 div 元素。我们最终的应用程序代码会替换掉这个元素,也就是 <div id="app"></div>;对,我们 Vue 渲染出来的内容是替换掉它,而不是插入在这个节点中。

    这也就是 Vue 不能挂载在 body、html 这样的根节点的原因。你总不能把 body、html 这样的元素节点替换掉把。

    知识点补充:
    如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的。最后,调用原先原型上的 $mount 方法挂载。

    参考: Vue 实例挂载的实现

    一个生动的例子

    在这里插入图片描述

    我们模版(index.html)里面的内容是这样的:

    <body>
      <div id="app">
        <span style="color: red;font-size: 34px;">你好</span>
      </div>
      <!-- built files will be auto injected -->
    </body>
    

      

    模版里面的挂载点是 div#appApp.vue 里面的根节点是 div#app-two,渲染完成以后,页面上的 div#app 就变成了 div#app-two

    那么,这里分析总结出来的最重要的一点就是:Vue 的 $mount 方法挂载元素,采用的是【替换】模版中的挂载点 这样的方法,知道了这个知识点以后,我们要实现骨架屏,就有了很好的实现思路了。

    实现方式(具体实现)

    方案一、在模版中来实现骨架屏

    思路:在 index.html 中的 div#app 中来实现骨架屏,程序渲染后就会替换掉 index.html 里面的 div#app 骨架屏内容;

    在这里插入图片描述
    ok,做完了;你觉得我这个骨架屏做的怎么样。

    方案二、使用一个Base64的图片来作为骨架屏

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

    按照方案一的方案,将这个 Base64 的图片写在我们的 index.html 模块中的 div#app 里面。

    方案三、使用 .vue 文件来完成骨架屏

    我们可能不希望在默认的模版(index.html)上来进行代码的coding;想在方案一的基础上,将骨架屏的代码抽离出来,使用一个 .vue 文件来 coding,易于维护。

    1、我们在 src 下建一个 skeleton 目录,在里面创建两个文件(skeleton.vueskeleton.entry.js);skeleton.vue 就是我们的骨架屏页面的代码,skeleton.entry.js 是编译 skeleton.vue 的入口文件,类似于我们 Vue 项目中的 main.js 文件;

    
    

    2、我们还需要在新建一个 webpack.skeleton.conf.js 文件,以专门用来进行骨架屏的构建(这个文件放在哪里无所谓,可以放在根目录下,也可以放在 build 目录中)。这是一个 webpack 的配置文件,配合使用 vue-server-renderer 将我们的 skeleton.vue 文件内容构建为单个的 json 格式的文件(这是 Vue SSR 渲染的策略)

    // webpack.skeleton.conf.js
    'use strict'
    const path = require('path')
    const nodeExternals = require('webpack-node-externals')
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    
    module.exports = {
      target: 'node',
      devtool: '#source-map',
      entry: './src/skeleton/skeleton.entry.js',
      output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/dist/',
        filename: '[name].js',
        libraryTarget: 'commonjs2'
      },
      module: {
        noParse: /es6-promise.js$/,  // avoid webpack shimming process
        rules: [
          {
            test: /.vue$/,
            loader: 'vue-loader',
            options: {
              compilerOptions: {
                preserveWhitespace: false
              }
            }
          },
          {
            test: /.css$/,
            use: ['vue-style-loader', 'css-loader']
          }
        ]
      },
      performance: {
        hints: false
      },
      externals: nodeExternals({
        // do not externalize CSS files in case we need to import it from a dep
        whitelist: /.css$/
      }),
      plugins: [
        // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
        // 默认文件名为 `vue-ssr-server-bundle.json`
        new VueSSRServerPlugin({
          filename: 'skeleton.json'
        })
      ]
    }
    

      

    3、写完 skeleton.vue 的内容以后,使用 webpack-cli 运行这个 webpack.skeleton.conf.js 配置文件。

    // package.json
    "skeleton": "webpack --progress --config build/webpack.skeleton.conf.js"
    然后运行:
    
    npm i webpack-cli@3.3.10 -D
    npm run skeleton

    就会在 dist 文件夹中生成一个skeleton.json 文件。

    4、将 skeleton.json 内容插入到模版文件 index.html 中。(在根目录下创建一个 skeleton.js 文件)

    // skeleton.js
    const fs = require('fs')
    const { resolve } = require('path')
    const { createBundleRenderer } = require('vue-server-renderer')
    
    function createRenderer(bundle, options) {
      return createBundleRenderer(bundle, Object.assign(options, {
        // recommended for performance
        // runInNewContext: false
      }))
    }
    
    const handleError = err => {
      console.error(`error during render : ${req.url}`)
      console.error(err.stack)
    }
    
    const bundle = require('./dist/skeleton.json')
    const templatePath = resolve('./index.html')
    const template = fs.readFileSync(templatePath, 'utf-8')
    const renderer = createRenderer(bundle, {
      template
    })
    
    // console.log(renderer)
    
    /**
     * 说明:
     * 默认的index.html中包含<%= BASE_URL %>的插值语法
     * 我们不在生成骨架屏这一步改变模板中的这个插值
     * 因为这个插值会在项目构建时完成
     * 但是如果模板中有这个插值语法,而我们在vue-server-renderder中使用这个模板,而不传值的话,是会报错的
     * 所以,我们去掉模板中的插值,而使用这个传参的方式,再将这两个插值原模原样返回到模板中
     * 
     * 文档: https://cli.vuejs.org/zh/guide/html-and-static-assets.html#%E6%8F%92%E5%80%BC
     */
    const context = {
      title: '',  // default title
      meta: `<meta name="theme-color" content="#4285f4">
        <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <link rel="stylesheet" href="<%= BASE_URL %>css/reset.css">`
    }
    
    renderer.renderToString(context, (err, html) => {
      if(err) {
        return handleError(err)
      }
      fs.writeFileSync(resolve(__dirname, './index.html'), html, 'utf-8')
    })
    

      

    5、模版 index.html 加上插槽注解
    这里需要注意的是:index.html 中的 div#app 中要加一个注解插槽,<!--vue-ssr-outlet--> 这个是必须的,Vue SSR 文档中有说这个。这个注解是必须的,请注意!

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title>vue-for-test</title>
      </head>
      <body>
        <div id="app">
          <!--vue-ssr-outlet-->
        </div>
        <!-- built files will be auto injected -->
      </body>
    </html>

    参考连接:https://ssr.vuejs.org/zh/guide/#%E4%BD%BF%E7%94%A8%E4%B8%80%E4%B8%AA%E9%A1%B5%E9%9D%A2%E6%A8%A1%E6%9D%BF

    6、执行

    node skeleton.js

    执行成功后,模版 index.html 中的 div#app 中的内容就会变成我们的骨架屏代码;

    7、看一下效果
    在这里插入图片描述
    这个骨架屏,你觉得效果如何?

    线上可以看到效果的例子也是有的: map-chart;记得选择,浏览器 -> network -> slow 3G 模式来预览 骨架屏效果。

    在方案三中,还涉及到了 Vue SSR 的内容,关于 SSR 的知识的学习,可以参考我之前写的一个教程: https://github.com/Neveryu/vue-ssr-lessons

    方案四、自动生成并自动插入静态骨架屏

    饿了么开源的插件 page-skeleton-webpack-plugin ,它根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中,不过要注意的是这个插件目前只支持 history 方式的路由,不支持 hash 方式,且目前只支持首页的骨架屏,并没有组件级的局部骨架屏实现,作者说以后会有计划实现(issue9)。

    另外还有个插件 vue-skeleton-webpack-plugin,它将插入骨架屏的方式由手动改为自动,原理在构建时使用 Vue 预渲染功能,将骨架屏组件的渲染结果 HTML 片段插入 HTML 页面模版的挂载点中,将样式内联到 head 标签中。这个插件可以给单页面的不同路由设置不同的骨架屏,也可以给多页面设置,同时为了开发时调试方便,会将骨架屏作为路由写入 router 中,可谓是相当体贴了。

  • 相关阅读:
    另类多线程生产者与消费者模式
    redis.conf配置详细翻译解析
    数据库优化之索引使用简介
    Comparable和Comparator的区别
    spring中用到哪些设计模式
    JVM之几种垃圾收集器简单介绍
    angular.extend()和 angular.copy()的区别
    ThreadLocal是什么?保证线程安全
    excel文件怎么使用php进行处理
    ubuntu 安装ssh 服务
  • 原文地址:https://www.cnblogs.com/ygunoil/p/14007175.html
Copyright © 2011-2022 走看看