Skeleton Design 这个概念,想必前端开发人员比较熟悉,而普通的 App 用户,可能甚至都没有听说过,但实际上我们每个人,每天几乎都会受益于这个设计理念。

Skeleton Design 在一些国内技术社区的技术博客里,通常被称为骨架屏设计,是一种用户界面设计策略。骨架屏设计不是一种具体的基于某种编程语言的技术,而是一种设计理念,旨在改善用户体验。通用的骨架屏设计,在页面内容完全加载前,会显示一个包含页面主要布局和元素位置的空白版本。这种方式能够让用户在等待页面加载的过程中,有一个直观的感知,知道接下来会出现什么内容,减少用户的等待焦虑感。

我目前工作在一个使用 Angular 开发的电商 Storefront 开源项目上,项目的 Github 地址如下,项目代号为 Spartacus.

Spartacus B2B 功能模块里,正常的 Cost Centers 列表显示如下:

在这些 Cost Center 的数据从后台取回来之前,页面显示是这样的:

大家注意到上图一行行灰色的横条吗?这就是一种典型的 Skeleton Design 理念在 Angular 应用里的实现。这种灰色的横条显示,主要目标就是提供一个视觉提示,让用户知道他们可以期待什么类型的信息。

如上图所示,骨架屏通常使用灰色调的占位符来表示正在加载的元素,这种设计方式提供了一种平滑的用户体验,使用户觉得网页的加载更快,更流畅。

试想一下,在用户网络接入速度比较慢,或者后端 API 响应速度比较慢的情况下,如果没有 Skeleton Design 这种设计,那么用户需要在页面加载完成后才能看到完整的内容。特别是在笔者负责开发的 Spartacus 这种电商应用领域内,超过3秒的页面等待就很可能会导致用户的不耐烦,甚至导致他们关闭网站。通过使用骨架屏设计,开发者可以立即显示一个页面的概要,把用户在等待时的注意力分散开,从而提高用户的参与度和用户体验。

本文余下部分,介绍 Spartacus 骨架屏设计的详细技术实现。

从 Spartacus 源代码的 list.service.ts 的实现源代码能看出,幽灵数据就是一个 length 属性值为10的空数组。

/**

* The ghost data contains an empty list of objects that is used in the UI

* to render the HTML elements.

*

* This list contains 10 items, so that the ghost will show 10 rows by default.

*/

protected ghostData = { values: new Array(10) } as EntitiesModel;

在 Chrome 开发者工具里,能观察到这些幽灵数据具有对应的 CSS class,这使得它们具有灰色矩形的视觉外观:

Cost Center 表格显示的数据最终通过 list.service.ts 从 SAP Commerce Cloud 后台取出,取数逻辑通过 Angular 响应式编程库 RxJS的 pipe 方法驱动:第 101 行 switchMap 操作符里的箭头函数,输入参数 pagination 包含了去 Commerce Cloud 取数据使用的分页设置,函数体 this.load 发送 HTTP 请求,消费 Commerce Cloud 的 OCC API.

而第 102 行的 startWith 操作符,语义上相当于给 pipe 驱动的 Observable 流赋上一个初始值,该初始值即为 length 属性为10的空数组。

/**

* Loads the data by delegating to the `load` method, which must be implemented

* in specific implementations of this abstract class.

*

* The load method is streamed from the `pagination$` stream, which is initialized

* with default pagination and structure drive properties.

*/

getData(...args: any): Observable | undefined> {

return this.pagination$.pipe(

// we merge any configured pagination from the table structure

switchMap((pagination) =>

this.getStructure().pipe(

map((config) => ({ ...pagination, ...config.options?.pagination }))

)

),

switchMap((pagination) => this.load(pagination, ...args)),

startWith(this.ghostData)

);

}

startWith 是一个非常有用的 RxJS 操作符,它的主要作用是在 Observable 序列开始之前插入一个指定的元素。这意味着,当你订阅一个 Observable 时,startWith 操作符会立即发出它的参数,然后继续发出 Observable 的值。

例如,我们有一个 Observable,它将发出一个数字序列:

const numbers$ = of(1, 2, 3, 4, 5);

我们可以使用 startWith 操作符在这个数字序列开始前插入一个数字 0:

const numbersWithZero$ = numbers$.pipe(startWith(0));

现在,当我们订阅 numbersWithZero$ 时,它会首先发出 0,然后发出 1, 2, 3, 4, 5。

这种功能在很多情况下都非常有用。例如,在 Angular 中,我们可能会用到 HttpClient 服务来从服务器获取数据。这个过程是异步的,所以我们会使用 RxJS 来处理它。但在数据到达之前,我们可能想要显示一些默认的数据或者加载指示器。这时,startWith 就能派上用场。

// 从服务器获取用户数据,但在数据到达之前,显示一个空的用户对象

const user$ = this.http.get('api/users/1').pipe(

startWith({id: null, name: 'Loading...'})

);

在这个例子中,user$ 是一个 Observable,它将发出从服务器获取的用户数据。但在数据到达之前,它会先发出一个包含 id: null 和 name: 'Loading...' 的对象。这样,我们就可以立即更新界面,显示一个加载指示器,然后当真实的数据到达时,再更新为真实的数据。

回到本文的例子,从运行时序来说,任何消费 getData 函数返回的 Observable 对象的 Angular UI 组件,都会先显示 startWith 设置的初始值,即幽灵数据。待从 Commerce Cloud 后台加载的真实数据返回给浏览器之后,组件自动刷新并显示这些真实的业务数据。

总结

本文首先介绍了 Skeleton Design 设计理念的引入初衷和旨在解决的用户体验,接着以 Spartacus Storefront B2B 页面为例,介绍了 Angular 应用里采用 Skeleton Design 改善页面加载用户体验的例子,希望对 Angular 同行有参考作用。

精彩文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: