骨架屏

首屏加载的演进

  • 进度条:明确知道交互所需时间,或者知道一个大概值的时候我们选择使用进度条。
  • Spinner:无法预测获取数据、或者打开页面的时长。

用途:

  • 告诉用户所进行的操作需要等待一段时间。
  • 其次,安抚用户,让其耐心等待。

缺陷:

  • 无法让用户感知到页面加载得更快
  • 无法给用户一个焦点,让用户将关注集中到这个焦点上,并且知道这个焦点即将呈现用户感兴趣的内容

用户感知

  • 大约有 47% 的用户期望他们的页面在两秒之内加载完成。
  • 如果页面加载时间超过 3s,大约有 40% 的用户选择离开或关闭页面。

有份研究报告是 MIT 神经科学家在 2014 年做的研究,人类可以在 13ms 内感知到离散图片的存在,并将图片的大概信息传输到我们的大脑中,在接下来的 100 到 140ms 之间,大脑会决定我们的眼睛具体关注图片的什么位置,也就是获取图片的关注焦点。从另一个角度来看,如果用户进行某项交互(比如点击某按钮),要让用户感知不到延迟或者数据加载,我们大概有 200 ms 的时间来准备新的界面信息呈现给用户。

在 200ms 到 1s 之间,用户似乎还感知不到自己处在交互等待状态,当一秒钟后依然得不到任何反馈,用户将会把其关注的焦点移到其他地方,如果等待超过 10s,用户将对网站失去兴趣,并浏览其他网站。

骨架屏(Skeleton Screen)

  • 骨架屏是一个页面的空白版本,通过这个空白版本传递信息,我们的页面正在渐进式的加载过程中

  • 在最开始关于 MIT 2014 年的研究中已有提到,用户大概会在 200ms 内获取到界面的具体关注点,在数据获取或页面加载完成之前,给用户首先展现骨架屏,骨架屏的样式、布局和真实数据渲染的页面保持一致,这样用户在骨架屏中获取到关注点,并能够预知页面什么地方将要展示文字什么地方展示图片,这样也就能够将关注焦点移到感兴趣的位置。当真实数据获取后,用真实数据渲染的页面替换骨架屏,如果整个过程在 1s 以内,用户几乎感知不到数据的加载过程和最终渲染的页面替换骨架屏,而在用户的感知上,出现骨架屏那一刻数据已经获取到了,而后只是数据渐进式的渲染出来。这样用户感知页面加载更快了。

  • 再看看现在的前端框架, ReactVueAngular 已经占据了主导地位,市面上大多数前端应用也都是基于这三个框架或库完成,这三个框架有一个共同的特点,都是 JS 驱动,在 JS 代码解析完成之前,页面不会展示任何内容,也就是所谓的白屏。用户是极其不喜欢看到白屏的,什么都没有展示,用户很有可能怀疑网络或者应用出了什么问题。 拿 Vue 来说,在应用启动时,Vue 会对组件中的 data 和 computed 中状态值通过 Object.defineProperty 方法转化成 set、get 访问属性,以便对数据变化进行监听。而这一过程都是在启动应用时完成的,这也势必导致页面启动阶段比非 JS 驱动(比如 jQuery 应用)的页面要慢一些。

优势

  1. 用户避免看到长时间的白页
  2. 可以获知页面的大体结构,减小用户认为页面出错而离开的机率
  3. 与菊花图相比视觉更加流畅

实现方案

UI 骨架图

  • 实现起来比较容易。

  • 缺点:就是需要 UI 设计师支持和开发介入,不能自动生成。

手写屏代码

  • 使用 HTML + CSS 的方式,可以很快的完成骨架屏效果,做到对页面真实样式的复刻

  • 缺点:面对视觉设计的改版以及需求的更迭,我们对骨架屏的跟进修改会非常被动,这种机械化重复劳作的方式此时未免显得有些机动性不足;

预渲染生成

  • 通过预渲染手动书写的代码生成相应的骨架屏
  • vue-skeleton-webpack-plugin:通过 vueSSR 结合 webpack 在构建时渲染写好的 vue 骨架屏组件,将预渲染生成的 DOM 节点和相关样式插入到最终输出的 html 中。
 // webpack.conf.js
 const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');
 plugins: [
  //...
  new SkeletonWebpackPlugin({
    webpackConfig: {
      entry: {
        app: resolve('./src/entry-skeleton.js')
      }
    }
  })
 ]

缺陷:

  1. 静态渲染,不可交互
  2. 可能与真实的页面数据有些出入

自动生成

page-skeleton-webpack-plugin
  • 饿了么内部的生成骨架页面的工具
  • 优点:生成骨架屏的方案可以根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中。
  • 缺陷:
    • 实际使用过程中无法监听接口返回导致生成骨架屏的时机是否准确
    • 生成的页面与业务人员写的结构质量有直接关系,经常出现需要手工二次调整的情况
    • 嵌套比较深,体积不会太小,并且只支持 history 模式
