构建灵活且可复用骨架加载器的极简方法

Avatar of Adrian Bece
Adrian Bece 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

像加载动画和骨架加载器这样的 UI 组件可以减少页面加载时的挫败感,并且如果使用得当,甚至可能会影响用户对加载时间的感知。它们无法完全阻止用户放弃网站,但可以鼓励他们等待更长时间。动画加载动画在大多数情况下使用,因为它们易于实现,并且通常能够胜任工作。骨架加载器的使用场景有限,并且实现和维护可能比较复杂,但它们在这些特定的使用场景中提供了更好的加载体验。

我注意到开发人员要么不确定何时使用骨架加载器来增强 UX,要么不知道如何着手实现。网络上更常见的骨架加载器示例并非都那么可复用或可扩展。它们通常是为单个组件量身定制的,无法应用于其他任何组件。这是开发人员改用常规加载动画并避免代码中潜在开销的原因之一。当然,一定有办法以更简单、更可复用和更可扩展的方式来实现骨架加载器。

加载动画元素和骨架加载器

加载动画(或进度条)元素是最简单且可能是最常用的元素,用于指示加载状态。加载动画可能比空白页面看起来更好,但它不会长时间吸引用户的注意力。加载动画告诉用户某些内容最终会加载。用户必须被动等待内容加载,这意味着他们无法与页面上的其他元素交互或使用页面上的任何其他内容。加载动画占据了整个屏幕空间,用户无法访问任何内容。

加载动画元素显示并覆盖整个屏幕,直到所有内容加载完毕。

但是,骨架加载器(或骨架屏幕)告诉用户内容即将加载,并且它们可能比简单的加载动画提供更好的加载 UX。空盒子(带有纯色或渐变背景)用作正在加载的内容的占位符。在大多数情况下,内容会逐渐加载,这使用户能够保持进度感,并认为页面加载速度比实际更快。用户处于主动等待状态,这意味着他们可以在其余内容加载时与页面交互或使用至少一部分内容。

空盒子(带有纯色或渐变背景)用作占位符,同时内容正在逐渐加载。文本内容首先加载并显示,然后加载和显示图像。

需要注意的是,加载组件不应用于解决性能问题。如果网站由于可以解决的问题(未优化的资产或代码、后端性能问题等)而遇到性能问题,则应首先修复这些问题。加载元素不会阻止用户放弃性能低下且加载时间过长的网站。加载元素应作为最后手段使用,在等待不可避免并且加载延迟不是由未解决的性能问题引起时使用。

正确使用骨架加载器

骨架加载器不应被视为全屏加载元素的替代品,而应在满足内容和布局的特定条件时使用。让我们一步一步地了解如何有效地使用加载 UI 组件,以及何时使用骨架加载器而不是常规加载动画。

加载延迟是否可以避免?

从 UX 角度来看,处理加载的最佳方法是完全避免它。我们需要确保加载延迟是不可避免的,并且不是前面提到的可以修复的性能问题导致的。首要任务始终应该是改进性能并减少获取和显示内容所需的时间。

加载是否由用户发起,以及是否需要反馈?

在某些情况下,用户操作可能会启动其他内容的加载。一些示例包括在用户滚动时延迟加载用户视口中的内容(例如图像)、单击按钮加载内容等。我们需要为用户需要获取其操作已启动加载过程的某种反馈的情况包含加载元素

如以下模型所示,如果没有加载元素提供反馈,用户不知道他们的操作已启动任何正在后台发生的加载过程。

当单击按钮时,我们异步地在模态窗口中加载内容。在第一个示例中,没有显示加载元素,用户可能会认为他们的点击未被注册。在第二个示例中,用户会收到反馈,表明他们的点击已注册,并且内容正在加载。

布局是否一致且可预测?

如果我们决定使用加载器元素,现在需要选择最适合我们使用场景的加载器元素类型。当我们可以预测正在加载的内容的类型和布局时,骨架加载器最有效。如果骨架加载器的布局在某种程度上不能准确地表示加载内容的布局,则突然的变化可能会导致布局偏移,并使用户感到困惑和迷失方向。对于具有可预测内容的一致布局的元素,请使用骨架加载器。

