2.1 路由约定总览
Remix v2 使用扁平文件路由(Flat Routes)约定:所有路由文件都放在 app/routes/ 目录下,用点号(.)分隔来表达路由层级关系,而不是用子目录嵌套。这让路由结构一目了然,无需层层展开文件夹。
app/routes/
├── _index.tsx # URL: /
├── about.tsx # URL: /about
├── blog.tsx # URL: /blog(布局路由,含 Outlet)
├── blog._index.tsx # URL: /blog(索引路由)
├── blog.$slug.tsx # URL: /blog/:slug(动态路由)
├── blog.new.tsx # URL: /blog/new
├── _auth.tsx # 无路径布局(下划线前缀)
├── _auth.login.tsx # URL: /login(共享 _auth 布局)
├── _auth.register.tsx # URL: /register(共享 _auth 布局)
└── $.tsx # 通配符路由(404 页面)
STRUCTURE
2.2 索引路由 _index.tsx
文件名以 _index 结尾表示索引路由,当访问父路由路径(不带子路径)时渲染。它解决了"既要父路由有 Outlet 布局,又要父路径有内容"的问题。
// app/routes/_index.tsx — 首页 /
export default function IndexRoute() {
return (
<main>
<h1>欢迎来到 Remix</h1>
</main>
);
}
TSX
// app/routes/blog._index.tsx — /blog 索引
// 当用户访问 /blog(不带文章)时显示文章列表
import { useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";
export async function loader() {
const posts = await getPosts();
return json({ posts });
}
export default function BlogIndex() {
const { posts } = useLoaderData<typeof loader>();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
TSX
2.3 布局路由与 Outlet
当一个路由文件(如 blog.tsx)与同名子路由(如 blog.$slug.tsx)同时存在时,前者自动成为布局路由。布局路由需要渲染 <Outlet /> 来指定子路由的插入位置。
// app/routes/blog.tsx — /blog 布局路由
import { Outlet } from "@remix-run/react";
export default function BlogLayout() {
return (
<div className="blog-container">
<nav className="blog-sidebar">
<h2>博客</h2>
{/* 侧边栏内容 */}
</nav>
<main className="blog-content">
{/* 子路由(文章列表或文章详情)在这里渲染 */}
<Outlet />
</main>
</div>
);
}
TSX
路由层级示意
访问 /blog/hello-world 时,渲染树为:
root.tsx → blog.tsx (布局) → blog.$slug.tsx (内容)
每一层都有各自的 loader,数据并行加载,互不阻塞。
2.4 无路径布局路由(下划线前缀)
文件名以单下划线 _ 开头(如 _auth.tsx)表示无路径布局路由:它提供共享 UI 布局,但不向 URL 贡献路径段。子路由(如 _auth.login.tsx)的 URL 仍是 /login,而非 /auth/login。
// app/routes/_auth.tsx — 认证页面共享布局
// URL 不包含 "_auth" 路径段
import { Outlet } from "@remix-run/react";
export default function AuthLayout() {
return (
<div className="auth-page">
<div className="auth-box">
<Outlet /> {/* /login 或 /register */}
</div>
</div>
);
}
TSX
2.5 动态路径参数 $param
文件名中以 $ 开头的片段表示动态路径参数。例如 blog.$slug.tsx 匹配 /blog/任意值,参数名为 slug,可通过 params 对象在 loader/action 中访问。
// app/routes/blog.$slug.tsx
import { type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useParams } from "@remix-run/react";
import { json } from "@remix-run/node";
export async function loader({ params }: LoaderFunctionArgs) {
// params.slug 即 URL 中的动态参数
const post = await getPost(params.slug!);
if (!post) throw new Response("Not Found", { status: 404 });
return json({ post });
}
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
TSX
可选参数片段 ($optional)
将参数名用括号括起来表示可选参数,例如 ($lang).about.tsx 同时匹配 /about 和 /zh/about。
// app/routes/($lang).about.tsx
// 匹配 /about 和 /en/about、/zh/about 等
export async function loader({ params }: LoaderFunctionArgs) {
const lang = params.lang ?? "zh"; // 未提供则默认中文
return json({ lang });
}
TSX
2.6 路由元数据 meta export
每条路由可以导出 meta 函数,返回该页面的 <title>、<meta> 等标签,自动插入到 root.tsx 的 <Meta /> 位置,无需 document.title 操作。
// app/routes/blog.$slug.tsx
import { type MetaFunction } from "@remix-run/node";
export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data) {
return [{ title: "文章未找到" }];
}
return [
{ title: `${data.post.title} — 我的博客` },
{ name: "description", content: data.post.excerpt },
{ property: "og:title", content: data.post.title },
{ property: "og:image", content: data.post.coverImage },
];
};
TSX
2.7 links export — 路由级 CSS
每条路由可以导出 links 函数,声明该路由需要加载的 CSS/预加载资源。Remix 会在路由激活时注入这些链接,路由卸载时移除,实现路由级样式隔离。
import { type LinksFunction } from "@remix-run/node";
import blogStyles from "~/styles/blog.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: blogStyles },
{ rel: "preload", href: "/fonts/title.woff2", as: "font", type: "font/woff2", crossOrigin: "anonymous" },
];
TSX
2.8 路由约定速查表
| 文件名模式 | URL 示例 | 作用 |
|---|---|---|
_index.tsx | / | 根索引路由 |
about.tsx | /about | 静态路由 |
blog.tsx | /blog/* | 布局路由(含 Outlet) |
blog._index.tsx | /blog | 布局的索引 |
blog.$slug.tsx | /blog/:slug | 动态参数 |
_layout.tsx | 无路径贡献 | 无 URL 的共享布局 |
($lang).about.tsx | /about 或 /zh/about | 可选参数 |
$.tsx | /*(兜底) | 通配符/404 |
路由可视化工具运行 npx remix routes 可以在终端输出当前项目的完整路由树,帮助理解嵌套关系。也可访问开发模式下的 /__remix/routes 查看路由详情。
本章小结Remix 的扁平路由约定通过点号分隔表达嵌套关系,核心规则:布局路由(同名文件)提供 Outlet 容器;索引路由(_index 后缀)处理父路径默认内容;无路径布局(下划线前缀)共享 UI 而不影响 URL;动态参数($ 前缀)匹配变化的路径段。每条路由还可通过 meta 和 links 导出控制页面标签和样式。