Web 上出色用户体验与平庸用户体验之间的差异,在于开发者如何通过在网络密集型操作期间提供视觉提示,来很好地实现网络感知用户界面反馈。待定 UI 主要有三种类型:忙碌指示器、乐观 UI 和骨架回退。本文档提供了根据特定场景选择和实现适当反馈机制的指南。
忙碌指示器:忙碌指示器在服务器处理操作时向用户显示视觉提示。当应用程序无法预测操作的结果,并且必须等待服务器的响应才能更新 UI 时,会使用此反馈机制。
乐观 UI:乐观 UI 通过在收到服务器响应之前立即使用预期状态更新 UI,来提高感知速度和响应能力。当应用程序可以根据上下文和用户输入预测操作的结果时,会使用此方法,从而允许对操作做出即时响应。
骨架回退:当 UI 最初加载时,会使用骨架回退,为用户提供一个视觉占位符,概述即将显示的内容结构。此反馈机制对于尽快渲染有用的内容特别有用。
使用乐观 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>
</>
);
}
创建骨架回退时,请考虑以下原则
<Link prefetch="intent">
通常可以完全跳过回退。当用户悬停或聚焦在链接上时,此方法会预加载所需的数据,从而允许网络在用户点击之前快速获取内容。这通常会导致立即导航到下一页。通过繁忙指示器、乐观 UI 和骨架回退创建网络感知的 UI 可以通过在需要网络交互的操作期间显示视觉提示来显着改善用户体验。擅长这一点是构建用户信任的应用程序的最佳方法。