左侧的网格布局(取自 discogs.com)代表了骨架加载器的理想使用场景,而右侧的评论示例(取自 CSS-Tricks)则是加载动画的理想使用场景。

页面上是否有内容可供用户立即使用?

当骨架加载器处于活动状态并且其他内容正在积极加载时,页面上已经存在部分或页面元素时,骨架加载器最有效。逐渐加载内容意味着静态内容在页面加载时可用,异步加载的内容在可用时显示(例如,首先加载第一个文本,然后加载图像)。这种方法确保用户保持进度感,并期望内容在任何时候都完成加载。在没有内容的情况下,并且没有逐渐加载内容的情况下,整个屏幕都被骨架加载器覆盖,这与整个屏幕被全屏加载动画或进度条覆盖相比,并没有显著的优势。

左侧的模型显示骨架加载器覆盖所有元素,直到所有内容加载完毕。右侧的模型显示骨架加载器仅覆盖正在异步加载的内容。页面是可用的,因为它们显示了网站内容的一部分,并且用户保持了进度感。

创建健壮的骨架加载器

现在我们知道何时使用骨架加载器以及如何正确使用它们,我们终于可以开始编写代码了!但首先,让我告诉您我们将如何处理这个问题。

在我看来,网络上大多数骨架加载示例都过度设计且维护成本高。您可能见过其中一些示例,其中骨架屏幕被创建为具有单独 CSS 样式的单独 UI 组件,或者使用精心设计的 CSS 渐变来模拟最终布局。对于每个 UI 组件创建和维护单独的骨架加载器或骨架样式,在开发中可能会产生严重的开销,因为这种方法过于特定。当我们考虑可扩展性时,这一点尤其如此,因为对现有布局的任何更改都涉及更新骨架布局或样式。

让我们尝试找到一种构建骨架加载的极简方法,这种方法应该适用于大多数使用场景,并且易于实现、复用和维护

卡片网格组件

我们将使用常规的 HTML、CSS 和 JavaScript 进行实现,但总体方法可以适用于大多数技术栈和框架。

我们将创建一个包含六个卡片元素(每行三个)的简单网格作为示例,并通过单击按钮模拟异步内容加载。

我们将对每个卡片使用以下标记。请注意,我们正在为图像设置宽度和高度,并使用 1px 透明图像作为占位符。这将确保在加载图像之前,图像骨架加载器可见。

<div class="card">
  <img width="200" height="200" class="card-image" src="..." />
  <h3 class="card-title"></h3>
  <p class="card-description"></p>
  <button class="card-button">Card button</button>
</div>

这是我们的卡片网格示例,其中应用了一些布局和展示样式。内容节点根据加载状态使用 JavaScript 从 DOM 中添加或移除,以模拟异步加载。

骨架加载器样式

开发人员通常通过创建替换骨架组件(使用专用的骨架 CSS 类)或使用 CSS 渐变重新创建整个布局来实现骨架加载器。这些方法缺乏灵活性,并且根本不可重用,因为各个骨架加载器是为每个布局量身定制的。考虑到布局样式(间距、网格、内联、块和弹性元素等)已经存在于主组件(卡片)样式中,**骨架加载器只需要替换内容,而不需要替换整个组件**!

考虑到这一点,让我们创建仅在设置父类时才激活的骨架加载器样式,并使用仅影响*展示*和*内容*的 CSS 属性。请注意,这些样式独立于它们应用到的元素的布局和内容,这应该使它们具有很高的可重用性。

.loading .loading-item {
  background: #949494 !important; /* Customizable skeleton loader color */
  color: rgba(0, 0, 0, 0) !important;
  border-color: rgba(0, 0, 0, 0) !important;
  user-select: none;
  cursor: wait;
}

.loading .loading-item * {
  visibility: hidden !important;
}

.loading .loading-item:empty::after,
.loading .loading-item *:empty::after {
  content: "\00a0";
}

基本父类 .loading 用于激活骨架加载样式。.loading-item 类用于覆盖元素的展示样式以显示骨架元素。这也确保了元素的布局和尺寸被骨架保留和继承。此外,.loading-item 确保所有子元素都隐藏,并且在其内部至少有一个空字符(\00a0),以便显示元素并呈现其布局。

