跳转到内容

Pending UI

待处理和乐观 UI

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

待定的 UI 反馈机制

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

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

Skeleton Fallbacks:Skeleton Fallbacks 用于 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 和其他字段等内容在完成之前都是未知的。另请注意,此操作会从操作重定向到新记录。

app/routes/create-project.tsx
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>
</>
);
}

延迟数据加载

Skeleton Fallback:当数据延迟时,可以使用 <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 可在需要网络交互的操作期间显示视觉提示,从而显著改善用户体验。掌握这一点是构建用户信任的应用程序的最佳方式。