骨架图,so EASY 😝

菊花和白屏并不能给用户美好的体验。因此在部分页面,我们尝试了一些骨架图。骨架图的初衷就是减少用户等待的感觉,loading 到 loaded 的过程中也可以探索页面结构。
骨架图大概有这两种类型:

  1. 纯样式的骨架图
  2. 空数据骨架图

纯样式的骨架图

纯样式,即没有数据联动的骨架图。
通常,这样的骨架图只占单屏,且一般不能滑动。页面上有大小各异的圆形矩形的占位,代表即将填充的内容。这些几何图形通常由是一些手动编写的元素、或者更简单的,一张图片组成。当然,最终的效果也不一定是静态的,有时也可以展示一些动画。
不过,这种纯样式的骨架图意味着设计师和前端工程师的大量工作,和一些冗余代码。一旦有新的页面结构出现,很可能就要重新实现。

空数据骨架图

这种类型的骨架图跟页面各个模块是挂钩的,数据填充前后的排版应该尽量一致(否则带给用户的就不是心理预期的建设,而是一个乱入的神展开了)。
比如我们的首页,有各种不同的推书模块,而且各个模块的先后顺序非常固定,构成这些模块的组件是“书”,以及更小的“书封”组件。
所以我们要做的几乎只有两件事:

  • 构造“书”这个组件的骨架(请设计同学帮忙设计)
  • 构造“骨架数据”

这样一来,当页面使用了具有骨架功能的组件,且当前数据尚未到位,页面上的相应位置就会显示这个组件的骨架。

上面说到“几乎”只要做两件事,但实际除了这两件事之外,我们还要考虑这些实现细节:

骨架组件的状态管理

我们需要精密地控制各种状态的边界:什么时候显示骨架,什么时候显示真正的数据,数据加载失败的状态,空数据如何处理

最简单的处理方式就是有数据时渲染数据,没有数据时显示骨架。空数据时则可以显示固定ui,或选择不进行渲染。

骨架数据写在哪一层

  • 全局 store 中的默认数据
  • 组件 state 的默认值
  • render 时的兜底项

这一选择取决于你的需求。
写在 render 中是最简单的,不需要有太多的判断,只要像一个单纯的小孩,饭来张口即可。一般我会在写基础组件时使用它。
写在 state 则相对复杂一些,像一个谨慎的社会人,有新数据到来的时候,会先进行一番判断(比如数据从无到有时),再决定是否要更新。一般我会在页面组件中使用它。
写在 store 中的效果跟 state 差不多,看你喜欢分而治之还是中心霸权了。
总的来说,就是选择在渲染时,还是在更新数据前。

这里提供一个完整的做法,仅供参考:

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
64
65
// 组件
const BookItem = ({ data }) => {
if (!(data)) {
return (
// 骨架图
);
}
return (
// 真正的数据填充样式
);
};


// 页面
const PLACE_HOLDER_DATA = {
data1: Array(9).fill({}),
data2: Array(5).fill({}),
}

class Home extends Component {
constructor(props) {
super(props);
this.state = {
data: _.isEmpty(this.props.data) ? PLACE_HOLDER_DATA : this.props.data
};
}

componentWillReceiveProps(nextProps) {
// 只在数据成功加载时更新
if (nextProps.fetchSuccess && !_.isEmpty(nextProps.data)) {
this.setState({
data: nextProps.data
});
}
}

renderBooks = (title, books) => {
return (
<div>
<span>{title}</span>
{
books.map((item, index) => (
<BookItem data={item} key={item.bookId || index} />
))
}
</div>
)
};

render() {
const {
data: {
data1,
data2,
}
} = this.state;
// 加载成功后如果部分模块无数据,则不显示
return (
<Fragment>
{!_.isEmpty(data1) && this.renderBooks('book section 1', data1)}
{!_.isEmpty(data2) && this.renderBooks('book section 2', data2)}
</Fragment>
);
}
}

上述的基础ui组件 BookItem 我们单独为骨架写了一个 html 结构,但大多数情况下,由于我们的骨架结构与实际渲染内容一致,所以还可以通过使用不同的 classname 或者 :empty伪元素 区分同一结构下的显示。

不需要骨架图的情况

正如我们一开始所说,骨架图是为了减少用户等待的感觉,提前让用户探索页面结构。基于这个说法,下面是我认为不需要骨架图的情况:

  1. 一个简单的列表,且用户已经有了一些心理预期。用户无法通过此处的骨架图有任何收获,反而让页面变得拥挤。
  2. 页面的数据结构未知(这应该是一句废话吧),当然,对于一定的抖动,可以衡量一下。

骨架图虽然在一定程度上能提高用户体验,但是并非万能。做得再完善的骨架图也不及一个ssr。毕竟,天下武功,唯快不破 :P。