headers

headers

每个路由都可以定义自己的 HTTP 标头。常见标头之一是Cache-Control 标头,它指示浏览器和 CDN 缓存页面可以在哪里以及多长时间缓存。

import type { HeadersFunction } from "@remix-run/node"; // or cloudflare/deno

export const headers: HeadersFunction = ({
  actionHeaders,
  errorHeaders,
  loaderHeaders,
  parentHeaders,
}) => ({
  "X-Stretchy-Pants": "its for fun",
  "Cache-Control": "max-age=300, s-maxage=3600",
});

通常,您的数据比路由模块更能指示缓存持续时间(数据往往比标记更动态),因此actionloader 的标头也会传递到 headers() 中。

import type { HeadersFunction } from "@remix-run/node"; // or cloudflare/deno

export const headers: HeadersFunction = ({
  loaderHeaders,
}) => ({
  "Cache-Control": loaderHeaders.get("Cache-Control"),
});

注意:actionHeadersloaderHeadersWeb Fetch API Headers 类的实例。

如果一个 actionloader 抛出了一个 Response 并且我们正在渲染一个边界,那么来自抛出 Response 的任何标头都将在 errorHeaders 中可用。这允许您访问在父错误边界中抛出的子加载器中的标头。

嵌套路由

由于 Remix 有嵌套路由,因此当嵌套路由匹配时,标头将进行一场争夺。默认行为是 Remix 只利用在其可渲染匹配中找到的最深的 headers 函数的返回值(包括错误存在时的边界路由)。

├── users.tsx
├── users.$userId.tsx
└── users.$userId.profile.tsx

如果我们正在查看 /users/123/profile,那么将渲染三个路由

<Users>
  <UserId>
    <Profile />
  </UserId>
</Users>

如果用户正在查看 /users/123/profile 并且 users.$userId.profile.tsx 未导出 headers 函数,那么 Remix 将使用 users.$userId.tsxheaders 函数的返回值。如果该文件未导出该函数,那么它将使用 users.tsx 中的函数的结果,依此类推。

如果所有三个文件都定义了 headers,则最深层的模块获胜,在本例中为 users.$userId.profile.tsx。但是,如果您的 users.$userId.profile.tsxloader 抛出并冒泡到 users.userId.tsx 中的边界 - 那么 users.userId.tsxheaders 函数将被使用,因为它是最深的已渲染路由。

我们不希望在响应中出现意外的标头,因此您需要自己进行合并。Remix 将 parentHeaders 传递到您的 headers 函数中。因此,users.tsx 的标头将传递到 users.$userId.tsx,然后 users.$userId.tsxheaders 将传递到 users.$userId.profile.tsxheaders 中。

也就是说,Remix 给您提供了一把很大的枪,用来射击您的脚。您需要小心,不要从子路由模块发送比父路由更激进的 Cache-Control。以下是一些代码,它在这些情况下会选择最不激进的缓存

import type { HeadersFunction } from "@remix-run/node"; // or cloudflare/deno
import parseCacheControl from "parse-cache-control";

export const headers: HeadersFunction = ({
  loaderHeaders,
  parentHeaders,
}) => {
  const loaderCache = parseCacheControl(
    loaderHeaders.get("Cache-Control")
  );
  const parentCache = parseCacheControl(
    parentHeaders.get("Cache-Control")
  );

  // take the most conservative between the parent and loader, otherwise
  // we'll be too aggressive for one of them.
  const maxAge = Math.min(
    loaderCache["max-age"],
    parentCache["max-age"]
  );

  return {
    "Cache-Control": `max-age=${maxAge}`,
  };
};

总之,您可以通过不在父路由中定义标头,而只在叶路由中定义,来避免整个问题。每个可以直接访问的布局都可能有一个“索引路由”。如果您只在叶路由(而不是父路由)上定义标头,您将永远不必担心合并标头。

请注意,您还可以在 entry.server.tsx 文件中添加标头,用于需要全局使用的标头,例如

import type {
  AppLoadContext,
  EntryContext,
} from "@remix-run/node"; // or cloudflare/deno
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  loadContext: AppLoadContext
) {
  const markup = renderToString(
    <RemixServer context={remixContext} url={request.url} />
  );

  responseHeaders.set("Content-Type", "text/html");
  responseHeaders.set("X-Powered-By", "Hugs");

  return new Response("<!DOCTYPE html>" + markup, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}

请注意,这样做将应用于所有文档请求,但不适用于data请求(例如客户端过渡)。对于那些,请使用 handleDataRequest

文档和示例根据 MIT