让我们将骨架加载器 CSS 类添加到我们的标记中。请注意,没有添加额外的 HTML 元素,我们只是应用了额外的 CSS 类。

<div class="card loading">
  <img width="200" height="200" class="card-image loading-item" src="..." />
  <h3 class="card-title loading-item"></h3>
  <p class="card-description loading-item"></p>
  <button class="card-button loading-item">Card button</button>
</div>

内容加载完成后,我们只需要从父组件中移除 loading CSS 类即可隐藏骨架加载器样式。

根据您的自定义 CSS,这几行代码应该适用于大多数(如果不是全部)用例,因为这些骨架加载器*从主(内容)样式继承布局*,并通过填充布局中留出的空隙来创建一个替换内容的实心框。我们还将这些类应用于非空元素(带有文本的按钮),并将其替换为骨架。按钮可能从一开始就具有文本内容,但可能缺少使其正常运行所需的额外数据,因此我们也应该在加载这些数据时隐藏它。

这种方法也可以适应布局和标记的大多数更改。例如,如果我们要删除卡片的描述部分或决定将标题移到图像上方,我们不需要对骨架样式进行任何更改,因为骨架会响应标记中的所有更改。

可以通过简单地使用 .loading .target-element 选择器将其他骨架加载覆盖样式应用于特定元素。

.loading .button,
.loading .link {
  pointer-events: none;
}

多行内容和布局偏移

如您所见,前面的示例在卡片和我们使用的网格布局中效果很好,但请注意,页面内容在加载时会略微跳动。这称为布局偏移。我们的 .card-description 组件具有固定高度,包含三行文本,但骨架占位符仅跨越一行文本。当加载额外内容时,容器尺寸会发生变化,从而导致整体布局发生偏移。在这种特定情况下,布局偏移不是问题,但在更严重的情况下可能会让用户感到困惑和迷失方向。

这可以在占位符元素中直接轻松修复。占位符内容无论如何都将被加载的内容替换,因此我们可以在其中添加任何需要的内容。因此,让我们添加几个 <br /> 元素来模拟多行文本。

<div class="card loading">
  <img width="200" height="200" class="card-image loading-item" src="..." />
  <h3 class="card-title loading-item"></h3>
  <p class="card-description loading-item"><br/><br/><br/></p>
  <button class="card-button loading-item">Card button</button>
</div>

我们使用基本的 HTML 来塑造骨架并更改其内部的行数。网络上的其他示例可能使用 CSS 填充或其他方式来实现此目的,但这会增加代码的开销。毕竟,内容可以跨越任意数量的行,我们希望涵盖所有这些情况。

使用 <br /> 元素的额外好处是,它们继承了影响内容尺寸的 CSS 属性(例如行高、字体大小等)。类似地,&nbsp 字符可用于向内联占位符元素添加额外的间距。

通过几行 CSS,我们成功创建了用途广泛且可扩展的骨架加载器样式,可以应用于各种 UI 组件。我们还找到了一种简单的方法来垂直扩展骨架框,以模拟跨越多行文本的内容。

为了进一步展示此骨架加载器 CSS 代码片段的通用性,我创建了一个简单的示例,在其中我将该代码片段添加到使用 Bootstrap CSS 框架的页面中,而无需进行任何其他更改或覆盖。请注意,在此示例中,不会显示或模拟任何文本内容,但它将像前面的示例一样工作。这只是为了展示样式如何轻松地与其他 CSS 系统集成。

这是一个额外的示例,用于展示如何将这些样式应用于各种元素,包括 inputlabela 元素。

可访问性要求

我们还应该考虑可访问性 (a11y) 要求,并确保所有用户都能访问内容。没有 a11y 功能的骨架加载器可能会让有视觉障碍或使用屏幕阅读器浏览网页的用户感到困惑和迷失方向。

对比度

您可能已经注意到,我们示例中的骨架加载器具有高对比度,并且与野外常见的低对比度骨架加载器相比,它们看起来更加突出。某些用户在感知和使用低对比度 UI 组件时可能会遇到困难。这就是为什么 Web 内容可访问性指南 (WCAG) 指定非文本 UI 组件的最小对比度为 3:1

