虽然你可以通过“routes”插件选项配置路由,但大多数路由都是通过此文件系统约定创建的。添加一个文件,获取一个路由。
请注意,你可以使用 .js、.jsx、.ts 或 .tsx 文件扩展名。在示例中,我们将坚持使用 .tsx 以避免重复。
在我们深入研究 Remix 的约定之前,我们想指出,基于文件的路由是一个非常主观的想法。有些人喜欢“扁平”路由的想法,有些人讨厌它,宁愿将路由嵌套在文件夹中。有些人只是讨厌基于文件的路由,宁愿通过 JSON 配置路由。有些人宁愿像在 React Router SPA 中那样通过 JSX 配置路由。
关键是,我们非常清楚这一点,并且从一开始,Remix 就一直为你提供了一种通过routes/ignoredRouteFiles和手动配置路由来选择退出的第一类方式。但是,必须有一些默认设置,以便人们可以快速轻松地启动并运行 - 我们认为,下面的扁平路由约定文档对于中小型应用程序来说是一个很好的默认设置。
具有数百或数千个路由的大型应用程序,无论你使用什么约定,都总是会有点混乱 - 并且我们的想法是通过 routes 配置,你可以构建完全最适合你的应用程序/团队的约定。Remix 拥有一个让每个人都满意的默认约定是不可能的。我们宁愿给你一个相当简单的默认设置,然后让社区构建你可以选择的任何数量的约定。
因此,在我们深入研究 Remix 默认约定的细节之前,这里有一些社区替代方案,如果你认为我们的默认不是你喜欢的,可以查看一下。
remix-flat-routes - Remix 默认基本上是此软件包的简化版本。作者一直在迭代和改进这个软件包,因此如果你通常喜欢“扁平路由”的想法,但想要更多的功能(包括文件和文件夹的混合方法),绝对应该查看这个软件包。remix-custom-routes - 如果你想要更多的自定义,此软件包允许你定义应被视为路由的文件类型。这使你可以超越简单的扁平/嵌套概念,并执行诸如“任何扩展名为 .route.tsx 的文件都是路由”之类的操作。remix-json-routes - 如果你只想通过配置文件指定路由,这是你的首选 - 只需向 Remix 提供带有路由的 JSON 对象,并完全跳过扁平/嵌套概念。那里甚至还有一个 JSX 选项。app/
├── routes/
└── root.tsx
app/root.tsx 文件是你的根布局,或者称为“根路由”(对于那些发音相同的人,非常抱歉!)。它的工作方式与其他所有路由一样,因此你可以导出一个 loader、action 等。
根路由通常看起来像这样。它充当整个应用程序的根布局,所有其他路由都将在 <Outlet /> 中渲染。
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export default function Root() {
return (
<html lang="en">
<head>
<Links />
<Meta />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
app/routes 目录中的任何 JavaScript 或 TypeScript 文件都将成为你的应用程序中的路由。文件名映射到路由的 URL 路径名,除了 _index.tsx,它是 根路由 的索引路由。
app/
├── routes/
│ ├── _index.tsx
│ └── about.tsx
└── root.tsx
| URL | 匹配的路由 |
|---|---|
/ |
app/routes/_index.tsx |
/about |
app/routes/about.tsx |
请注意,由于嵌套路由,这些路由将在 app/root.tsx 的 outlet 中渲染。
在路由文件名中添加 . 将在 URL 中创建一个 /。
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.salt-lake-city.tsx
│ └── concerts.san-diego.tsx
└── root.tsx
| URL | 匹配的路由 |
|---|---|
/ |
app/routes/_index.tsx |
/about |
app/routes/about.tsx |
/concerts/trending |
app/routes/concerts.trending.tsx |
/concerts/salt-lake-city |
app/routes/concerts.salt-lake-city.tsx |
/concerts/san-diego |
app/routes/concerts.san-diego.tsx |
点分隔符还会创建嵌套,有关更多信息,请参见嵌套部分。
通常,你的 URL 不是静态的,而是数据驱动的。动态片段允许你匹配 URL 的片段并在你的代码中使用该值。 你可以使用 $ 前缀创建它们。
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ └── concerts.trending.tsx
└── root.tsx
| URL | 匹配的路由 |
|---|---|
/ |
app/routes/_index.tsx |
/about |
app/routes/about.tsx |
/concerts/trending |
app/routes/concerts.trending.tsx |
/concerts/salt-lake-city |
app/routes/concerts.$city.tsx |
/concerts/san-diego |
app/routes/concerts.$city.tsx |
Remix 将解析 URL 中的值并将其传递给各种 API。 我们将这些值称为“URL 参数”。 访问 URL 参数最有用的地方是在 loaders 和 actions 中。
export async function loader({
params,
}: LoaderFunctionArgs) {
return fakeDb.getAllConcertsForCity(params.city);
}
你会注意到 params 对象上的属性名称直接映射到你的文件名:$city.tsx 变为 params.city。
路由可以有多个动态片段,例如 concerts.$city.$date,它们都可以通过名称在 params 对象上访问
export async function loader({
params,
}: LoaderFunctionArgs) {
return fake.db.getConcerts({
date: params.date,
city: params.city,
});
}
有关更多信息,请参见路由指南。
嵌套路由是将 URL 片段与组件层次结构和数据耦合在一起的一般概念。你可以在路由指南中阅读更多相关信息。
你可以使用点分隔符创建嵌套路由。 如果 . 前的文件名与另一个路由文件名匹配,它将自动成为匹配父路由的子路由。 考虑这些路由
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts._index.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ └── concerts.tsx
└── root.tsx
所有以 app/routes/concerts. 开头的路由都将是 app/routes/concerts.tsx 的子路由,并在父路由的 outlet_component 中渲染。
| URL | 匹配的路由 | 布局 |
|---|---|---|
/ |
app/routes/_index.tsx |
app/root.tsx |
/about |
app/routes/about.tsx |
app/root.tsx |
/concerts |
app/routes/concerts._index.tsx |
app/routes/concerts.tsx |
/concerts/trending |
app/routes/concerts.trending.tsx |
app/routes/concerts.tsx |
/concerts/salt-lake-city |
app/routes/concerts.$city.tsx |
app/routes/concerts.tsx |
请注意,通常当你添加嵌套路由时,你需要添加一个索引路由,以便当用户直接访问父 URL 时,在父路由的 outlet 中渲染一些内容。
例如,如果 URL 为 /concerts/salt-lake-city,则 UI 层次结构将如下所示
<Root>
<Concerts>
<City />
</Concerts>
</Root>
有时你希望 URL 是嵌套的,但你不希望自动进行布局嵌套。 你可以使用父片段上的尾随下划线来选择退出嵌套
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.tsx
│ └── concerts_.mine.tsx
└── root.tsx
| URL | 匹配的路由 | 布局 |
|---|---|---|
/ |
app/routes/_index.tsx |
app/root.tsx |
/about |
app/routes/about.tsx |
app/root.tsx |
/concerts/mine |
app/routes/concerts_.mine.tsx |
app/root.tsx |
/concerts/trending |
app/routes/concerts.trending.tsx |
app/routes/concerts.tsx |
/concerts/salt-lake-city |
app/routes/concerts.$city.tsx |
app/routes/concerts.tsx |
请注意,/concerts/mine 不再与 app/routes/concerts.tsx 嵌套,而是与 app/root.tsx 嵌套。 trailing_ 下划线创建一个路径片段,但它不会创建布局嵌套。
将 trailing_ 下划线视为父级签名末尾的长位,将你从遗嘱中剔除,从而从布局嵌套中删除以下片段。
我们称这些为 无路径路由
有时你希望与一组路由共享布局,而不在 URL 中添加任何路径片段。一个常见的示例是一组具有与公共页面或登录应用程序体验不同的页眉/页脚的身份验证路由。 你可以使用 _leading 下划线来执行此操作。
app/
├── routes/
│ ├── _auth.login.tsx
│ ├── _auth.register.tsx
│ ├── _auth.tsx
│ ├── _index.tsx
│ ├── concerts.$city.tsx
│ └── concerts.tsx
└── root.tsx
| URL | 匹配的路由 | 布局 |
|---|---|---|
/ |
app/routes/_index.tsx |
app/root.tsx |
/login |
app/routes/_auth.login.tsx |
app/routes/_auth.tsx |
/register |
app/routes/_auth.register.tsx |
app/routes/_auth.tsx |
/concerts |
app/routes/concerts.tsx |
app/root.tsx |
/concerts/salt-lake-city |
app/routes/concerts.$city.tsx |
app/routes/concerts.tsx |
将 _leading 下划线视为你正在覆盖文件名的毯子,隐藏 URL 中的文件名。
将路由片段括在括号中将使该片段成为可选的。
app/
├── routes/
│ ├── ($lang)._index.tsx
│ ├── ($lang).$productId.tsx
│ └── ($lang).categories.tsx
└── root.tsx
| URL | 匹配的路由 |
|---|---|
/ |
app/routes/($lang)._index.tsx |
/categories |
app/routes/($lang).categories.tsx |
/en/categories |
app/routes/($lang).categories.tsx |
/fr/categories |
app/routes/($lang).categories.tsx |
/american-flag-speedo |
app/routes/($lang)._index.tsx |
/en/american-flag-speedo |
app/routes/($lang).$productId.tsx |
/fr/american-flag-speedo |
app/routes/($lang).$productId.tsx |
你可能想知道为什么 /american-flag-speedo 与 ($lang)._index.tsx 路由匹配,而不是 ($lang).$productId.tsx。 这是因为当你有一个可选的动态参数片段,后跟另一个动态参数时,Remix 无法可靠地确定诸如 /american-flag-speedo 之类的单片段 URL 应与 /:lang /:productId 匹配。 可选片段会急切地匹配,因此它将与 /:lang 匹配。 如果你有这种类型的设置,建议查看 ($lang)._index.tsx loader 中的 params.lang,如果 params.lang 不是有效的语言代码,则重定向到当前/默认语言的 /:lang/american-flag-speedo。
虽然 动态片段匹配 URL 中的单个路径片段(两个 / 之间的内容),但 splat 路由将匹配 URL 的其余部分,包括斜杠。
app/
├── routes/
│ ├── _index.tsx
│ ├── $.tsx
│ ├── about.tsx
│ └── files.$.tsx
└── root.tsx
| URL | 匹配的路由 |
|---|---|
/ |
app/routes/_index.tsx |
/about |
app/routes/about.tsx |
/beef/and/cheese |
app/routes/$.tsx |
/files |
app/routes/files.$.tsx |
/files/talks/remix-conf_old.pdf |
app/routes/files.$.tsx |
/files/talks/remix-conf_final.pdf |
app/routes/files.$.tsx |
/files/talks/remix-conf-FINAL-MAY_2022.pdf |
app/routes/files.$.tsx |
与动态路由参数类似,你可以使用 "*" 键访问 splat 路由的 params 上匹配的路径的值。
export async function loader({
params,
}: LoaderFunctionArgs) {
const filePath = params["*"];
return fake.getFileInfo(filePath);
}
如果你希望 Remix 用于这些路由约定的特殊字符实际上成为 URL 的一部分,则可以使用 [] 字符来转义约定。
| 文件名 | URL |
|---|---|
app/routes/sitemap[.]xml.tsx |
/sitemap.xml |
app/routes/[sitemap.xml].tsx |
/sitemap.xml |
app/routes/weird-url.[_index].tsx |
/weird-url/_index |
app/routes/dolla-bills-[$].tsx |
/dolla-bills-$ |
app/routes/[[so-weird]].tsx |
/[so-weird] |
路由也可以是文件夹,其中包含一个 route.tsx 文件来定义路由模块。 文件夹中的其余文件不会成为路由。 这允许你将代码组织得更靠近使用它们的路由,而不是在其他文件夹中重复功能名称。
考虑这些路由
app/
├── routes/
│ ├── _landing._index.tsx
│ ├── _landing.about.tsx
│ ├── _landing.tsx
│ ├── app._index.tsx
│ ├── app.projects.tsx
│ ├── app.tsx
│ └── app_.projects.$id.roadmap.tsx
└── root.tsx
它们中的一些或全部可以是保存自己 route 模块的文件夹。
app/
├── routes/
│ ├── _landing._index/
│ │ ├── route.tsx
│ │ └── scroll-experience.tsx
│ ├── _landing.about/
│ │ ├── employee-profile-card.tsx
│ │ ├── get-employee-data.server.ts
│ │ ├── route.tsx
│ │ └── team-photo.jpg
│ ├── _landing/
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ └── route.tsx
│ ├── app._index/
│ │ ├── route.tsx
│ │ └── stats.tsx
│ ├── app.projects/
│ │ ├── get-projects.server.ts
│ │ ├── project-buttons.tsx
│ │ ├── project-card.tsx
│ │ └── route.tsx
│ ├── app/
│ │ ├── footer.tsx
│ │ ├── primary-nav.tsx
│ │ └── route.tsx
│ ├── app_.projects.$id.roadmap/
│ │ ├── chart.tsx
│ │ ├── route.tsx
│ │ └── update-timeline.server.ts
│ └── contact-us.tsx
└── root.tsx
请注意,当你将路由模块转换为文件夹时,路由模块将变为 folder/route.tsx,该文件夹中的所有其他模块都不会成为路由。例如
# these are the same route:
app/routes/app.tsx
app/routes/app/route.tsx
# as are these
app/routes/app._index.tsx
app/routes/app._index/route.tsx
我们关于缩放的一般建议是使每个路由都成为一个文件夹,并将该路由专门使用的模块放在该文件夹中,然后将共享模块放在 routes 文件夹之外的其他位置。 这有几个好处