React Router 在你的项目中会查找一些特殊文件。并非所有这些文件都是必需的
此文件是可选的
config 文件用于配置你的应用的一些方面,例如是否使用服务器端渲染、特定目录的位置等等。
import type { Config } from "@react-router/dev/config";
export default {
// Config options...
} satisfies Config;
更多信息请参阅 react-router config API 的详细说明。
此文件是必需的
“根”路由(app/root.tsx
)是 React Router 应用中唯一一个必需的路由,因为它是 routes/
目录下所有路由的父级,并且负责渲染根 <html>
文档。
由于根路由管理着你的文档,它是渲染 React Router 提供的一些“文档级”组件的合适位置。这些组件应在根路由内部使用一次,它们包含了 React Router 为确保你的页面正确渲染所确定或构建的一切。
import type { LinksFunction } from "react-router";
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import "./global-styles.css";
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
{/* All `meta` exports on all routes will render here */}
<Meta />
{/* All `link` exports on all routes will render here */}
<Links />
</head>
<body>
{/* Child routes render here */}
<Outlet />
{/* Manages scroll position for client-side transitions */}
{/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */}
<ScrollRestoration />
{/* Script tags go here */}
{/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */}
<Scripts />
</body>
</html>
);
}
根路由支持所有 路由模块导出。
根路由还支持一个额外的可选 Layout
导出。Layout
组件有两个用途
HydrateFallback
和 ErrorBoundary
中重复文档的“应用外壳”(app shell)HydrateFallback
/ErrorBoundary
之间切换时重新挂载应用外壳元素,这可能导致闪烁(FOUC)如果 React 从 <Links>
组件中移除并重新添加 <link rel="stylesheet">
标签。export function Layout({ children }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<Meta />
<Links />
</head>
<body>
{/* children will be the root Component, ErrorBoundary, or HydrateFallback */}
{children}
<Scripts />
<ScrollRestoration />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary() {}
关于在 Layout
组件中使用 useLoaderData
的注意事项
不允许在 ErrorBoundary
组件中使用 useLoaderData
,因为它用于正常情况下的路由渲染,并且其类型定义内置了 loader
已成功运行并返回内容的假设。在 ErrorBoundary
中该假设不成立,因为触发边界的可能正是抛出错误的 loader
!为了在 ErrorBoundary
中访问 loader 数据,你可以使用 useRouteLoaderData
,它考虑到了 loader 数据可能为 undefined
的情况。
由于你的 Layout
组件在成功和错误流程中都会使用,因此也受到同样的限制。如果你需要在 Layout
中根据请求是否成功来分支逻辑,可以使用 useRouteLoaderData("root")
和 useRouteError()
。
<Layout>
组件用于渲染 ErrorBoundary
,你应该非常谨慎,确保在渲染 ErrorBoundary
时不会遇到任何渲染错误。如果你的 Layout
在尝试渲染边界时抛出另一个错误,那么它将无法使用,你的 UI 将回退到非常简陋的内置默认 ErrorBoundary
。
export function Layout({
children,
}: {
children: React.ReactNode;
}) {
const data = useRouteLoaderData("root");
const error = useRouteError();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<Meta />
<Links />
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--themeVar: ${
data?.themeVar || defaultThemeVar
}
}
`,
}}
/>
</head>
<body>
{data ? (
<Analytics token={data.analyticsToken} />
) : null}
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
此文件是必需的
routes.ts
文件用于配置 URL 模式与哪个路由模块匹配。
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("some/path", "./some/file.tsx"),
// pattern ^ ^ module file
] satisfies RouteConfig;
更多信息请参阅路由指南。
此文件是可选的
默认情况下,React Router 会为你处理客户端应用的 Hydrate 过程。你可以通过以下方式显示默认的客户端入口文件:
react-router reveal
此文件是浏览器的入口点,负责对你在服务器入口模块中由服务器生成的标记进行 Hydrate,不过你也可以在此处初始化任何其他客户端代码。
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
这是在浏览器中运行的第一段代码。你可以在此处初始化客户端库、添加仅限客户端的 Provider 等。
此文件是可选的
默认情况下,React Router 会为你处理生成 HTTP 响应。你可以通过以下方式显示默认的服务器入口文件:
react-router reveal
此模块的 default
导出是一个函数,它允许你创建响应,包括 HTTP 状态、头和 HTML,使你可以完全控制标记的生成方式以及发送给客户端的方式。
此模块应使用带有当前请求的 context
和 url
的 <ServerRouter>
元素来渲染当前页面的标记。此标记将在 JavaScript 加载到浏览器后(可选地)使用客户端入口模块进行重新 Hydrate。
streamTimeout
如果你正在流式传输响应,你可以导出一个可选的 streamTimeout
值(以毫秒为单位),该值将控制服务器等待流式 Promise settled 的时间,超过该时间后将拒绝未完成的 Promise 并关闭流。
建议将此值与用于中断 React 渲染器的超时时间解耦。你应该始终将 React 渲染超时设置为一个更高的值,以便它有时间从你的 streamTimeout
中流下底层的拒绝信息。
// Reject all pending promises from handler functions after 10 seconds
export const streamTimeout = 10000;
export default function handleRequest(...) {
return new Promise((resolve, reject) => {
// ...
const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />,
{ /* ... */ }
);
// Abort the streaming render pass after 11 seconds to allow the rejected
// boundaries to be flushed
setTimeout(abort, streamTimeout + 1000);
});
}
handleDataRequest
你可以导出一个可选的 handleDataRequest
函数,该函数允许你修改数据请求的响应。这些请求不渲染 HTML,而是在客户端 Hydrate 发生后向浏览器返回 loader 和 action 数据。
export function handleDataRequest(
response: Response,
{
request,
params,
context,
}: LoaderFunctionArgs | ActionFunctionArgs
) {
response.headers.set("X-Custom-Header", "value");
return response;
}
handleError
默认情况下,React Router 会将遇到的服务器端错误记录到控制台。如果你想更精细地控制日志记录,或者想将这些错误报告给外部服务,那么你可以导出一个可选的 handleError
函数,该函数将赋予你控制权(并禁用内置的错误日志记录)。
export function handleError(
error: unknown,
{
request,
params,
context,
}: LoaderFunctionArgs | ActionFunctionArgs
) {
if (!request.signal.aborted) {
sendErrorToErrorReportingService(error);
console.error(formatErrorForJsonLogging(error));
}
}
请注意,当请求被中断时,你通常希望避免记录日志,因为 React Router 的取消和竞态条件处理可能会导致许多请求被中断。
当你通过renderToPipeableStream
或renderToReadableStream
流式传输 HTML 响应时,你自己的 handleError
实现将只处理初始 shell 渲染期间遇到的错误。如果在后续的流式渲染期间遇到渲染错误,你需要手动处理这些错误,因为到那时 React Router 服务器已经发送了响应。
对于 renderToPipeableStream
,你可以在 onError
回调函数中处理这些错误。你需要在 onShellReady
中切换一个布尔值,以便了解错误是 shell 渲染错误(可以忽略)还是异步错误
有关示例,请参考 Node 的默认entry.server.tsx
。
抛出的 Response
请注意,这不处理从你的 loader
/action
函数中抛出的 Response
实例。此处理器的目的是查找导致意外抛出错误的你代码中的 bug。如果你正在检测某种场景并在 loader
/action
中抛出 401/404 等 Response
,那么这是一个由你的代码处理的预期流程。如果你还希望记录日志或将这些发送到外部服务,则应在抛出响应时进行。
.server
模块虽然并非严格必需,但 .server
模块是明确将整个模块标记为仅服务器端代码的好方法。如果 .server
文件或 .server
目录中的任何代码意外地进入客户端模块图,构建将失败。
app
├── .server 👈 marks all files in this directory as server-only
│ ├── auth.ts
│ └── db.ts
├── cms.server.ts 👈 marks this file as server-only
├── root.tsx
└── routes.ts
.server
模块必须位于你的应用程序目录中。
有关更多信息,请参阅侧边栏中的路由模块部分。
.client
模块虽然不常见,但你可能有一些文件或依赖项在浏览器中使用了模块副作用。你可以在文件名上使用 *.client.ts
或将文件嵌套在 .client
目录中,以强制将它们排除在服务器 bundles 之外。
// this would break the server
export const supportsVibrationAPI =
"vibrate" in window.navigator;
请注意,从该模块导出的值在服务器端都将是 undefined
,因此唯一可以使用它们的地方是 useEffect
和用户事件(例如点击处理函数)中。
import { supportsVibrationAPI } from "./feature-check.client.ts";
console.log(supportsVibrationAPI);
// server: undefined
// client: true | false