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