待处理 UI
On this page

待处理 UI 和乐观 UI

¥Pending and Optimistic UI

优秀的 Web 用户体验和平庸的 Web 用户体验之间的区别在于,开发者能否通过在网络密集型操作期间提供视觉提示来实现网络感知的用户界面反馈。待处理的 UI 主要有三种类型:繁忙指标、乐观的 UI 和骨架回退。本文档提供了根据具体场景选择和实现适当反馈机制的指南。

¥The difference between a great user experience on the web and a mediocre one is how well the developer implements network-aware user interface feedback by providing visual cues during network-intensive actions. There are three main types of pending UI: busy indicators, optimistic UI, and skeleton fallbacks. This document provides guidelines for selecting and implementing the appropriate feedback mechanism based on specific scenarios.

待处理 UI 反馈机制

¥Pending UI Feedback Mechanisms

繁忙指示器:服务器处理 action 时,繁忙指示器会向用户显示视觉提示。当应用无法预测操作结果,并且必须等待服务器响应才能更新 UI 时,将使用此反馈机制。

¥Busy Indicators: Busy indicators display visual cues to users while the server is processing an action. This feedback mechanism is used when the application cannot predict the outcome of the action and must wait for the server's response before updating the UI.

乐观 UI:Optimistic UI 通过在收到服务器响应之前立即使用预期状态更新 UI 来提升感知速度和响应能力。当应用可以根据上下文和用户输入预测操作的结果,从而能够立即响应操作时,使用此方法。

¥Optimistic UI: Optimistic UI enhances perceived speed and responsiveness by immediately updating the UI with an expected state before the server's response is received. This approach is used when the application can predict the outcome of an action based on context and user input, allowing for an immediate response to actions.

框架回退:在 UI 初始加载时使用框架回退,为用户提供一个可视化的占位符,概述即将加载的内容结构。此反馈机制对于尽快渲染有用内容特别有用。

¥Skeleton Fallbacks: Skeleton fallbacks are used when the UI is initially loading, providing users with a visual placeholder that outlines the structure of the upcoming content. This feedback mechanism is particularly useful to render something useful as soon as possible.

反馈指导原则选择

¥Guiding Principles for Feedback Selection

使用乐观 UI:

¥Use Optimistic UI:

  • 下一状态可预测性:该应用可以根据用户的操作准确预测 UI 的下一个状态。

    ¥Next State Predictability: The application can accurately predict the next state of the UI based on the user's action.

  • 错误处理:我们已建立强大的错误处理机制,以解决此过程中可能出现的潜在错误。

    ¥Error Handling: Robust error handling mechanisms are in place to address potential errors that may occur during the process.

  • URL 稳定性:该操作不会导致 URL 发生变化,从而确保用户仍停留在同一页面。

    ¥URL Stability: The action does not result in a change of the URL, ensuring that the user remains within the same page.

使用繁忙指示器:

¥Use Busy Indicators:

  • 下一状态不确定性:该操作的结果无法可靠地预测,因此需要等待服务器的响应。

    ¥Next State Uncertainty: The outcome of the action cannot be reliably predicted, necessitating waiting for the server's response.

  • URL 更改:该操作会导致 URL 发生变化,指示导航到新页面或新版块。

    ¥URL Change: The action leads to a change in the URL, indicating navigation to a new page or section.

  • 错误边界:错误处理方法主要依赖于管理异常和意外行为的错误边界。

    ¥Error Boundaries: The error handling approach primarily relies on error boundaries that manage exceptions and unexpected behavior.

  • 副作用:该操作会触发涉及关键流程的副作用,例如发送电子邮件、处理付款等。

    ¥Side Effects: The action triggers side effects that involve critical processes, such as sending email, processing payments, etc.

使用框架回退:

¥Use Skeleton Fallbacks:

  • 初始加载:UI 正在加载,为用户提供即将加载的内容结构的视觉指示。

    ¥Initial Loading: The UI is in the process of loading, providing users with a visual indication of the upcoming content structure.

  • 关键数据:数据对于页面的初始渲染并不重要,因此可以在数据加载期间显示框架回退。

    ¥Critical Data: The data is not critical for the initial rendering of the page, allowing the skeleton fallback to be displayed while the data is loading.

  • 类似应用的感觉:该应用的设计类似于独立应用的行为,允许立即切换到回退状态。

    ¥App-Like Feel: The application is designed to resemble the behavior of a standalone app, allowing for immediate transitions to the fallbacks.

