特殊文件
在本页

特殊文件

React Router 在你的项目中会查找一些特殊文件。并非所有这些文件都是必需的

react-router.config.ts

此文件是可选的

config 文件用于配置你的应用的一些方面,例如是否使用服务器端渲染、特定目录的位置等等。

import type { Config } from "@react-router/dev/config";

export default {
  // Config options...
} satisfies Config;

更多信息请参阅 react-router config API 的详细说明。

root.tsx

此文件是必需的

“根”路由(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 导出。Layout 组件有两个用途

  1. 避免在根组件、HydrateFallbackErrorBoundary 中重复文档的“应用外壳”(app shell)
  2. 防止 React 在根组件/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

此文件是必需的

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;

更多信息请参阅路由指南

entry.client.tsx

此文件是可选的

默认情况下,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 等。

entry.server.tsx

此文件是可选的

默认情况下,React Router 会为你处理生成 HTTP 响应。你可以通过以下方式显示默认的服务器入口文件:

react-router reveal

此模块的 default 导出是一个函数,它允许你创建响应,包括 HTTP 状态、头和 HTML,使你可以完全控制标记的生成方式以及发送给客户端的方式。

此模块应使用带有当前请求的 contexturl<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 的取消和竞态条件处理可能会导致许多请求被中断。

流式渲染错误

当你通过renderToPipeableStreamrenderToReadableStream流式传输 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
文档和示例 CC 4.0