通过显示框架 UI 来改善 React 应用中 UX

2025-05-24

通过显示框架 UI 来改善 React 应用中 UX

作者:Paramananham Harrison✏️

介绍

骨架屏是一种不包含实际内容的 UI;相反,它以类似于实际内容的形状显示页面的加载元素。

骨架屏幕向用户显示内容正在加载,并提供内容完全加载后的外观的模糊预览。

前端开发人员使用骨架 UI 的原因多种多样。

其中最主要的是 UI 能够在视觉上简化用户体验、模拟内容加载速度以及逐步加载内容,而无需一次获取页面上的所有内容。

Slack、Youtube、Facebook、Pinterest 和其他大型科技公司在加载内容时显示骨架屏幕以提升用户体验。

中等内容加载
中等内容加载

中等骨架 UI
中等骨架 UI

除了骨架屏幕之外,这些用户界面通常被称为内容占位符、内容加载器和幽灵元素。

骨架屏幕如何改善用户体验

Skeleton UI 与真实 UI 非常相似,因此用户在内容显示之前就能了解网站的加载速度。让我们通过两个屏幕的对比来看一下实际效果:

空的 Facebook 页面,无加载程序
空的 Facebook 页面,无加载程序

空的 Facebook,带有框架 UI
空的 Facebook,带有框架 UI

两个屏幕都没有加载实际内容,但空白页面对用户来说似乎比较慢,而骨架屏幕看起来更丰富,速度更快,响应也更灵敏。

尽管两个屏幕上的实际内容加载速度相同,但骨架屏幕提供了更出色的用户体验。

不同的骨架UI

骨架 UI 有几种不同的类型。主要包括内容占位符和图像(或颜色)占位符。

Medium、Slack 和 Youtube 等公司在其主页的骨架 UI 中使用内容占位符。

它们很容易构建,因为它们不需要任何有关实际内容数据的详细信息,而只是模仿 UI。

与此同时,Pinterest 和 Unsplash 这两个以图片为主的网站则使用了颜色占位符。颜色占位符的构建难度更大,因为它们需要实际内容数据的详细信息。

工作原理

首先,加载骨架而不是图像(通常带有灰色或灰白色背景)。

一旦获取数据,就从图像元数据中加载图像的实际颜色。

该元数据是通过后端算法从图像上传时获取的,并在图像之上进行处理。

最后,延迟加载图像以允许用户使用交叉观察器 API 实际查看内容。

演示

在我们的教程中,我们将通过创建 YouTube 主页的模拟来探索 React 中的骨架 UI。

在开始之前,让我们列出 React 中已经提供的用于骨架 UI 开发的最流行的软件包:

这些软件包维护得相当好,但也存在一些缺点。在决定在我们的应用中使用哪个之前,我们先来分析一下它们的优缺点。

React 内容加载器

优点

  • 基于 SVG 的 API;您可以使用任何 SVG 形状来创建骨架元素
  • 轻松创建动画占位符,从左到右发光(脉冲动画)
  • 有一些预先设置的内容加载器(例如 Facebook、Instagram 等)
  • 由于 SVG 支持多种形状,因此可以用于任何复杂的骨架 UI

缺点

  • 您需要为所有组件分别创建自定义骨架组件
  • SVG 与 CSS 元素不同,因此创建具有自定义对齐方式的自定义元素需要陡峭的学习曲线
  • 由于 SVG 依赖性,浏览器支持可能不一致,因此骨架在不同的浏览器上的外观和感觉可能会有所不同

以下是使用骨架组件的示例react-content-loader

import ContentLoader from "react-content-loader";

    // API support all SVG shapes - rect is a SVG shape for rectangle
    const SkeletonComponent = () => (
      <ContentLoader>
        <rect x="0" y="0" rx="5" ry="5" width="70" height="70" />
        <rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
        <rect x="80" y="40" rx="3" ry="3" width="250" height="10" />
      </ContentLoader>
    )
Enter fullscreen mode Exit fullscreen mode

反应占位符

优点

  • 基于组件的 API
  • 使用占位符组件轻松创建自定义骨架 UI
  • 支持脉冲动画,可以通过道具控制

缺点

  • 与 React 内容加载器类似,我们需要单独维护一个骨架组件,因此更新组件的样式也可能需要更新骨架组件
  • 学习曲线不是很线性,因为有多个组件可以满足不同的需求

以下是使用 的骨架组件的示例react-placeholder

import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
import ReactPlaceholder from 'react-placeholder';

// 
const MyCustomPlaceholder = () => (
  <div className='my-custom-placeholder'>
    <RectShape color='gray' style={{width: 30, height: 80}} />
    <TextBlock rows={7} color='yellow'/>
  </div>
);

// This is how the skeleton component is used
<ReactPlaceholder ready={ready} customPlaceholder={<MyCustomPlaceholder />}>
  <MyComponent />
