React Router v7 已发布。 查看文档
待定 UI
本页内容

待定和乐观 UI

Web 上出色用户体验与平庸用户体验之间的差异,在于开发者如何通过在网络密集型操作期间提供视觉提示,来很好地实现网络感知用户界面反馈。待定 UI 主要有三种类型:忙碌指示器、乐观 UI 和骨架回退。本文档提供了根据特定场景选择和实现适当反馈机制的指南。

待定 UI 反馈机制

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

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

骨架回退:当 UI 最初加载时,会使用骨架回退,为用户提供一个视觉占位符,概述即将显示的内容结构。此反馈机制对于尽快渲染有用的内容特别有用。

反馈选择的指导原则

使用乐观 UI

  • 下一状态可预测性:应用程序可以根据用户的操作准确预测 UI 的下一状态。
  • 错误处理:已部署强大的错误处理机制,以解决该过程中可能发生的潜在错误。
  • URL 稳定性:该操作不会导致 URL 发生更改,确保用户停留在同一页面内。

使用忙碌指示器

  • 下一状态不确定性:无法可靠地预测操作的结果,因此需要等待服务器的响应。
  • URL 更改:该操作会导致 URL 发生更改,指示导航到新页面或新部分。
  • 错误边界:错误处理方法主要依赖于管理异常和意外行为的错误边界。
  • 副作用:该操作会触发涉及关键过程的副作用,例如发送电子邮件、处理付款等。

使用骨架回退

  • 初始加载:UI 正在加载过程中,为用户提供即将显示的内容结构的视觉指示。
  • 关键数据:数据对于页面的初始渲染不是至关重要的,因此允许在加载数据时显示骨架回退。
  • 类似应用程序的体验:应用程序旨在模仿独立应用程序的行为,允许立即转换为回退。

示例

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

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

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

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

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>
  );
}

或者通过检查参数在其旁边添加一个加载指示器。

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) 调用,因此您通常需要一个全局指示器来捕获所有内容。

记录创建

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

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(可能只是将记录添加到列表)这会很有用。

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>
  );
}

记录更新

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

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>
    </>
  );
}

延迟数据加载

骨架回退:当数据被延迟时,您可以使用 <Suspense> 添加回退。这允许 UI 在不等待数据加载的情况下呈现,从而加快应用程序的感知和实际性能。

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>
    </>
  );
}

创建骨架回退时,请考虑以下原则

  • 一致的大小: 确保骨架回退与实际内容的尺寸相匹配。这可以防止突然的布局偏移,提供更平滑、更具视觉凝聚力的加载体验。在 Web 性能方面,这种权衡最大限度地减少了 累积布局偏移 (CLS),以提高 首次内容绘制 (FCP)。您可以通过回退中的准确尺寸来最小化这种权衡。
  • 关键数据: 避免对基本信息(页面的主要内容)使用回退。这对于 SEO 和元标记尤其重要。如果您延迟显示关键数据,则无法提供准确的元标记,搜索引擎将无法正确索引您的页面。
  • 类似应用程序的感觉:对于没有 SEO 问题的 Web 应用程序 UI,更广泛地使用骨架回退可能是有益的。这将创建一个类似于独立应用程序行为的界面。当用户单击链接时,他们会立即过渡到骨架回退。
  • 链接预取: 使用 <Link prefetch="intent"> 通常可以完全跳过回退。当用户悬停或聚焦在链接上时,此方法会预加载所需的数据,从而允许网络在用户点击之前快速获取内容。这通常会导致立即导航到下一页。

结论

通过繁忙指示器、乐观 UI 和骨架回退创建网络感知的 UI 可以通过在需要网络交互的操作期间显示视觉提示来显着改善用户体验。擅长这一点是构建用户信任的应用程序的最佳方法。

文档和示例在以下许可下授权 MIT