¥Route File Naming
虽然你可以通过 "routes" 插件选项 配置路由,但大多数路由都是使用此文件系统约定创建的。添加文件,获取路由。
¥While you can configure routes via the "routes" plugin option, most routes are created with this file system convention. Add a file, get a route.
请注意,你可以使用 .js
、.jsx
、.ts
或 .tsx
文件扩展名。为了避免重复,我们将在示例中使用 .tsx
。
¥Please note that you can use either .js
, .jsx
, .ts
or .tsx
file extensions. We'll stick with .tsx
in the examples to avoid duplication.
¥Disclaimer
不过,在深入探讨 Remix 的惯例之前,我们想指出基于文件的路由是一个非常主观的想法。有些人喜欢 "flat" 路由的想法,有些人讨厌它,更喜欢将路由嵌套在文件夹中。有些人只是讨厌基于文件的路由,更喜欢通过 JSON 配置路由。有些人更喜欢通过 JSX 配置路由,就像他们在 React Router SPA 中所做的那样。
¥Before we go too far into the Remix convention, though, we'd like to point out that file-based routing is an incredibly subjective idea. Some folks love the "flat" routes idea, some folks hate it and would prefer nesting routes in folders. Some folks simply hate file-based routing and would prefer to configure routes via JSON. Some folks would prefer to configure routes via JSX like they did in their React Router SPA's.
重点是,我们非常清楚这一点,并且从一开始,Remix 就始终为你提供一流的退出方式,即通过 routes
/ignoredRouteFiles
和 手动配置路由。但是,必须有一些默认设置,以便人们能够快速轻松地启动和运行。 - 我们认为下面的扁平路由约定文档是一个非常好的默认约定,可以很好地扩展到中小型应用。
¥The point is, we are well aware of this and from the get-go, Remix has always given you a first-class way to opt-out via the routes
/ignoredRouteFiles
and configure your routes manually. But, there has to be some default so that folks can get up and running quickly and easily - and we think that the flat routes convention document below is a pretty good default that scales well for small-to-medium sized apps.
无论你使用哪种约定,拥有数百或数千条路由的大型应用总是会有些混乱 - 而我们的想法是,通过 routes
配置,你可以构建最适合你的应用/团队的约定。Remix 根本不可能有一个让所有人都满意的默认约定。我们更倾向于为你提供一个相当简单的默认设置,然后让社区构建任意数量的约定供你选择。
¥Large applications with hundreds or thousands of routes will always be a bit chaotic no matter what convention you use — and the idea is that via the routes
config, you get to build exactly the convention that works best for your application/team. It would be quite literally impossible for Remix to have a default convention that made everyone happy. We'd much rather give you a fairly straightforward default and then let the community build any number of conventions you can pick and choose from.
所以,在我们深入探讨 Remix 默认约定的细节之前,如果你觉得我们的默认约定不合你的口味,可以查看以下一些社区替代方案。
¥So, before we dive into the details of the Remix default convention, here are some community alternatives you can check out if you decide that our default is not your cup of tea.
remix-flat-routes
- Remix 默认版本基本上是此软件包的简化版本。作者一直在不断迭代和改进这个软件包,所以如果你喜欢 "监视路由" 的理念,但想要更强大的功能(包括文件和文件夹的混合方法),一定要看看这个。
¥remix-flat-routes
- The Remix default is basically a simplified version of this package. The author has continued to iterate on and evolve this package, so if you generally like the "flat routes" idea but want a bit more power (including a hybrid approach of files and folders), definitely check this one out.
remix-custom-routes
- 如果你想要更多自定义功能,此软件包允许你定义哪些类型的文件应被视为路由。这让你超越了简单的扁平/嵌套概念,可以实现诸如“任何扩展名为 .route.tsx
的文件都是一个路由”之类的功能。
¥remix-custom-routes
- If you want even more customization, this package lets you define that types of files should be treated as routes. This lets you go beyond the simple flat/nested concept and do something such as "any file with an extension of .route.tsx
is a route".
remix-json-routes
- 如果你只想通过配置文件指定路由,那么这就是你的难题 - 为 Remix 路由提供一个 JSON 对象,完全跳过扁平/嵌套的概念。甚至还有一个 JSX 选项。
¥remix-json-routes
- If you just want to specify your routes via a config file, this is your jam — provide Remix a JSON object with your routes and skip the flat/nested concept entirely. There's even a JSX option in there too.
¥Root Route
app/
├── routes/
└── root.tsx
app/root.tsx
中的文件是你的根布局,或者 "根路由"(对于那些发音相同的人,非常抱歉!)。它的工作方式与其他所有路由一样,因此你可以导出 loader
、action
等。
¥The file in app/root.tsx
is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes, so you can export a loader
, action
, etc.
根路由通常如下所示。它作为整个应用的根布局,所有其他路由都将在 <Outlet />
内渲染。
¥The root route typically looks something like this. It serves as the root layout of the entire app, all other routes will render inside the <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>
);
}
¥Basic Routes
app/routes
目录下的任何 JavaScript 或 TypeScript 文件都将成为你应用中的路由。文件名映射到路由的 URL 路径名,除了 _index.tsx
之外,_index.tsx
对应 根路由 的 索引路由。
¥Any JavaScript or TypeScript files in the app/routes
directory will become routes in your application. The filename maps to the route's URL pathname, except for _index.tsx
which is the index route for the root route.
app/
├── routes/
│ ├── _index.tsx
│ └── about.tsx
└── root.tsx
URL | 匹配的路由 |
---|---|
/ |
app/routes/_index.tsx |
/about |
app/routes/about.tsx |
请注意,由于 嵌套路由 的原因,这些路由将在 app/root.tsx
的出口中渲染。
¥Note that these routes will be rendered in the outlet of app/root.tsx
because of nested routing.
¥Dot Delimiters
在路由文件名中添加 .
将在 URL 中创建 /
。
¥Adding a .
to a route filename will create a /
in the 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 |
点分隔符也会创建嵌套,有关更多信息,请参阅 嵌套部分。
¥The dot delimiter also creates nesting, see the nesting section for more information.
¥Dynamic Segments
通常,你的 URL 不是静态的,而是由数据驱动的。动态段允许你匹配 URL 的段并在代码中使用该值。你可以使用 $
前缀创建它们。
¥Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the $
prefix.
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 中。
¥Remix will parse the value from the URL and pass it to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in loaders and actions.
export async function loader({
params,
}: LoaderFunctionArgs) {
return fakeDb.getAllConcertsForCity(params.city);
}
你会注意到 params
对象上的属性名称直接映射到你的文件名称:$city.tsx
变为 params.city
。
¥You'll note the property name on the params
object maps directly to the name of your file: $city.tsx
becomes params.city
.
路由可以包含多个动态片段,例如 concerts.$city.$date
,它们都可以通过名称在 params 对象上访问:
¥Routes can have multiple dynamic segments, like concerts.$city.$date
, both are accessed on the params object by name:
export async function loader({
params,
}: LoaderFunctionArgs) {
return fake.db.getConcerts({
date: params.date,
city: params.city,
});
}
有关更多信息,请参阅 路由指南。
¥See the routing guide for more information.
¥Nested Routes
嵌套路由是将 URL 的各个部分与组件层次结构和数据耦合起来的一般概念。你可以在 路由指南 中阅读更多相关信息。
¥Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the Routing Guide.
你可以使用 点分隔符 创建嵌套路由。如果 .
之前的文件名与另一个路由文件名匹配,它将自动成为匹配父路由的子路由。考虑以下路由:
¥You create nested routes with dot delimiters. If the filename before the .
matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes:
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 中渲染。
¥All the routes that start with app/routes/concerts.
will be child routes of app/routes/concerts.tsx
and render inside the parent route's 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 时,某些内容会在父级的出口内渲染。
¥Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
例如,如果 URL 是 /concerts/salt-lake-city
,则 UI 层次结构将如下所示:
¥For example, if the URL is /concerts/salt-lake-city
then the UI hierarchy will look like this:
<Root>
<Concerts>
<City />
</Concerts>
</Root>
¥Nested URLs without Layout Nesting
有时你希望 URL 嵌套,但不希望布局自动嵌套。你可以选择在父段末尾使用下划线退出嵌套:
¥Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment:
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_
下划线创建路径段,但不会创建布局嵌套。
¥Note that /concerts/mine
does not nest with app/routes/concerts.tsx
anymore, but app/root.tsx
. The trailing_
underscore creates a path segment, but it does not create layout nesting.
把 trailing_
下划线想象成你父级签名末尾的长位,将你从遗嘱中剔除,并从布局嵌套中删除后面的部分。
¥Think of the trailing_
underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting.
¥Nested Layouts without Nested URLs
我们称之为 无路径路由
¥We call these Pathless Routes
有时你想与一组路由共享一个布局,但不在 URL 中添加任何路径段。一个常见示例是一组身份验证路由,其页眉/页脚与公共页面或登录应用体验不同。你可以使用 _leading
下划线执行此操作。
¥Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged-in-app experience. You can do this with a _leading
underscore.
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 中。
¥Think of the _leading
underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
¥Optional Segments
将路由段括在括号中将使该段成为可选的。
¥Wrapping a route segment in parentheses will make the segment optional.
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
加载器中查看 params.lang
,如果 params.lang
不是有效的语言代码,则重定向到当前/默认语言的 /:lang/american-flag-speedo
。
¥You may wonder why /american-flag-speedo
is matching the ($lang)._index.tsx
route instead of ($lang).$productId.tsx
. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as /american-flag-speedo
should match /:lang
/:productId
. Optional segments match eagerly and thus it will match /:lang
. If you have this type of setup, it's recommended to look at params.lang
in the ($lang)._index.tsx
loader and redirect to /:lang/american-flag-speedo
for the current/default language if params.lang
is not a valid language code.
¥Splat Routes
动态段 匹配单个路径段(URL 中两个 /
之间的内容),而 splat 路由将匹配 URL 的其余部分,包括斜杠。
¥While dynamic segments match a single path segment (the stuff between two /
in a URL), a splat route will match the rest of a URL, including the slashes.
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
上匹配路径的值。
¥Similar to dynamic route parameters, you can access the value of the matched path on the splat route's params
with the "*"
key.
export async function loader({
params,
}: LoaderFunctionArgs) {
const filePath = params["*"];
return fake.getFileInfo(filePath);
}
¥Escaping Special Characters
如果你希望 Remix 用于这些路由约定的特殊字符之一成为 URL 的一部分,你可以使用 []
字符转义这些约定。
¥If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with []
characters.
文件名 | 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] |
¥Folders for Organization
路由也可以是文件夹,其中包含定义路由模块的 route.tsx
文件。文件夹中的其余文件不会成为路由。这允许你将代码组织得更接近使用它们的路由,而不是在其他文件夹中重复功能名称。
¥Routes can also be folders with a route.tsx
file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
考虑以下路由:
¥Consider these routes:
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
模块的文件夹。
¥Some, or all of them can be folders holding their own route
module inside.
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
,文件夹中的所有其他模块都不会成为路由。例如:
¥Note that when you turn a route module into a folder, the route module becomes folder/route.tsx
, all other modules in the folder will not become routes. For example:
# 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
¥Scaling
我们关于扩展的一般建议是将每个路由创建一个文件夹,并将该路由专用的模块放在该文件夹中,然后将共享模块放在 routes
文件夹之外的其他位置。这有几个好处:
¥Our general recommendation for scale is to make every route a folder and put the modules used exclusively by that route in the folder, then put the shared modules outside the routes
folder elsewhere. This has a couple of benefits:
易于识别共享模块,因此在更改它们时请谨慎操作。
¥Easy to identify shared modules, so tread lightly when changing them
易于组织和重构特定路由的模块,而无需创建 "文件组织疲劳" 并扰乱应用的其他部分。
¥Easy to organize and refactor the modules for a specific route without creating "file organization fatigue" and cluttering up other parts of the app