Remix v2 Meta、Sessions、Auth 与 CSRF 快速参考。
【文档 Head:meta 与 links】
v2 的 meta 返回的是描述符对象数组,而不是 v1 的对象结构。v1 风格的对象字面量在陈旧代码库中仍能通过类型检查,但运行时不会渲染出任何标签。
// app/routes/posts.$slug.tsx import type { MetaFunction } from "@remix-run/node";
export const meta: MetaFunction = ({ data }) => { if (!data?.post) return [{ title: "Not Found" }]; return [ { title: ${data.post.title} | My Blog }, { name: "description", content: data.post.excerpt }, { property: "og:title", content: data.post.title }, { tagName: "link", rel: "canonical", href: data.post.url }, ]; };
<Meta /> 与 <Links /> 必须位于 root.tsx 的 <head> 内;<ScrollRestoration />、<Scripts /> 与 <LiveReload /> 放在 <body> 末尾。缺少任一聚合组件都会导致“CSS 不加载”或“meta 标签缺失”,且不会产生编译错误。
// app/root.tsx import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
export default function App() { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> <Outlet /> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> ); }
<Meta /> 与 <Links /> 的聚合方式不同。<Links /> 会遍历整条路由匹配链并渲染每个匹配路由的 links 导出——在叶子路由中声明的样式表会被自动渲染,并在导航离开时卸载。<Meta /> 不会聚合;Remix 只会选取最后一个匹配路由的 meta 数组。若要在 meta 中继承父级,需用 flatMap 显式匹配:
import type { MetaFunction } from "@remix-run/node"; import type { loader as projectLoader } from "./project.$pid";
export const meta: MetaFunction< typeof loader, { "routes/project.$pid": typeof projectLoader }
= ({ data, matches }) => { const parentMeta = matches.flatMap((m) => m.meta ?? []); const project = matches.find((m) => m.id === "routes/project.$pid")?.data; return [ ...parentMeta, { title:
${data?.task.name} | ${project?.name}}, ]; };
MetaFunction 的第二个泛型(以路由 id 为键)为父路由的 matches.find(...).data 提供类型。详见 references/meta-v2.md。
【Sessions】
必须在每个变更响应的 Set-Cookie 头中附加 commitSession。Remix 不会自动提交;调用 session.set(...) 后直接返回普通 json(data) 会静默丢弃该变更。
return redirect("/dashboard", { headers: { "Set-Cookie": await commitSession(session) }, });
session.flash(key, value) 是只读一次的;消费它的 loader 读取后仍须调用 commitSession 以清除 flash。详见 references/sessions.md。
【Auth:在 loader 中 throw redirect】
规范做法是 requireUserId(request) 辅助函数,对未鉴权请求抛出 redirect()。被抛出的响应会短路 loader,无需顶层 return。
// app/auth.server.ts import { redirect } from "@remix-run/node"; import { getSession } from "./session.server";
export async function requireUserId(request: Request): Promise<string> {
const session = await getSession(request.headers.get("Cookie"));
const userId = session.get("userId");
if (!userId) {
const url = new URL(request.url);
const redirectTo = ${url.pathname}${url.search};
throw redirect(/login?redirectTo=${encodeURIComponent(redirectTo)});
}
return userId;
}
切勿在 React 组件内做路由门禁——受保护组件仍会进行 SSR,并把 HTML/loader 数据发送给未鉴权用户。详见 references/auth-csrf.md。
【CSRF】
Remix 没有内置的 CSRF 保护。同源 <Form> 提交完全依赖你在会话 Cookie 上设置的 SameSite 值。SameSite=Lax 在当前所有浏览器中都会阻止跨站 POST 导航携带 Cookie。(Chrome 曾在 2020 年短暂出现 2 分钟的“Lax+POST”窗口——2021 年已移除。)Lax 与 Strict 的真正权衡在于子域接管:使用 Lax 时,被攻陷的子域可以凭据发起顶层 GET 导航;使用 Strict 时,来自外部站点的深链导航会丢失会话。若应用因合法跨站需求(OAuth 弹窗、iframe 嵌入)使用 SameSite=None,则完全没有 Cookie 层面的 CSRF 保护。建议使用 remix-utils/csrf 搭配专用的签名 Cookie——切勿复用会话 Cookie。Manual fetch("/api/x", { method: "POST" }) 会绕过 AuthenticityTokenInput,因此任何未调用 csrf.validate(request) 的 action 都是攻击入口。
【门禁(决策排序)】
按顺序回答。Pass 表示条件为真;在同一行选择 API 并停止。
这个 meta 标签放在哪里?
它是全站级的(charset、viewport、默认 OG 图片)?
Pass → 在 root.tsx 的 <head> 内写普通 JSX。可避免 v2 的“不合并”意外,并防止重复标签。停止。
Fail → 进入第 2 步。
它是路由专属的(title、description、canonical、JSON-LD)?
Pass → 在叶子路由文件中 export const meta;如需父级值,用 matches.flatMap((m) => m.meta ?? [])。停止。
鉴权检查:loader、action 还是辅助函数?
这是必须保护的 GET(页面渲染)?
Pass → 在 loader 顶部调用 await requireUserId(request)。停止。
这是必须保护的 POST/PUT/DELETE 变更?
Pass → 在 action 顶部调用 await requireUserId(request),并且调用 await csrf.validate(request)。停止。
登出?
Pass → 只放在 action,绝不放在 loader。指向 loader 的 <Link to="/logout"> 可被 <img src="/logout"> 触发 CSRF。应使用 <Form method="post" action="/logout">。停止。
CSRF 令牌放在哪里?
你在使用 remix-utils/csrf 吗?
Pass → 使用独立的 createCookie("csrf", { ... }) Cookie,与会话 Cookie 分开。CSRF 值是签名字符串;会话值是序列化对象——复用同一个 Cookie 会在 validate 时抛错。停止。
Fail → 进入第 2 步。
没有 CSRF 库?
Pass → 记录威胁模型;要求会话 Cookie 设置 sameSite: "strict",并在每个 action 中校验 Origin 头。更推荐改为引入 remix-utils/csrf。
【补充文档】
Meta v2:见 references/meta-v2.md,了解描述符类型、通过 matches 合并父级、JSON-LD 以及 v1→v2 迁移陷阱。
Links:见 references/links.md,了解样式表、preload、dns-prefetch 以及 <Links /> 的父级聚合行为。
Sessions:见 references/sessions.md,了解 createCookieSessionStorage 配置、commitSession/destroySession 模式、flash 消息以及数据库支持的会话。
Auth 与 CSRF:见 references/auth-csrf.md,了解 requireUserId 辅助函数、登录/登出 action、remix-auth 以及 remix-utils/csrf 接线。
【v1 与 v2 快速对比】
关注点 | v1 | v2
meta 返回结构 | 对象 { title, description } | 数组 [{ title }, { name, content }]
父级 meta 合并 | 自动合并(按 key 最后写入胜出) | 不合并;仅最后一个匹配路由
父数据 meta 参数 | parentsData | matches(手动 flatMap)
OG 标签 | { "og:title": "..." } 简写 | { property: "og:title", content: "..." }
迁移标志 | v2_meta: true future flag | 不适用(v2 默认)