即将推出的“媒体查询级别 5”草案包含一个 prefers-contrast 媒体查询,它将使我们能够检测用户的对比度偏好。这将通过允许我们为请求高对比度版本的用户的骨架加载器分配高对比度背景颜色,并为其他用户分配微妙的低对比度背景颜色,从而为我们提供更大的灵活性。我建议默认情况下实现高对比度骨架加载器,直到 prefers-contrast 媒体查询得到更广泛的支持。

/* NOTE: as of the time of writing this article, this feature is not supported in browsers, so this code won't work */

.loading .loading-item {
/* Default skeleton loader styles */
}

@media (prefers-contrast: high) {
  .loading .loading-item {
    /* High-contrast skeleton loader styles */
  }
}

动画

根据动画骨架加载器的设计和实现,患有视觉障碍的用户可能会因动画而感到不知所措,并发现网站无法使用。最好防止动画对偏好减少运动的用户触发。此媒体查询在现代浏览器中得到了广泛支持,并且可以在没有任何警告的情况下使用。

.loading .loading-item {
  animation-name: skeleton;
  background: /* animated gradient background */;
}

@media (prefers-reduced-motion) {
  .loading .loading-item {
    animation: none !important;
    background: /* solid color */;
  }
}

屏幕阅读器

为了更好地支持屏幕阅读器,我们需要使用ARIA(可访问的富互联网应用程序)标记更新我们的 HTML。此标记不会影响我们的内容或展示,但它将允许使用屏幕阅读器的用户更好地理解和浏览我们的网站内容,包括我们的骨架加载器。

Adrian Roselli 对 可访问的骨架加载器 主题进行了非常详细的研究,适用于将骨架加载器作为独立 UI 组件实现的情况。在我们的示例中,我将使用 aria-hidden 属性 并结合 视觉隐藏文本 来提示屏幕阅读器内容正在加载。屏幕阅读器会忽略具有 aria-hidden="true" 的内容,但它们会使用 visually-hidden 元素向用户指示加载状态。

让我们使用 ARIA 标记和加载指示器元素更新我们的卡片。

<div class="card loading">
  <span aria-hidden="false" class="visually-hidden loading-text">Loading... Please wait.</span>
  <img width="200" height="200" class="card-image loading-item" aria-hidden="true" src="..." />
  <h3 class="card-title loading-item" aria-hidden="true"></h3>
  <p class="card-description loading-item" aria-hidden="true"><br/><br/><br/></p>
  <button class="card-button loading-item" aria-hidden="true">Card button</button>
</div>

我们也可以将 aria-hidden 应用于网格容器元素并在容器标记之前添加一个视觉隐藏元素,但我希望将标记示例集中在单个卡片元素上,而不是整个网格上,因此我使用了此版本。

当内容加载完成并在 DOM 中显示时,我们需要将内容容器的 aria-hidden 切换为 false,并将视觉隐藏的加载文本指示器的 aria-hidden 切换为 true

这是完成的示例

总结

实现骨架加载器需要与实现常规加载元素(如加载动画)略微不同的方法。我在网络上看到了很多实现骨架加载器的示例,这些示例严重限制了其可重用性。这些过度设计的解决方案通常涉及创建具有专用(范围狭窄)骨架 CSS 标记的单独骨架加载器 UI 组件,或使用 CSS 渐变和魔术数字重新创建布局。我们已经看到,只需要用骨架加载器替换内容,而不需要替换整个组件。

我们已经成功创建了简单、通用且可重用的骨架加载器,它们继承了默认样式的布局,并用实心框替换了空容器内部的内容。只需两个 CSS 类,这些骨架加载器就可以轻松地添加到几乎任何 HTML 元素中并进行扩展(如有需要)。我们还确保此解决方案可访问,并且不会因额外的 HTML 元素或重复的 UI 组件而导致标记膨胀。

感谢您抽出时间阅读本文。请告诉我您对这种方法的看法,并告诉我您在项目中如何实现骨架加载器。