</ReactPlaceholder>
Enter fullscreen mode Exit fullscreen mode

React 加载骨架

优点

  • 非常简单的 API — 它只有一个包含所有自定义属性的组件
  • 相当容易学习
  • 可以作为单独的骨架组件使用,也可以直接在任何组件内部使用,因此可以灵活地按照我们想要的方式使用
  • 支持动画和主题

缺点

  • 对于简单的骨架 UI 来说非常好,但对于复杂的骨架来说很难

以下是 React 加载骨架的示例:

import Skeleton, { SkeletonTheme } from "react-loading-skeleton";

const SkeletonCompoent = () => (
  <SkeletonTheme color="#202020" highlightColor="#444">
    <section>
      <Skeleton count={3} />
      <Skeleton width={100} />
      <Skeleton circle={true} height={50} width={50} />
    </section>
  </SkeletonTheme>
);
Enter fullscreen mode Exit fullscreen mode

对于完整的演示,我们将使用react-loading-skeleton

也就是说,这三个库足以满足简单的用例。您可以随意浏览文档,并选择最适合您应用程序使用的那个。

使用 React 的 Skeleton UI 示例

我们将构建一个类似 YouTube 的 UI,并展示骨架 UI 的工作原理。

首先,让我们创建 YouTube UI:

import React from "react";
    // Youtube fake data
    import youtubeData from "./data";
    // Styles for the layout
    import "./App.css";

    // Each Card item component which display one video - shows thumbnail, title and other details of a video
    const Card = ({ item, channel }) => {
      return (
        <li className="card">
          <a
            href={`https://www.youtube.com/watch?v=${item.id}`}
            target="_blank"
            rel="noopener noreferrer"
            className="card-link"
          >
            <img src={item.image} alt={item.title} className="card-image" />
            <h4 className="card-title">{item.title}</h4>
            <p className="card-channel">
              <i>{channel}</i>
            </p>
            <div className="card-metrics">
              {item.views} &bull; {item.published}
            </div>
          </a>
        </li>
      );
    };

    // Card list component
    const CardList = ({ list }) => {
      return (
        <ul className="list">
          {list.items.map((item, index) => {
            return <Card key={index} item={item} channel={list.channel} />;
          })}
        </ul>
      );
    };

    // App component - each section have multiple videos
    const App = () => {
      return (
        <div className="App">
          {youtubeData.map((list, index) => {
            return (
              <section key={index}>
                <h2 className="section-title">{list.section}</h2>
                <CardList list={list} />
                <hr />
              </section>
            );
          })}
        </div>
      );
    }

    export default App;
Enter fullscreen mode Exit fullscreen mode

接下来,让我们输入虚假的 YouTube 数据:

const youtubeData = [
  {
    section: "JavaScript Tutorials by freeCodeCamp",
    channel: "freeCodeCamp.org",
    items: [
      {
        id: "PkZNo7MFNFg",
        image: "https://img.youtube.com/vi/PkZNo7MFNFg/maxresdefault.jpg",
        title: "Learn JavaScript - Full Course for Beginners",
        views: "1.9M views",
        published: "9 months ago"
      },
      {
        id: "jaVNP3nIAv0",
        image: "https://img.youtube.com/vi/jaVNP3nIAv0/maxresdefault.jpg",
        title: "JavaScript, HTML, CSS - Rock Paper Scissors Game",
        views: "216K views",
        published: "1 year ago"
      }
    ]
  },
  {
    section: "Small steps on React",
    channel: "Learn with Param",
    items: [
      {
        id: "ylbVzIBhDIM",
        image: "https://img.youtube.com/vi/ylbVzIBhDIM/maxresdefault.jpg",
        title: "useState example by building a text-size changer",
        views: "148 views",
        published: "3 days ago"
      }
    ]
  }
];
export default youtubeData
Enter fullscreen mode Exit fullscreen mode

在加载实际数据之前,让我们先展示一下框架 UI。由于我们的数据是伪造的,我们需要像 API 数据一样模拟它,在两秒超时后加载:

import React, { useState, useEffect } from "react";

