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",
});
通常,您的数据比您的路由模块更能指示您的缓存持续时间(数据往往比标记更动态),因此 action
的 & loader
的标头也会传递到 headers()
中。
import type { HeadersFunction } from "@remix-run/node"; // or cloudflare/deno
export const headers: HeadersFunction = ({
loaderHeaders,
}) => ({
"Cache-Control": loaderHeaders.get("Cache-Control"),
});
注意:actionHeaders
& loaderHeaders
是 Web Fetch API Headers
类的实例。
如果 action
或 loader
抛出了 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.tsx
的 headers
函数的返回值。如果该文件没有导出一个,则它将使用 users.tsx
中的结果,依此类推。
如果所有三个都定义了 headers
,则最深层的模块获胜,在本例中为 users.$userId.profile.tsx
。但是,如果您的 users.$userId.profile.tsx
的 loader
抛出并冒泡到 users.userId.tsx
中的边界 - 则将使用 users.userId.tsx
的 headers
函数,因为它才是叶子渲染路由。
我们不希望在您的响应中出现意外的标头,因此如果您愿意,则需要合并它们。Remix 将 parentHeaders
传递给您的 headers
函数。因此,users.tsx
标头将传递到 users.$userId.tsx
,然后 users.$userId.tsx
的 headers
将传递到 users.$userId.profile.tsx
的 headers
。
也就是说,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
。