React Router 实现了“客户端路由”。
在传统的网站中,浏览器从 Web 服务器请求文档,下载并评估 CSS 和 JavaScript 资产,然后渲染从服务器发送的 HTML。当用户点击链接时,它会为新页面重新开始整个过程。
客户端路由允许您的应用程序从链接点击更新 URL,而无需从服务器请求另一个文档。相反,您的应用程序可以立即渲染一些新的 UI 并使用 fetch
发出数据请求以使用新信息更新页面。
这可以实现更快的用户体验,因为浏览器不需要为下一页请求全新的文档或重新评估 CSS 和 JavaScript 资产。它还使用动画等功能实现了更动态的用户体验。
客户端路由通过创建 Router
并使用 Link
和 <Form>
链接/提交页面来实现。
import * as React from "react";
import { createRoot } from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
Route,
Link,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
<h1>Hello World</h1>
<Link to="about">About Us</Link>
</div>
),
},
{
path: "about",
element: <div>About</div>,
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
嵌套路由是将 URL 片段与组件层次结构和数据耦合的通用概念。React Router 的嵌套路由灵感来自 2014 年左右的 Ember.js 中的路由系统。Ember 团队意识到,在几乎所有情况下,URL 片段都会决定
React Router 通过 API 接受这种约定,用于创建与 URL 片段和数据耦合的嵌套布局。
// Configure nested routes with JSX
createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
<Route path="contact" element={<Contact />} />
<Route
path="dashboard"
element={<Dashboard />}
loader={({ request }) =>
fetch("/api/dashboard.json", {
signal: request.signal,
})
}
/>
<Route element={<AuthLayout />}>
<Route
path="login"
element={<Login />}
loader={redirectIfUser}
/>
<Route path="logout" action={logoutUser} />
</Route>
</Route>
)
);
// Or use plain objects
createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "contact",
element: <Contact />,
},
{
path: "dashboard",
element: <Dashboard />,
loader: ({ request }) =>
fetch("/api/dashboard.json", {
signal: request.signal,
}),
},
{
element: <AuthLayout />,
children: [
{
path: "login",
element: <Login />,
loader: redirectIfUser,
},
{
path: "logout",
action: logoutUser,
},
],
},
],
},
]);
这个 可视化 可能会有所帮助。
URL 的片段可以是动态占位符,这些占位符会被解析并提供给各种 API。
<Route path="projects/:projectId/tasks/:taskId" />
使用 :
的两个片段是动态的,并提供给以下 API
// If the current location is /projects/abc/tasks/3
<Route
// sent to loaders
loader={({ params }) => {
params.projectId; // abc
params.taskId; // 3
}}
// and actions
action={({ params }) => {
params.projectId; // abc
params.taskId; // 3
}}
element={<Task />}
/>;
function Task() {
// returned from `useParams`
const params = useParams();
params.projectId; // abc
params.taskId; // 3
}
function Random() {
const match = useMatch(
"/projects/:projectId/tasks/:taskId"
);
match.params.projectId; // abc
match.params.taskId; // 3
}
查看
当将 URL 与路由匹配时,React Router 会根据片段数量、静态片段、动态片段、通配符等对路由进行排名,并选择最具体的匹配项。
例如,考虑以下两个路由
<Route path="/teams/:teamId" />
<Route path="/teams/new" />
现在,假设 URL 是 http://example.com/teams/new。
即使这两个路由在技术上都与 URL 匹配(new
可以是 :teamId
),您直观地知道我们希望选择第二个路由(/teams/new
)。React Router 的匹配算法也知道这一点。
使用排名路由,您无需担心路由顺序。
大多数 Web 应用程序在 UI 的顶部、侧边栏以及通常的多个级别都有持久导航部分。使用 <NavLink>
可以轻松地为活动导航项设置样式,以便用户知道他们在应用程序中的位置 (isActive
) 或他们将要前往的位置 (isPending
)。
<NavLink
style={({ isActive, isPending }) => {
return {
color: isActive ? "red" : "inherit",
};
}}
className={({ isActive, isPending }) => {
return isActive ? "active" : isPending ? "pending" : "";
}}
/>
您还可以 useMatch
用于链接之外的任何其他“活动”指示。
function SomeComp() {
const match = useMatch("/messages");
return <li className={Boolean(match) ? "active" : ""} />;
}
查看
与 HTML <a href>
一样,<Link to>
和 <NavLink to>
可以接受相对路径,并具有嵌套路由的增强行为。
假设以下路由配置
<Route path="home" element={<Home />}>
<Route path="project/:projectId" element={<Project />}>
<Route path=":taskId" element={<Task />} />
</Route>
</Route>
考虑 URL https://example.com/home/project/123,它会渲染以下路由组件层次结构
<Home>
<Project />
</Home>
如果 <Project />
渲染以下链接,则链接的 href 将按如下方式解析
在 <Project> @ /home/project/123 中 |
解析后的 <a href> |
---|---|
<Link to="abc"> |
/home/project/123/abc |
<Link to="."> |
/home/project/123 |
<Link to=".."> |
/home |
<Link to=".." relative="path"> |
/home/project |
请注意,第一个 ..
会删除 project/:projectId
路由的两个片段。默认情况下,相对链接中的 ..
会遍历路由层次结构,而不是 URL 片段。在下一个示例中添加 relative="path"
允许您遍历路径片段。
相对链接始终相对于它们渲染的路由路径,而不是相对于完整的 URL。这意味着,如果用户使用 <Link to="abc">
导航到更深层的 <Task />
,URL 为 /home/project/123/abc
,则 <Project>
中的 href 不会改变(与普通的 <a href>
相反,这是客户端路由器的一个常见问题)。
由于 URL 片段通常映射到应用程序的持久数据,因此 React Router 提供了传统的 data loading hook,以便在导航期间启动 data loading。与嵌套路由结合使用,可以在特定 URL 下为多个布局并行加载所有数据。
<Route
path="/"
loader={async ({ request }) => {
// loaders can be async functions
const res = await fetch("/api/user.json", {
signal: request.signal,
});
const user = await res.json();
return user;
}}
element={<Root />}
>
<Route
path=":teamId"
// loaders understand Fetch Responses and will automatically
// unwrap the res.json(), so you can simply return a fetch
loader={({ params }) => {
return fetch(`/api/teams/${params.teamId}`);
}}
element={<Team />}
>
<Route
path=":gameId"
loader={({ params }) => {
// of course you can use any data store
return fakeSdk.getTeam(params.gameId);
}}
element={<Game />}
/>
</Route>
</Route>
数据通过 useLoaderData
提供给您的组件。
function Root() {
const user = useLoaderData();
// data from <Route path="/">
}
function Team() {
const team = useLoaderData();
// data from <Route path=":teamId">
}
function Game() {
const game = useLoaderData();
// data from <Route path=":gameId">
}
当用户访问或点击链接到 https://example.com/real-salt-lake/45face3 时,所有三个路由加载器都将被调用并并行加载,然后渲染该 URL 的 UI。
在加载或更改数据时,通常需要将用户重定向到不同的路由。
<Route
path="dashboard"
loader={async () => {
const user = await fake.getUser();
if (!user) {
// if you know you can't render the route, you can
// throw a redirect to stop executing code here,
// sending the user to a new route
throw redirect("/login");
}
// otherwise continue
const stats = await fake.getDashboardStats();
return { user, stats };
}}
/>
<Route
path="project/new"
action={async ({ request }) => {
const data = await request.formData();
const newProject = await createProject(data);
// it's common to redirect after actions complete,
// sending the user to the new record
return redirect(`/projects/${newProject.id}`);
}}
/>
查看
当用户在应用程序中导航时,下一页的数据会在页面渲染之前加载。在此期间提供用户反馈非常重要,这样应用程序就不会感觉没有响应。
function Root() {
const navigation = useNavigation();
return (
<div>
{navigation.state === "loading" && <GlobalSpinner />}
<FakeSidebar />
<Outlet />
<FakeFooter />
</div>
);
}
查看
<Suspense>
的骨架 UI您可以 defer
数据,而不是等待下一页的数据,这样 UI 就可以立即翻转到下一个屏幕,并显示占位符 UI,同时数据正在加载。
<Route
path="issue/:issueId"
element={<Issue />}
loader={async ({ params }) => {
// these are promises, but *not* awaited
const comments = fake.getIssueComments(params.issueId);
const history = fake.getIssueHistory(params.issueId);
// the issue, however, *is* awaited
const issue = await fake.getIssue(params.issueId);
// defer enables suspense for the un-awaited promises
return defer({ issue, comments, history });
}}
/>;
function Issue() {
const { issue, history, comments } = useLoaderData();
return (
<div>
<IssueDescription issue={issue} />
{/* Suspense provides the placeholder fallback */}
<Suspense fallback={<IssueHistorySkeleton />}>
{/* Await manages the deferred data (promise) */}
<Await resolve={history}>
{/* this calls back when the data is resolved */}
{(resolvedHistory) => (
<IssueHistory history={resolvedHistory} />
)}
</Await>
</Suspense>
<Suspense fallback={<IssueCommentsSkeleton />}>
<Await resolve={comments}>
{/* ... or you can use hooks to access the data */}
<IssueComments />
</Await>
</Suspense>
</div>
);
}
function IssueComments() {
const comments = useAsyncValue();
return <div>{/* ... */}</div>;
}
查看
HTML 表单是导航事件,就像链接一样。React Router 支持使用客户端路由的 HTML 表单工作流程。
当提交表单时,会阻止正常的浏览器导航事件,并创建一个带有包含提交的 FormData
的主体的 Request
。此请求将发送到与表单的 <Form action>
匹配的 <Route action>
。
表单元素的 name
属性将提交到操作
<Form action="/project/new">
<label>
Project title
<br />
<input type="text" name="title" />
</label>
<label>
Target Finish Date
<br />
<input type="date" name="due" />
</label>
</Form>
正常的 HTML 文档请求将被阻止,并发送到匹配路由的操作(与 <form action>
匹配的 <Route path>
),包括 request.formData
。
<Route
path="project/new"
action={async ({ request }) => {
const formData = await request.formData();
const newProject = await createProject({
title: formData.get("title"),
due: formData.get("due"),
});
return redirect(`/projects/${newProject.id}`);
}}
/>
几十年的 Web 惯例表明,当将表单发布到服务器时,数据正在更改,并且会渲染一个新页面。React Router 的基于 HTML 的数据变动 API 遵循了这一惯例。
在调用路由操作后,将再次调用页面上所有数据的加载器,以确保 UI 自动与数据保持最新。无需过期缓存键,无需重新加载上下文提供者。
查看
当将表单提交到路由操作时,您可以访问导航状态以显示繁忙指示器、禁用字段集等。
function NewProjectForm() {
const navigation = useNavigation();
const busy = navigation.state === "submitting";
return (
<Form action="/project/new">
<fieldset disabled={busy}>
<label>
Project title
<br />
<input type="text" name="title" />
</label>
<label>
Target Finish Date
<br />
<input type="date" name="due" />
</label>
</fieldset>
<button type="submit" disabled={busy}>
{busy ? "Creating..." : "Create"}
</button>
</Form>
);
}
查看
了解发送到 action 的 formData
通常足以跳过繁忙指示器,并立即在下一个状态中渲染 UI,即使您的异步工作仍在进行中。这称为“乐观 UI”。
function LikeButton({ tweet }) {
const fetcher = useFetcher();
// if there is `formData` then it is posting to the action
const liked = fetcher.formData
? // check the formData to be optimistic
fetcher.formData.get("liked") === "yes"
: // if its not posting to the action, use the record's value
tweet.liked;
return (
<fetcher.Form method="post" action="toggle-liked">
<button
type="submit"
name="liked"
value={liked ? "yes" : "no"}
/>
</fetcher.Form>
);
}
(是的,HTML 按钮可以有 name
和 value
)。
虽然使用 fetcher
进行乐观 UI 更常见,但您也可以使用普通表单使用 navigation.formData
做到同样的事情。
HTML 表单是变动的模型,但它们有一个主要限制:一次只能有一个,因为表单提交是导航。
大多数 Web 应用程序需要允许同时发生多个变动,例如记录列表,其中每个记录可以独立删除、标记为已完成、喜欢等。
获取器 允许您与路由 操作 和 加载器 交互,而不会在浏览器中引起导航,但仍然可以获得所有传统的好处,例如错误处理、重新验证、中断处理和竞争条件处理。
想象一个任务列表
function Tasks() {
const tasks = useLoaderData();
return tasks.map((task) => (
<div>
<p>{task.name}</p>
<ToggleCompleteButton task={task} />
</div>
));
}
每个任务都可以独立于其他任务标记为已完成,并具有自己的挂起状态,而不会使用 获取器 引起导航。
function ToggleCompleteButton({ task }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="post" action="/toggle-complete">
<fieldset disabled={fetcher.state !== "idle"}>
<input type="hidden" name="id" value={task.id} />
<input
type="hidden"
name="status"
value={task.complete ? "incomplete" : "complete"}
/>
<button type="submit">
{task.status === "complete"
? "Mark Incomplete"
: "Mark Complete"}
</button>
</fieldset>
</fetcher.Form>
);
}
查看
React Router 会自动取消陈旧的操作,并且只提交最新数据。
只要有异步 UI,就会有发生竞争条件的风险:当异步操作在较早操作之后开始,但在较早操作之前完成时。结果是用户界面显示了错误的状态。
考虑一个搜索字段,它会在用户键入时更新列表
?q=ry |---------------|
^ commit wrong state
?q=ryan |--------|
^ lose correct state
即使 q?=ryan
的查询后来发出,但它先完成。如果处理不当,结果将短暂地显示 ?q=ryan
的正确值,然后翻转到 ?q=ry
的不正确结果。节流和去抖动还不够(您仍然可以中断通过的请求)。您需要取消。
如果您使用 React Router 的数据约定,您将完全自动避免此问题。
?q=ry |-----------X
^ cancel wrong state when
correct state completes earlier
?q=ryan |--------|
^ commit correct state
React Router 不仅处理此类导航的竞争条件,而且还处理许多其他情况,例如加载自动完成的结果或使用 fetcher
(及其自动的并发重新验证)执行多个并发变动。
React Router 会自动处理应用程序的大多数错误。它会捕获在以下情况下抛出的任何错误:
实际上,这几乎是您应用程序中的所有错误,除了在事件处理程序(<button onClick>
)或 useEffect
中抛出的错误。React Router 应用程序往往很少使用其中任何一个。
当抛出错误时,React Router 会渲染 errorElement
,而不是渲染路由的 element
。
<Route
path="/"
loader={() => {
something.that.throws.an.error();
}}
// this will not be rendered
element={<HappyPath />}
// but this will instead
errorElement={<ErrorBoundary />}
/>
如果路由没有 errorElement
,错误将冒泡到具有 errorElement
的最近的父路由。
<Route
path="/"
element={<HappyPath />}
errorElement={<ErrorBoundary />}
>
{/* Errors here bubble up to the parent route */}
<Route path="login" element={<Login />} />
</Route>
查看
React Router 会在导航时模拟浏览器的滚动恢复,等待数据加载后再滚动。这确保滚动位置恢复到正确的位置。
您还可以通过根据除位置之外的其他内容(例如 URL 路径名)进行恢复,以及阻止在某些链接(例如页面中间的选项卡)上发生滚动来自定义行为。
查看
React Router 是基于 Web 标准 API 构建的。 加载器 和 操作 接收标准 Web Fetch API Request
对象,并且也可以返回 Response
对象。取消是使用 中止信号 完成的,搜索参数是使用 URLSearchParams
处理的,数据变动是使用 HTML 表单 处理的。
当您对 React Router 越来越熟悉时,您也会对 Web 平台越来越熟悉。