• 学习骨架屏(Skeleton Screens)技术


    骨架屏

    1.背景
    近两年来,前、后端分离的架构得到越来越多的认可,越来越多的团队在尝试、推广这种架构。然而在带来便利的同时,也带来了一些弊端,比如首屏渲染时间(FCP)因为首屏需要请求更多内容,比原来多了更多HTTP的往返时间(RTT),这造成了白屏,如果白屏时间过长,用户体验会大打折扣。
     
     
    为了优化首屏渲染时间这个指标,减少白屏时间,前端想了很多办法:
    加速或减少HTTP请求损耗
    延迟加载
    减少请求内容的体积

    优化用户等待体验

    这里要介绍的就是优化用户等待体验的骨架屏,目前市面上用得比较多的是下面这几个插件:
    vue-server-renderer
    vue-skeleton-webpack-plugin

    page-skeleton-webpack-plugin

    一,使用vue-server-renderer

    为了简单起见,我们使用vue-cli搭配webpack-simple这个模板来新建项目:

    vue init webpack-simple vue-skeleton

    这时我们便获得了一个最基本的Vue项目:

    .
    ├── package.json ├── src
    │   ├── App.vue │   ├── assets
    │   └── main.js ├── index.html └── webpack.conf.js

    安装完了依赖以后,便可以通过npm run dev去运行这个项目了。但是,在运行项目之前,我们先看看入口的html文件里面都写了些什么。

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> 
    <title>vue-skeleton</title>
     </head> <body> 
    <div id="app"></div> <script src="/dist/build.js"></script> </body> 
    </html>

    可以看到,DOM里面有且仅有一个div#app,当js被执行完成之后,此div#app会被整个替换掉,因此,我们可以来做一下实验,在此div里面添加一些内容:

    <div id="app"> <p>Hello skeleton</p> <p>Hello skeleton</p> <p>Hello skeleton</p> </div>

    打开chrome的开发者工具,在Network里面找到throttle功能,调节网速为“Slow 3G”,刷新页面,就能看到页面先是展示了三句“Hello skeleton”,待js加载完了才会替换为原本要展示的内容。

    hzv4.gif

    现在,我们对于如何在Vue页面实现骨架屏,已经有了一个很清晰的思路——在div#app内直接插入骨架屏相关内容即可。

    显然,手动在 div#app 里面写入骨架屏内容是不科学的,我们需要一个扩展性强且自动化的易维护方案。既然是在Vue项目里,我们当然希望所谓的骨架屏也是一个 .vue 文件,它能够在构建时由工具自动注入到 div#app 里面。

    首先,我们在/src目录下新建一个Skeleton.vue文件,其内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    <template>
      <div class="skeleton page">
        <div class="skeleton-nav"></div>
        <div class="skeleton-swiper"></div>
        <ul class="skeleton-tabs">
          <li v-for="i in 8" class="skeleton-tabs-item"><span></span></li>
        </ul>
        <div class="skeleton-banner"></div>
        <div v-for="i in 6" class="skeleton-productions"></div>
      </div>
    </template>
     
    <style>
    .skeleton {
      position: relative;
      height: 100%;
      overflow: hidden;
      padding: 15px;
      box-sizing: border-box;
      background: #fff;
    }
    .skeleton-nav {
      height: 45px;
      background: #eee;
      margin-bottom: 15px;
    }
    .skeleton-swiper {
      height: 160px;
      background: #eee;
      margin-bottom: 15px;
    }
    .skeleton-tabs {
      list-style: none;
      padding: 0;
      margin: 0 -15px;
      display: flex;
      flex-wrap: wrap;
    }
    .skeleton-tabs-item {
       25%;
      height: 55px;
      box-sizing: border-box;
      text-align: center;
      margin-bottom: 15px;
    }
    .skeleton-tabs-item span {
      display: inline-block;
       55px;
      height: 55px;
      border-radius: 55px;
      background: #eee;
    }
    .skeleton-banner {
      height: 60px;
      background: #eee;
      margin-bottom: 15px;
    }
    .skeleton-productions {
      height: 20px;
      margin-bottom: 15px;
      background: #eee;
    }
    </style>
     

    接下来,再新建一个 skeleton.entry.js 入口文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import Vue from 'vue'
    import Skeleton from './Skeleton.vue'
     
    export default new Vue({
      components: {
        Skeleton
      },
      template: '<skeleton />'
    })
     

    在完成了骨架屏的准备之后,就轮到一个关键插件vue-server-renderer登场了。该插件本用于服务端渲染,但是在这个例子里,我们主要利用它能够把.vue文件处理成html和css字符串的功能,来完成骨架屏的注入,流程如下:

    clipboard.png

    根据流程图,我们还需要在根目录新建一个webpack.skeleton.conf.js文件,以专门用来进行骨架屏的构建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    const path = require('path')
    const webpack = require('webpack')
    const nodeExternals = require('webpack-node-externals')
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
     
    module.exports = {
      target: 'node',
      entry: {
        skeleton: './src/skeleton.entry.js'
      },
      output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: '[name].js',
        libraryTarget: 'commonjs2'
      },
      module: {
        rules: [
          {
            test: /.css$/,
            use: [
              'vue-style-loader',
              'css-loader'
            ]
          },
          {
            test: /.vue$/,
            loader: 'vue-loader'
          }
        ]
      },
      externals: nodeExternals({
        whitelist: /.css$/
      }),
      resolve: {
        alias: {
          'vue$''vue/dist/vue.esm.js'
        },
        extensions: ['*''.js''.vue''.json']
      },
      plugins: [
        new VueSSRServerPlugin({
          filename: 'skeleton.json'
        })
      ]
    }
     

    可以看到,该配置文件和普通的配置文件基本完全一致,主要的区别在于其 target: 'node' ,配置了 externals ,以及在 plugins 里面加入了 VueSSRServerPlugin 。在 VueSSRServerPlugin 中,指定了其输出的json文件名。我们可以通过运行下列指令,在 /dist 目录下生成一个 skeleton.json 文件:

    1
    webpack --config ./webpack.skeleton.conf.js
     

    这个文件在记载了骨架屏的内容和样式,会提供给vue-server-renderer使用。

    接下来,在根目录下新建一个skeleton.js,该文件即将被用于往index.html内插入骨架屏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const fs = require('fs')
    const { resolve } = require('path')
     
    const createBundleRenderer = require('vue-server-renderer').createBundleRenderer
     
    // 读取`skeleton.json`,以`index.html`为模板写入内容
    const renderer = createBundleRenderer(resolve(__dirname, './dist/skeleton.json'), {
      template: fs.readFileSync(resolve(__dirname, './index.html'), 'utf-8')
    })
     
    // 把上一步模板完成的内容写入(替换)`index.html`
    renderer.renderToString({}, (err, html) => {
      fs.writeFileSync('index.html', html, 'utf-8')
    })
     

    注意,作为模板的html文件,需要在被写入内容的位置添加<!--vue-ssr-outlet-->占位符,本例子在div#app里写入:

    <div id="app"> <!--vue-ssr-outlet--> </div>

    接下来,只要运行 node skeleton.js ,就可以完成骨架屏的注入了。运行效果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>vue-skeleton</title>
      <style data-vue-ssr-id="742d88be:0">
    .skeleton {
      position: relative;
      height: 100%;
      overflow: hidden;
      padding: 15px;
      box-sizing: border-box;
      background: #fff;
    }
    .skeleton-nav {
      height: 45px;
      background: #eee;
      margin-bottom: 15px;
    }
    .skeleton-swiper {
      height: 160px;
      background: #eee;
      margin-bottom: 15px;
    }
    .skeleton-tabs {
      list-style: none;
      padding: 0;
      margin: 0 -15px;
      display: flex;
      flex-wrap: wrap;
    }
    .skeleton-tabs-item {
       25%;
      height: 55px;
      box-sizing: border-box;
      text-align: center;
      margin-bottom: 15px;
    }
    .skeleton-tabs-item span {
      display: inline-block;
       55px;
      height: 55px;
      border-radius: 55px;
      background: #eee;
    }
    .skeleton-banner {
      height: 60px;
      background: #eee;
      margin-bottom: 15px;
    }
    .skeleton-productions {
      height: 20px;
      margin-bottom: 15px;
      background: #eee;
    }
    </style></head>
      <body>
        <div id="app">
          <div data-server-rendered="true" class="skeleton page"><div class="skeleton-nav"></div> <div class="skeleton-swiper"></div> <ul class="skeleton-tabs"><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li><li class="skeleton-tabs-item"><span></span></li></ul> <div class="skeleton-banner"></div> <div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div><div class="skeleton-productions"></div></div>
        </div>
        <script src="/dist/build.js"></script>
      </body>
    </html>
     

    可以看到,骨架屏的样式通过 <style></style> 标签直接被插入,而骨架屏的内容也被放置在 div#app 之间。当然,我们还可以进一步处理,把这些内容都压缩一下。改写 skeleton.js ,在里面添加 html-minifier :

    ...
    
    + const htmlMinifier = require('html-minifier')
    
    ...
    
    renderer.renderToString({}, (err, html) => {
    +  html = htmlMinifier.minify(html, {
    +    collapseWhitespace: true,
    +    minifyCSS: true +  })
      fs.writeFileSync('index.html', html, 'utf-8')
    })

    效果:

    clipboard.png

     二,vue-skeleton-webpack-plugin

    我们希望在构建时渲染 skeleton 组件,将渲染 DOM 插入 html 的挂载点中,同时将使用的样式通过 style 标签内联。这样在前端 JS 渲染完成之前,用户将看到页面的大致骨架,感知到页面是正在加载的。

    我们当然可以选择在开发时直接将页面骨架内容写入 html 模版中,但是这会带来两个问题:

    1. 开发 skeleton 与其他组件体验不一致。
    2. 多页应用中多个页面可能共用同一个 html 模版,而又有独立的 skeleton。

    下面我们将看看插件在具体实现中是如何解决这两个问题的:

    具体实现步骤:

    1、我们用vue-cli 直接构建一下项目跑起来(具体怎么构建就不说了)

    2、进去当前项目,执行命令 : npm install vue-skeleton-webpack-plugin 

    3、我们在src目录下创建 Skeleton.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <template>
      <div class="skeleton-wrapper">
        <header class="skeleton-header"></header>
        <section class="skeleton-block">
          <img src="data:image/svg+xml;base64...">
          <img src="data:image/svg+xml;base64...">
        </section>
      </div>
    </template>
     
    <script>
      export default {
        name: 'skeleton'
      }
    </script>
     
    <style scoped>
      .skeleton-header {
        height: 40px;
        background: #1976d2;
        padding:0;
        margin: 0;
         100%;
      }
      .skeleton-block {
        display: flex;
        flex-direction: column;
        padding-top: 8px;
      }
     
    </style>
     

    4、创建入口文件:entry-skeleton.js

    1
    2
    3
    4
    5
    6
    7
    8
    import Vue from 'vue'
    import Skeleton from './Skeleton'
    export default new Vue({
      components: {
        Skeleton
      },
      template: '<Skeleton />'
    })
     

    5、我们在build 目录下创建 webpack.skeleton.conf.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    'use strict';
     
    const path = require('path')
    const merge = require('webpack-merge')
    const baseWebpackConfig = require('./webpack.base.conf')
    const nodeExternals = require('webpack-node-externals')
     
    function resolve(dir) {
      return path.join(__dirname, dir)
    }
     
    module.exports = merge(baseWebpackConfig, {
      target: 'node',
      devtool: false,
      entry: {
        app: resolve('../src/entry-skeleton.js')
      },
      output: Object.assign({}, baseWebpackConfig.output, {
        libraryTarget: 'commonjs2'
      }),
      externals: nodeExternals({
        whitelist: /.css$/
      }),
      plugins: []
    })
     

    然后在webpack.dev.conf.js和webpack.prod.conf.js分别加入

    1
    2
    3
    4
    5
    6
    const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
    // inject skeleton content(DOM & CSS) into HTML
        new SkeletonWebpackPlugin({
          webpackConfig: require('./webpack.skeleton.conf'),
          quiet: true
        })
     

    然后就完成了。

    延伸:

    1、vue-skeleton-webpack-plugin 可以 使用多个 骨架屏 ,具体的可以查看官网地址:https://github.com/lavas-project/vue-skeleton-webpack-plugin

    三,page-skeleton-webpack-plugin

    对比发现,就开发难度、灵活性和插件开源方来看,如果项目是基于vue-cli脚手架的,那么饿了么团队的 page-skeleton-webpack-plugin是你的最佳选择,如果不是,那么可以选择vue-router开源的 vue-server-renderer.

    优势:

    • 支持多种加载动画
    • 针对移动端 web 页面
    • 支持多路由
    • 可定制化,可以通过配置项对骨架块形状颜色进行配置,同时也可以在预览页面直接修改骨架页面源码
    • 几乎可以零配置使用

    安装:

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

    配置:

    第一步:配置插件,详细配置可参考官方文档
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const { SkeletonPlugin } = require('page-skeleton-webpack-plugin')
    const path = require('path')
    const webpackConfig = {
      entry: 'index.js',
      output: {
        path: __dirname + '/dist',
        filename: 'index.bundle.js'
      },
      plugin: [
        new HtmlWebpackPlugin({
           // Your HtmlWebpackPlugin config
        }),
     
        new SkeletonPlugin({
          pathname: path.resolve(__dirname, `${customPath}`), // 用来存储 shell 文件的地址
          staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同
          routes: ['/''/search'], // 将需要生成骨架屏的路由添加到数组中
        })
      ]
    }
     
    第二步:修改 HTML Webpack Plugin 插件的模板

    在你启动 App 的根元素内部添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <!-- shell -->
      </div>
    </body>
    </html>
     
    第三步:界面操作生成、写入骨架页面
    1. 在开发页面中通过 Ctrl|Cmd + enter 呼出插件交互界面,或者在在浏览器的 JavaScript 控制台内输入toggleBar 呼出交互界面。
    2. 点击交互界面中的按钮,进行骨架页面的预览,这一过程可能会花费 20s 左右时间,当插件准备好骨架页面后,会自动通过浏览器打开预览页面,如下图。

     

    uniapp中使用骨架屏

    插件:

    https://ext.dcloud.net.cn/plugin?id=256#detail

    小程序使用骨架屏

    地址:https://github.com/jayZOU/skeleton

    基于skeleton组件的骨架屏生成及其简单,主要有以下几个步骤。

    • 1、下载组件到项目中
    • 2、配置json文件,允许使用组件
    • 3、构建基本页面骨骼
    • 4、在wxml中引入组件并设置相关类

    实践

    1、这一点简单,跳过。

    2、这一点也简单,在json文件中,添加如下代码:

    1
    2
    3
    "usingComponents": {
        "skeleton""/component/skeleton/skeleton"
      },
     

    3、构建基本页面骨骼,也就是填充默认数据(模拟数据)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <view class='container skeleton'>
      <view class='row' wx:for="{{studentList}}" wx:key="{{index}}">
          <image class='skeleton-radius' src='{{item.avatarUrl}}' mode='widthFix'></image>
          <view>
              <text class='nickName skeleton-rect'>{{item.class}}-{{item.nickName}}</text>
              <text class='skeleton-rect'>{{item.detailInfo}}</text>
          </view>
      </view>
    </view>
     
     
    /* pages/student/student.wxss */
    page{
      border-top: solid 2rpx #999;
    }
    .container{
      padding: 10rpx 20rpx;
    }
    .row{
      display: flex;
      flex-direction: row;
      justify-content: flex-start;
      align-items: center;
      
      height: 100rpx;
       690rpx;
      padding: 10rpx 10rpx;
      margin-top: 20rpx;
      font-size: 26rpx;
      color: #666;
       
    }
     
    .row > image{
       100rpx;
      height: 100rpx;
      border-radius: 50%;
      margin-right: 20rpx;
    }
     
    .nickName{
      display: block;
      color: #333;
      font-size: 30rpx;
      margin-bottom: 16rpx;
    }
     

    这样就完成了页面骨骼的构架,接下来就是在wxml中引入组件并设置相关类了。

    4、在wxml中引入,可以在头部或者底部引入,如下:

    1
    2
    3
    4
    <skeleton selector="skeleton"
              loading="spin"
              bgcolor="#FFF"
              wx:if="{{showSkeleton}}"></skeleton>
     

    接下来就是设置相关类了,主要要设置3个类,分别是:skeleton、skeleton-radius和skeleton-rect。

    skeleton就是作用范围,相当于vue中的el:“#app” 的范围,一般设置给最底部的view即可。
    skeleton-radius:设置为圆形的骨架
    skeleton-rect:设置为长方形的骨架

    注意:这里的skeleton-radius和skeleton-rect渲染出来的骨架大小,是受默认的填充的元素大小影响的。

    完整的wxml代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!--pages/student/student.wxml-->
    <skeleton selector="skeleton"
              loading="spin"
              bgcolor="#FFF"
              wx:if="{{showSkeleton}}"></skeleton>
    <view class='container skeleton'>
      <view class='row' wx:for="{{studentList}}" wx:key="{{index}}">
          <image class='skeleton-radius' src='{{item.avatarUrl}}' mode='widthFix'></image>
          <view>
              <text class='nickName skeleton-rect'>{{item.class}}-{{item.nickName}}</text>
              <text class='skeleton-rect'>{{item.detailInfo}}</text>
          </view>
      </view>
    </view>
     

    然后在js中的data中加入showSkeleton变量,设置一个定时器结束(用来模拟网络请求的等待过程)。

    然后运行就可以看见效果了。

    js中的data 和 onLoad

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    data:{
            //...
            showSkeleton:true,
            //...
    },
    onLoad: function (options) {
        const that = this;
        setTimeout(() => {
          that.setData({
            showSkeleton: false
          })
        }, 5000)
      },

     
  • 相关阅读:
    在Windows Phone应用中使用Google Map替代Bing Map
    《从入门到精通:Windows Phone 7应用开发》
    判断最小割的唯一性
    ASP.NET页面生命周期
    SQL排序
    window.open
    VS2008中英文转换
    asp.net下载文件的常用方法
    TSQL Convert转换时间类型
    TreeView
  • 原文地址:https://www.cnblogs.com/cfcastiel/p/14469927.html
Copyright © 2020-2023  润新知