一. 骨架屏简介

简单来说, 骨架屏就是填充了背景等特效的真实页面手稿轮廓图。 它可以是精确/粗略的描述了页面各个元素大小,形状,位置占位的一种页面真实数据渲染加载前的排版。 目的是加载页面过程中给用户一种较好体验的过渡效果,降低用户的焦灼情绪。避免页面过大/网络过慢长时间白屏或者闪烁。

二. 骨架屏的实现方案

目前生成骨架屏的技术方案大概有三种:

  1. 使用图片,svg 或者手动编写骨架屏代码: 使用 HTML + CSS 的方式可以很快的完成骨架屏效果, 但是面对视觉设计的改变及需求的更迭, 对骨架屏的跟进修改会非常被动, 这种机械化重复劳作的方式就显得机动性不足;
  2. 通过预渲染手动书写的代码生成相应的骨架屏: 该方案做的比较成熟的是 vue-skeleton-webpack-plugin , 通过vueSSR 结合 webpack 在构建时渲染写好的vue 骨架屏组件, 将预渲染生成的Dom 节点和相关样式插入到最终输出的 HTML 中;
    该方案与vue 技术直接关联, 在当今前端架构三分天下的环境下,不是一个很灵活,可控的方案
  3. 饿了么内部的生成骨架页面工具: 该方案通过一个webpack 插件 page-skeleton-webpack-plugin 的方式与项目开发无缝集成, 属于自动生成骨架屏方面做得非常完善的一种。并且可以启动UI界面专门调整骨架屏, 但是在面对复杂的页面也会有不尽人意的地方,他生成的骨架屏节点是基于页面本身的结构和css, 存在嵌套比较深的情况, 体积不会太小, 并且只支持history 模式。

如果你的项目Webpack构建,且非SSR:

四. 一个基于方案二没有使用任何第三方骨架屏插件的粗暴快捷方案

方案前提:

既然是解决首屏效果。 考虑到效率和成本, 骨架屏给大致轮廓即可,忽略细节上的设计。

方案设计

一般开发流程是这样的: 给出设计稿/静态html -> 开发 -> 测试 -> …
其中设计稿/静态HTML, 可能是UED手写/第三方UI库 。

这里我们的方案是:

  1. 开发一个提供:占位标签动画, 背景色及关闭骨架屏开关等属性的组件;
  2. 根据个人需求,在需要展示骨架屏效果的页面/标签上设置组件提供的属性即可;
  3. 通过vue ssr server build后, 会自动生成相关静态标签/css 到其配置文件中(这是webpack自动做的知道即可);

这里在设置占位高度有个小技巧: min-height

注: 本方案基于VUE SSR, 这里不对SSR的相关实现及设计做讲解。只要你的项目已支持SSR 即可。

方案优点

  1. 简单快捷;
  2. 依赖少;
  3. 门槛低;
  4. 较灵活;
  5. 控制权交给开发者;
  6. 支持自定义骨架屏展示时间;
  7. 支持根据实际数据返回,控制骨架屏展示

方案缺点

  • 需要根据自己的需求,手动设置标签盒子的背景色/高/宽度;

走进源码及实例

  • 骨架屏组件源码:
<template>
<div :class="[openSkeleton? 'skeleton-animate':'']">
  <slot :openSkeleton="openSkeleton"></slot>
</div>
</template>

<script>
// 骨架屏计时器
let skeletonTimeoutIns = null
@Component
export default class Dialoglogin extends Vue {
  // 手动控制骨架屏 关闭
  @Prop({
    default: false
  })
  closeSkeleton
  // 骨架屏自动关闭秒数 (3秒:  3000 关闭)
  @Prop({
    default: 1000
  })
  timeOutSecond
  // 是否为自动模式(setTimeout 来自动控制开/关)
  @Prop({
    default: false
  })
  isAuto

  @Watch('isAuto', {
    immediate: true
  })
  watchIsAuto(newVal, oldVal) {
    if (!!newVal) {
      !!skeletonTimeoutIns && clearTimeout(skeletonTimeoutIns)
      skeletonTimeoutIns = setTimeout(() => {
        this.openSkeleton = false
      }, this.timeOutSecond)
    } else {
      clearTimeout(skeletonTimeoutIns)
      skeletonTimeoutIns = null
    }
  }

  @Watch('closeSkeleton', {
    immediate: true
  })
  watchCloseSkeleton(newVal, oldVal) {
    if (!!newVal) {
      clearTimeout(skeletonTimeoutIns)
      skeletonTimeoutIns = null
      this.openSkeleton = false
    }
  }

  // 开启骨架屏
  openSkeleton = true
  mounted() {}
}
</script>
<style scoped>
.skeleton-animate {
  -webkit-animation: van-skeleton-blink 1.2s ease-in-out infinite;
  animation: van-skeleton-blink 1.2s ease-in-out infinite;
}
.skeleton-bgcolor {
  background-color: rgb(231, 230, 230);
  /*#f2f3f5;*/
}
</style>

上面即为骨架屏的主要源码, 优化空间还是很大的,比如开放背景色API, 动画API 等等。

  • 在页面中使用骨架屏组件
<template>
 
    .....................
    
    <SkeletonAll v-slot:default="slotProps" :is-auto="true" :time-out-second="4000">
    <div class="special skeleton-bgcolor" style="margin-bottom: 6px; border-top: 1px solid #eee; min-height: 200px;">
      <van-tabs v-model="activeSpecial" animated v-show="!slotProps.openSkeleton">
        <van-tab style="height:200px" v-for="(item, index) in specialInfoList" :key="index" :title="item.title">
          <van-image style="width:100%; height: 211px;" :src="item.img" @click="handleTabsItemClick">
            <template v-slot:loading>
              <van-loading type="spinner" size="30" color="#1989fa" />
            </template>
          </van-image>
        </van-tab>
      </van-tabs>
    </div>
    </SkeletonAll>
    
    ......................................
    
</template>

<script>
    .........
    import SkeletonAll from '@/components/skeleton-all'
    .........
</script>

细心的同学应该留意到, 这里在骨架屏标签盒子上控制切换用的是 v-show 而非 v-if 。 为什么? 哈哈~

最终实现效果

骨架屏:

android Shimmer骨架屏 骨架屏方案_HTML


加载数据后:

android Shimmer骨架屏 骨架屏方案_加载_02