// webpack.conf.js
 const HtmlWebpackPlugin = require('html-webpack-plugin')
 const { SkeletonPlugin } = require('page-skeleton-webpack-plugin')
 const path = require('path')

 plugins: [
  //...
  new HtmlWebpackPlugin({
    // Your HtmlWebpackPlugin config
  }),
  new SkeletonPlugin({
    pathname: path.resolve(__dirname, `${customPath}`), // 用来存储 shell 文件的地址
    staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同
    routes: ['/', '/search'], // 将需要生成骨架屏的路由添加到数组中
  })
 ]
draw-page-structure

缺陷

  • 只能对线上已经存在的 URL 生成骨架屏,不支持开发环境
  • 由于是自动生成,当页面存在重定向的情况时,生成的骨架屏可能与预期不一致
  • 由于内部实现并不完善,可能导致某些结构复杂的页面下生成的骨架屏需要二次优化调整

实现

  • 区分入口节点

    1. 只遍历可见区域可见的 DOM 节点:非隐藏元素、宽高大于 0 的元素、非透明元素、内容不是空格的元素、位于浏览窗口可见区域内的元素等;
    2. 针对(背景)图片、文字、表单项、音频视频、Canvas、自定义特征的块等区域来生成颜色块;
    3. 页面节点使用的样式不可控,所以不可取 style 的尺寸相关的值,可通过 getBoundingClientRect 获取节点宽、高、距离视口距离的绝对值,计算出与当前设备的宽高对应的百分比作为颜色块的单位,来适配不同设备;
    function isHideStyle(node) {
        return getStyle(node, 'display') === 'none' || 
            getStyle(node, 'visibility') === 'hidden' || 
            getStyle(node, 'opacity') == 0 ||
            node.hidden;
    }
    
  • 生成颜色块

    const blocks = [];
    // width,height,top,left 都是算好的百分比
    function drawBlock({width, height, top, left, zIndex = 9999999, background, radius} = {}) {
      const styles = [
        'position: fixed',
        'z-index: '+ zIndex,
        'top: '+ top +'%',
        'left: '+ left +'%',
        'width: '+ width +'%',
        'height: '+ height +'%',
        'background: '+ background
      ];
      radius && radius != '0px' && styles.push('border-radius: ' + radius);
      // animation && styles.push('animation: ' + animation);
      blocks.push(`<div style="${ styles.join(';') }"></div>`);
    }
    
  • 常见的配置项

    • url: 待生成骨架屏的页面地址
    • output.filepath: 生成的骨架屏节点写入的文件
    • output.injectSelector: 骨架屏节点插入的位置,默认 #app
    • background: 骨架屏主题色
    • animation: css3 动画属性
    • rootNode: 真对某个模块生成骨架屏
    • device: 设备类型,默认 mobile
    • extraHTTPHeaders: 添加请求头
    • init: 开始生成之前的操作
    • includeElement(node, draw): 定制某个节点如何生成
    • writePageStructure(html, filepath): 回调的骨架屏节点
  • 使用

    1. 全局安装,npm i draw-page-structure –g
    2. dps init 生成配置文件 dps.config.js
    3. 修改 dps.config.js 进行相关配置
    4. dps start 开始生成骨架屏
const dpsConfig = {
   // 默认生成位置为当前项目目录skeleton文件夹,已有骨架屏页面不会再次生成,新页面配置只需要添加新条目即可
    visa_guide: {
        url: 'https://w.mafengwo.cn/sfe-app/visa_guide.html?mdd_id=10083', // 必填项
    },
    call_charge: {
        url: 'http://localhost:8081/sfe-app/call_charge.html?rights_id=25', // 必填项 待生成骨架屏页面的地址,用百度(https://baidu.com)试试也可以
        //url:'https://www.baidu.com',
        device: 'pc', // 非必填,默认mobile
        background: '#eee', // 非必填
        animation: 'opacity 1s linear infinite;', // 非必填
        headless:false, // 非必填
        customizeElement: function(node) { // 非必填
            //返回值枚举如果是true表示不会向下递归到这层为止,如果返回值是一个对象那么节点的档子就按照对象里面的样式来绘制
            //如果返回值为0表示正常递归渲染
            //如果返回值为1表示渲染当前节点不在向下递归
            //如果返回值为2表示对当前节点不作任何处理
            if(node.className === 'navs-bottom-bar'){
                return 2;
            }
            return 0;
        },
        showInitiativeBtn: true,// 非必填 如果此值设置为true表示开发需要主动触发生成骨架屏了,此时headless需设置为false
        writePageStructure: function(html) { // 非必填
            // 自己处理生成的骨架屏
            // fs.writeFileSync(filepath, html);
            // console.log(html)
        },
        init: function() { // 非必填
            // 生成骨架屏之前的操作,比如删除干扰节点
        } 
    }
}
module.exports = dpsConfig;
posted @ 2020-09-11 17:23  千年轮回  阅读(435)  评论(0编辑  收藏  举报