待处理的 UI
在本页

待定和乐观 UI

网页上出色的用户体验与平庸的用户体验之间的区别在于开发人员如何通过在网络密集型操作期间提供视觉提示来实施网络感知的用户界面反馈。主要有三种类型的待定 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 是一个不错的选择。许多(如果不是大多数)网络应用程序中的用户交互往往是更新,因此这是一种常见模式。

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

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

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

结论

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

文档和示例受 MIT