异步数据加载完成前的内容占位符


一个在前端通过接口拉取数据再渲染的页面,比如我们用 vue + webpack 打包做的各种应用,一般来说有两段等待时间:

“打开页面”到“加载完入口JS”的时间

这段时间页面可能只有一个 <div id="app"></div>作为启动的根节点,或者干脆直接挂载在 document.body上。因为我们所有的页面内容都是由js去生成、插入到页面中的,所以除了已经在 head 里先加载好的样式,文档树里可能什么都没有。而如果我们用了各种类库模块打包进了入口js, 会导致 js 体积变大,这段加载等待的空白时间就会变久。因此为了提高用户的体验,比如说加一个加载动画条之类的效果,可能不得不把这段节点放在入口文件之外——也就是原本的 index.html 之中。

首先制作一段纯 css 的加载动画“转菊花”,让它固定在屏幕中间,并且保持最高层级不致被覆盖。加载动画的样式使用 sass 里的 for 循环简化了各个写法:

html:

<div class="loading" id="loading">
  <div class="m-loading">
    <div class="m-loading-line m-line-1"></div>
    <div class="m-loading-line m-line-2"></div>
    <div class="m-loading-line m-line-3"></div>
    <div class="m-loading-line m-line-4"></div>
    <div class="m-loading-line m-line-5"></div>
    <div class="m-loading-line m-line-6"></div>
    <div class="m-loading-line m-line-7"></div>
    <div class="m-loading-line m-line-8"></div>
    <div class="m-loading-line m-line-9"></div>
    <div class="m-loading-line m-line-10"></div>
    <div class="m-loading-line m-line-11"></div>
    <div class="m-loading-line m-line-12"></div>
  </div>
</div>

loading.scss:

.loading {
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 100000;
}

.m-loading {
  width: 40px;
  height: 40px;
  position: relative;
  transform: translate(-50%, -50%);
  left: 50%;
  top: 50%;

  .m-loading-line {
    width: 100%;
    height: 100%;
    position: absolute;
    left: 0;
    top: 0;

    &:before {
      content: '';
      display: block;
      margin: 0 auto;
      width: 5%;
      height: 25%;
      background-color: #333;
    }
  }

  @for $i from 1 through 12 {
    .m-line-#{$i} {
      transform: rotate(30deg * $i);
      animation: m-loading-fade 1.2s (-0.1s * (12 - $i)) infinite ease-in-out both;
    }
  }
}

@keyframes m-loading-fade {
  0%, 39%, 100% { opacity: .3; }
  40% { opacity: 1; }
}

接下来,在入口 js 文件或者根组件的样式中引用该样式文件——样式部分最终会提取、合并打包成一个单独的样式文件,并插入到页面 head 里先行加载,再把 html 的部分放在文档的 body 中。 然后,入口 js 里通过选择器找到 loading 节点,并隐藏或者删除,或者写一些控制它隐藏/展示的方法以达到复用的目的, 例如:

loading.js:

var el = document.querySelector('#loading')
var isLoading = true

//  if is loading, prevent any scroll events
function onScroll (e) {
  if (isLoading) {
    e.preventDefault()
  }
}

window.addEventListener('touchmove', onScroll)
window.addEventListener('scroll', onScroll)

export default {
  show () {
    isLoading = true
    el.classList.remove('hide')
  },
  hide () {
    isLoading = false
    el.classList.add('hide')
  }
}

异步数据加载完成前的时间

当入口 js 开始执行后,往往需要通过接口去取页面所需要的数据,最后再根据这些数据渲染视图结构。这段等待时间如果不做处理,默认的视图结构可能也会影响体验。 我们可以根据视图数据是否加载完成来决定是否显示视图元素。这样会稍好一些,缺点就是页面内容区域空置的时间会稍长。

有一种更好的处理方式——专门设置一种排好的内容占位符(content placeholder),这种占位符由一些方形色块填充起来,与拿到数据之后的实际渲染相比有一定的相似,切换起来体验也会自然很多。国外大概是 facebook 带领起来的潮流,国内的网站上,知乎、微博都有用到过类似的效果。

参考文章 facebook-content-placeholder-deconstruction。文中的 css 可能有点杂乱,因为它的目的是让那些占位区块的背景色具有进度条一般的动画效果,不得不用镂空的办法实现。明白了其策略之后,我们也可以自己实现一个类似的、稍微灵活一些的占位显示。以下是我用 vue(1.0) 制作的一个占位区域组件:

```html name=placeholder.vue

```

这个组件里通过 layout 定义每一行占位区块的位置大小,因此可以依据规则自己排布一些合适的结构。另外还有一些不足,如果一行内要出现网格型的布局,按照目前的配置规则就无法满足了。这个留待以后真正需要了再去解决吧!

总结

以上两种策略实施起来其实都不是那么方便,要在框架之外加 dom 元素导致不便整理、复用,或者是得写一些判断语句和结构参数。但总的来说,加上了体验会更好一些。还有一个问题——如果加载入口js比js去异步加载数据慢,那么理论上说应该把占位效果放在框架之外、入口js之前去显示,以达到最佳的显示效果。