示例

¥Examples

页面导航

¥Page Navigation

繁忙指示器:你可以使用 useNavigation 指示用户正在导航到新页面:

¥Busy Indicator: You can indicate the user is navigating to a new page with useNavigation:

import { useNavigation } from "@remix-run/react";

function PendingNavigation() {
  const navigation = useNavigation();
  return navigation.state === "loading" ? (
    <div className="spinner" />
  ) : null;
}

待定链接

¥Pending Links

繁忙指示器:你可以使用 <NavLink className> 回调在导航链接本身上指示用户正在导航到该链接。

¥Busy Indicator: You can indicate on the nav link itself that the user is navigating to it with the <NavLink className> callback.

import { NavLink } from "@remix-run/react";

export function ProjectList({ projects }) {
  return (
    <nav>
      {projects.map((project) => (
        <NavLink
          key={project.id}
          to={project.id}
          className={({ isPending }) =>
            isPending ? "pending" : null
          }
        >
          {project.name}
        </NavLink>
      ))}
    </nav>
  );
}

或者通过检查参数在其旁边添加一个加载控件:

¥Or add a spinner next to it by inspecting params:

import { useParams } from "@remix-run/react";

export function ProjectList({ projects }) {
  const params = useParams();
  return (
    <nav>
      {projects.map((project) => (
        <NavLink key={project.id} to={project.id}>
          {project.name}
          {params.projectId === project.id ? (
            <Spinner />
          ) : null}
        </NavLink>
      ))}
    </nav>
  );
}

虽然链接上的本地化指示符很好,但它们并不完整。还有许多其他触发导航的方式:表单提交、浏览器中的后退和前进按钮点击、操作重定向以及命令式 navigate(path) 调用,因此通常需要一个全局指示器来捕获所有内容。

¥While localized indicators on links are nice, they are incomplete. There are many other ways a navigation can be triggered: form submissions, back and forward button clicks in the browser, action redirects, and imperative navigate(path) calls, so you'll typically want a global indicator to capture everything.

记录创建

¥Record Creation

繁忙指示器:通常最好等待记录创建完成,而不是使用乐观 UI,因为 ID 和其他字段在创建完成之前都是未知的。另请注意,此操作会重定向到新记录。

¥Busy Indicator: It's typically best to wait for a record to be created instead of using an optimistic UI since things like IDs and other fields are unknown until it completes. Also note this action redirects to the new record from the action.

import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
import { useNavigation } from "@remix-run/react";

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const project = await createRecord({
    name: formData.get("name"),
    owner: formData.get("owner"),
  });
  return redirect(`/projects/${project.id}`);
}

export default function CreateProject() {
  const navigation = useNavigation();

  // important to check you're submitting to the action
  // for the pending UI, not just any action
  const isSubmitting =
    navigation.formAction === "/create-project";

  return (
    <Form method="post" action="/create-project">
      <fieldset disabled={isSubmitting}>
        <label>
          Name: <input type="text" name="projectName" />
        </label>
        <label>
          Owner: <UserSelect />
        </label>
        <button type="submit">Create</button>
      </fieldset>
      {isSubmitting ? <BusyIndicator /> : null}
    </Form>
  );
}

你可以对 useFetcher 执行相同的操作,如果你不更改 URL(例如将记录添加到列表中),这很有用。

¥You can do the same with useFetcher, which is useful if you aren't changing the URL (maybe adding the record to a list)

import { useFetcher } from "@remix-run/react";

function CreateProject() {
  const fetcher = useFetcher();
  const isSubmitting = fetcher.state === "submitting";

  return (
    <fetcher.Form method="post" action="/create-project">
      {/* ... */}
    </fetcher.Form>
  );
}

记录更新

¥Record Updates

乐观 UI:当 UI 只是更新记录中的某个字段时,乐观 UI 是一个不错的选择。Web 应用中的许多(如果不是大多数)用户交互都倾向于更新,因此这是一种常见的模式。

¥Optimistic UI: When the UI simply updates a field on a record, an optimistic UI is a great choice. Many, if not most user interactions in a web app tend to be updates, so this is a common pattern.

import { useFetcher } from "@remix-run/react";