const App = () => {
  const [videos, setVideos] = useState([]);
  // Load this effect on mount
  useEffect(() => {
    const timer = setTimeout(() => {
      setVideos(youtubeData);
    }, 2000);
    // Cancel the timer while unmounting
    return () => clearTimeout(timer);
  }, []);

  return (
    <div className="App">
      {videos.map((list, index) => {
        ...
      })}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

您会看到白屏三秒钟,然后数据突然加载。

现在,我们将安装react-loading-skeleton

yarn add react-loading-skeleton
Enter fullscreen mode Exit fullscreen mode

让我们为视频数据创建一个骨架组件:

import Skeleton from "react-loading-skeleton";

/* 
   Separate Skeleton component 
  - It is created with the same shape as Card component
  - Pros: Component will be isolated from the skeletons so the component won't become complex or heavy
  - Cons: Maintaining separate skeleton component will make it harder to maintain when UI changes and style gets changed
*/
const CardSkeleton = () => {
  return (
    <section>
      <h2 className="section-title">
        <Skeleton height={28} width={300} />
      </h2>
      <ul className="list">
        {Array(9)
          .fill()
          .map((item, index) => (
            <li className="card" key={index}>
              <Skeleton height={180} />
              <h4 className="card-title">
                <Skeleton height={36} width={`80%`} />
              </h4>
              <p className="card-channel">
                <Skeleton width={`60%`} />
              </p>
              <div className="card-metrics">
                <Skeleton width={`90%`} />
              </div>
            </li>
          ))}
      </ul>
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

您还可以通过将骨架直接嵌入到组件中来创建骨架组件,如下所示:

import Skeleton from "react-loading-skeleton";

/*
  Cards component with embedded skeleton UI
  - Pros: This is much easier to maintain for UI and styles changes
  - Cons: UI will become complex and heavy with lot of unnecessary elements in it
*/
const Card = ({ item, channel }) => {
  return (
    <li className="card">
      <a
        href={item.id ? `https://www.youtube.com/watch?v=${item.id}` : `javascript:void(0)`}
        target="_blank"
        rel="noopener noreferrer"
        className="card-link"
      >
        {
          item.image ? 
          <img src={item.image} alt={item.title} className="card-image" /> 
          : 
          <Skeleton height={180} /> 
        }
        <h4 className="card-title">
          {
            item.title ? item.title : 
            <Skeleton height={36} width={`80%`} />
          }
        </h4>
        <p className="card-channel">
          { channel ? <i>{channel}</i> : <Skeleton width={`60%`} /> }
        </p>
        <div className="card-metrics">
          {
            item.id ? 
            <>{item.views} &bull; {item.published}</>
            :
            <Skeleton width={`90%`} />
        </div>
      </a>
    </li>
  );
};
Enter fullscreen mode Exit fullscreen mode

我在示例中使用了独立的骨架组件,但您可以随意使用最适合您需求的样式组件。这完全取决于个人喜好和组件的复杂程度。

最后,这是CardSkeleton实际数据加载之前的组件:

const App = () => {
  const [videos, setVideos] = useState([]);
  // Manage loading state - default value false
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // set the loading state to true for 2 seconds
    setLoading(true);

    const timer = setTimeout(() => {
      setVideos(youtubeData);
      // loading state to false once videos state is set
      setLoading(false);
    }, 2000);

    return () => clearTimeout(timer);
  }, []);

  // Show the CardSkeleton when loading state is true
  return (
    <div className="App">
      {loading && <CardSkeleton />}
      {!loading &&
        videos.map((list, index) => {
          return (
            <section key={index}>
              <h2 className="section-title">{list.section}</h2>
              <CardList list={list} />
              <hr />
            </section>
          );
        })}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

现在,我们已经有了一个功能齐全的骨架 UI 示例。我们的示例会先加载骨架 2 秒,然后再显示数据。点击此处查看实际效果。

此示例的代码库可在Github中找到。我已编写了分支,以便您可以运行所有中间阶段并查看差异。

结论

骨架屏幕可显著改善用户体验,减轻用户因完全空白屏幕而产生的挫败感,并让用户了解内容在加载之前的样子。

在 React 应用程序中使用骨架 UI 很容易。

如果您不想使用现有的包,您也可以通过创建矩形和圆形元素来模拟骨架的 div 元素,从而轻松创建自己的骨架 UI。

在评论部分分享您使用骨架 UI 的经验。


编者注:觉得这篇文章有什么问题?您可以在这里找到正确版本

插件:LogRocket,一个用于 Web 应用的 DVR

 
LogRocket 仪表板免费试用横幅
 
LogRocket是一款前端日志工具,可让您重播问题,就像它们发生在您自己的浏览器中一样。无需猜测错误发生的原因,也无需要求用户提供屏幕截图和日志转储,LogRocket 让您重播会话,快速了解问题所在。它可与任何应用程序完美兼容,不受框架限制,并且提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的额外上下文。
 
除了记录 Redux 操作和状态外,LogRocket 还记录控制台日志、JavaScript 错误、堆栈跟踪、带有标头 + 正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,以记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能重现像素完美的视频。
 
免费试用


通过显示骨架 UI 来改善 React 应用程序中的 UX一文首先出现在LogRocket 博客上。

文章来源:https://dev.to/bnevilleoneill/improve-ux-in-react-apps-by-showing-sculpture-ui-5a3i
PREV
学习这些键盘快捷键,成为 VS Code 忍者
NEXT
如何使用 Node.js 和 Elastic 编写自己的搜索引擎