function ProjectListItem({ project }) {
  const fetcher = useFetcher();

  const starred = fetcher.formData
    ? // use optimistic value if submitting
      fetcher.formData.get("starred") === "1"
    : // fall back to the database state
      project.starred;

  return (
    <>
      <div>{project.name}</div>
      <fetcher.Form method="post">
        <button
          type="submit"
          name="starred"
          // use optimistic value to allow interruptions
          value={starred ? "0" : "1"}
        >
          {/* 👇 display optimistic value */}
          {starred ? "" : ""}
        </button>
      </fetcher.Form>
    </>
  );
}

延迟数据加载

¥Deferred Data Loading

框架回退:当数据延迟时,你可以使用 <Suspense> 添加回退组件。这使得 UI 无需等待数据加载即可渲染,从而提升应用的感知和实际性能。

¥Skeleton Fallback: When data is deferred, you can add fallbacks with <Suspense>. This allows the UI to render without waiting for the data to load, speeding up the perceived and actual performance of the application.

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { defer } from "@remix-run/node"; // or cloudflare/deno
import { Await } from "@remix-run/react";
import { Suspense } from "react";

export async function loader({
  params,
}: LoaderFunctionArgs) {
  const reviewsPromise = getReviews(params.productId);
  const product = await getProduct(params.productId);
  return defer({
    product: product,
    reviews: reviewsPromise,
  });
}

export default function ProductRoute() {
  const { product, reviews } =
    useLoaderData<typeof loader>();
  return (
    <>
      <ProductPage product={product} />

      <Suspense fallback={<ReviewsSkeleton />}>
        <Await resolve={reviews}>
          {(reviews) => <Reviews reviews={reviews} />}
        </Await>
      </Suspense>
    </>
  );
}

创建框架回退组件时,请考虑以下原则:

¥When creating skeleton fallbacks, consider the following principles:

  • 一致大小:确保框架回退与实际内容的尺寸匹配。这可以防止布局突然发生变化,从而提供更流畅、视觉上更具凝聚力的加载体验。在 Web 性能方面,这种权衡会最小化 累积布局偏移 (CLS) 以改进 首次内容绘制 (FCP)。你可以在回退中使用精确的维度来最小化交易。

    ¥Consistent Size: Ensure that the skeleton fallbacks match the dimensions of the actual content. This prevents sudden layout shifts, providing a smoother and more visually cohesive loading experience. In terms of web performance, this trade-off minimizes Cumulative Layout Shift (CLS) in favor of improving First Contentful Paint (FCP). You can minimize the trade with accurate dimensions in the fallback.

  • 关键数据:避免对重要信息(即页面的主要内容)使用回退策略。这对于 SEO 和元标记尤其重要。如果你延迟显示关键数据,则无法提供准确的元标记,搜索引擎也无法正确索引你的页面。

    ¥Critical Data: Avoid using fallbacks for essential information—the main content of the page. This is especially important for SEO and meta tags. If you delay showing critical data, accurate meta-tags can't be provided, and search engines won't correctly index your page.

  • 类似应用的感觉:对于不考虑 SEO 的 Web 应用 UI,更广泛地使用框架回退可能会有所帮助。这将创建一个类似于独立应用行为的界面。当用户点击链接时,他们会立即转换到框架回退。

    ¥App-Like Feel: For web application UI that doesn't have SEO concerns, it can be beneficial to use skeleton fallbacks more extensively. This creates an interface that resembles the behavior of a standalone app. When users click on links, they get an instantaneous transition to the skeleton fallbacks.

  • 链接预取:使用 <Link prefetch="intent"> 通常可以完全跳过回退。当用户将鼠标悬停或聚焦于链接时,此方法会预加载必要的数据,让网络在用户点击之前快速获取内容。这通常会导致立即导航到下一页。

    ¥Link Prefetching: Using <Link prefetch="intent"> can often skip the fallbacks completely. When users hover or focus on the link, this method preloads the necessary data, allowing the network a quick moment to fetch content before the user clicks. This often results in an immediate navigation to the next page.

结论

¥Conclusion

通过繁忙指示器、乐观 UI 和框架回退创建网络感知 UI,通过在需要网络交互的操作期间显示视觉提示,显著提升用户体验。精通这一点是构建用户信任的应用的最佳方法。

¥Creating a network-aware UI via busy indicators, optimistic UI, and skeleton fallbacks significantly improves the user experience by showing visual cues during actions that require network interaction. Getting good at this is the best way to build applications your users trust.

Remix v2.17 中文网 - 粤ICP备13048890号