高级路由

404 页面处理

任何用户都有可能访问您网站上不存在的 URL。例如,如果用户访问 https://example.com/does-not-exist,那么服务器应该以 404 HTTP 状态码 响应,并且页面应该至少有一些解释,而不仅仅是一个空白页面。

对于任何给定的路由,Qwik City 可以选择如何最好地处理用户的 404 响应,无论是使用默认的 404 页面、自定义的 404 页面还是动态生成的 404 页面。

默认的 404 页面

默认情况下,Qwik City 为未处理的任何路由提供了一个通用的 404 页面,而不是显示一个空白页面。当找不到自定义的 404 页面时,将呈现默认的 404 页面。Qwik City 的默认页面还可以,但我们建议提供一个自定义的 404 页面,以便用户可以获得更好的体验,例如提供常见的页眉和导航,以便用户可以找到他们正在寻找的页面。

自定义的 404 页面

可以使用与站点的其余部分相同的熟悉布局来提供自定义的 404 页面,而不是显示通用(无聊)的 404 响应。

要创建自定义的 404 页面,请将 404.tsx 文件添加到根目录的 src/routes 目录中。

src/
└── routes/
    ├── 404.tsx            # 自定义 404 页面
    ├── layout.tsx         # 默认布局
    └── index.tsx          # https://example.com/

在上面的示例中,404.tsx 页面也将使用 layout.tsx 布局,因为它是与布局位于同一目录中的同级文件。

此外,使用 Qwik City 的基于目录的路由允许在不同路径上创建自定义的 404 页面。例如,如果还将 src/routes/account/404.tsx 添加到结构中,则自定义的帐户 404 页面仅适用于 /account/* 路由,而所有其他 404 页面将使用根目录的 404.tsx 页面。

注意:在开发和预览模式下,自定义的 404 页面不会被呈现,而是显示默认的 Qwik City 404 页面。然而,在构建用于生产的应用程序时,自定义的 404 页面将作为静态的 404.html 文件静态生成。

src/
└── routes/
    ├── account/
       └── 404.tsx        # 自定义帐户 404 页面
       └── index.tsx      # https://example.com/account/
    ├── 404.tsx            # 自定义 404 页面
    ├── layout.tsx         # 默认布局
    └── index.tsx          # https://example.com/

值得注意的是,自定义的 404 页面在构建时静态生成为静态的 404.html 文件,而不是单独的服务器端渲染页面。这种策略减轻了 HTTP 服务器的负载,避免了对 404 页面的服务器端渲染,从而保留了资源。

动态 404 页面

在渲染页面时,默认情况下始终使用 200 HTTP 状态码 响应,告诉浏览器一切正常,路由存在。然而,也可以处理渲染页面,但手动将响应状态码设置为其他值,例如 404。

例如,假设我们有一个产品页面,URL 类似于 https://example.com/product/abc。产品页面将使用 src/routes/product/[id]/index.tsx 基于目录的路由进行处理,而 [id] 是 URL 中的动态参数。

在此示例中,id 用作从数据库加载产品数据的键。当找到产品数据时,很好,我们将正确地渲染数据。然而,如果数据库中找不到产品数据,我们仍然可以处理渲染此页面,但是响应一个 404 HTTP 状态码。

import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
 
export const useProductLoader = routeLoader$(async ({ params, status }) => {
  // 使用 id 参数进行示例数据库调用
  // 如果找不到产品,则数据库可能返回 null
  const data = await productDatabase.get(params.id);
 
  if (!data) {
    // 未找到产品数据
    // 将状态码设置为 404
    status(404);
  }
 
  // 返回数据(可能为 null)
  return data;
});
 
export default component$(() => {
  // 从加载器中获取产品数据
  const product = useProductLoader();
 
  if (!product.value) {
    // 没有找到产品数据
    // 因此渲染我们自己的自定义产品 404 页面
    return <p>抱歉,看起来我们没有这个产品。</p>;
  }
 
  // 找到了产品数据,因此渲染它
  return (
    <div>
      <h1>{product.value.name}</h1>
      <p>{product.value.price}</p>
      <p>{product.value.description}</p>
    </div>
  );
});

分组布局

通常,常见的路由会被放置在目录中,以便它们可以共享布局,并且相关的源文件可以逻辑上归为一组。但是,可能希望将用于分组类似文件和共享布局的目录从公开的 URL 中排除。这就是“分组”布局(也称为“无路径”布局路由)的用途。

通过使用括号将任何目录名称括起来,例如 (name),则目录名称本身将不包含在 URL 路径中。

例如,假设一个应用程序将所有 account 路由放在一个目录中,但是 /account/ 可以从 URL 中删除,以获得更简洁、更短的 URL。在下面的示例中,请注意路径位于 src/routes/(account)/ 目录中。但是,URL 路径不包括 (account)/

src/
└── routes/
    └── (account)/             # 注意括号
        ├── layout.tsx         # 共享的帐户布局
        └── profile/
            └── index.tsx      # https://example.com/profile
        └── settings/
            └── index.tsx      # https://example.com/settings

命名布局

有时,相关路由需要与其兄弟路由有明显不同的布局。可以为不同的兄弟路由定义多个布局。一个默认布局和任意数量的命名布局。然后,子路由可以请求特定的命名布局。

Qwik City 定义了布局位于 src/routes 中,并且文件名以 layout 开头的约定。这就是为什么默认布局被命名为 layout.tsx。命名布局也以 layout 开头,后面跟着一个破折号 - 和一个唯一的名称,例如 layout-narrow.tsx

要引用命名布局,路由的 index.tsx 文件必须以 @<name> 结尾。例如,index@narrow.tsx 将使用 layout-narrow.tsx 布局。

src/
└── routes/
    ├── contact/
       └── index@narrow.tsx      # https://example.com/contact(布局:layout-narrow.tsx)
    ├── layout.tsx                # 默认布局
    ├── layout-narrow.tsx         # 命名布局
    └── index.tsx                 # https://example.com/(布局:layout.tsx)
  • https://example.com/
    ┌──────────────────────────────────────────────────┐
    │       src/routes/layout.tsx                      │
    │  ┌────────────────────────────────────────────┐  │
    │  │    src/routes/index.tsx                    │  │
    │  │                                            │  │
    │  └────────────────────────────────────────────┘  │
    │                                                  │
    └──────────────────────────────────────────────────┘
  • https://example.com/contact
    ┌──────────────────────────────────────────────────┐
    │       src/routes/layout-narrow.tsx               │
    │  ┌────────────────────────────────────────────┐  │
    │  │    src/routes/contact/index@narrow.tsx     │  │
    │  │                                            │  │
    │  └────────────────────────────────────────────┘  │
    │                                                  │
    └──────────────────────────────────────────────────┘

嵌套布局

大多数情况下,希望将布局嵌套在彼此之中。页面的内容可以嵌套在多个包装布局中,这由目录结构决定。

src/
└── routes/
    ├── layout.tsx           # 父布局
    └── about/
        ├── layout.tsx       # 子布局
        └── index.tsx        # https://example.com/about

在上面的示例中,有两个布局应用于 /about 页面组件。

  1. src/routes/layout.tsx
  2. src/routes/about/layout.tsx

在这种情况下,布局将相互嵌套,页面包含在其中。

┌────────────────────────────────────────────────┐
│       src/routes/layout.tsx                    │
│  ┌──────────────────────────────────────────┐  │
│  │    src/routes/about/layout.tsx           │  │
│  │  ┌────────────────────────────────────┐  │  │
│  │  │ src/routes/about/index.tsx         │  │  │
│  │  │                                    │  │  │
│  │  └────────────────────────────────────┘  │  │
│  │                                          │  │
│  └──────────────────────────────────────────┘  │
│                                                │
└────────────────────────────────────────────────┘
src/routes/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <main>
      <Slot /> {/* <== 在此处插入子布局/路由 */}
    </main>
  );
});
src/routes/about/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <section>
      <Slot /> {/* <== 在此处插入子布局/路由 */}
    </section>
  );
});
src/routes/about/index.tsx
import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <h1>About</h1>;
});

上面的示例将呈现以下 HTML:

<main>
  <section>
    <h1>About</h1>
  </section>
</main>

plugin.ts

可以在 src/routes 目录的根目录中创建 plugin.tsplugin@<name>.ts 文件,以在根布局执行之前处理任何传入的请求。

可以有多个 plugin.ts 文件,每个文件都有不同的名称。例如,plugin@auth.tsplugin@security.ts@<name> 是可选的,仅用于开发人员帮助识别插件。

请求处理程序(如 onRequestonGetonPost 等)在执行 server$ 函数之前被调用。

plugin.ts 文件的执行顺序

如果存在 plugin.ts 文件,并且它导出了请求处理程序,则它们首先被执行。

然后按照文件名的字母顺序执行 plugin@<name>.ts 文件中导出的请求处理程序。例如,plugin@auth.ts 中的 onGetplugin@security.ts 中的 onGet 之前执行,因为 auth 在字母顺序上位于 security 之前。

最后,如果存在 server$ 函数,则最后执行它。

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • manucorporat
  • adamdbradley
  • cunzaizhuyi
  • the-r3aper7
  • mhevery
  • jakovljevic-mladen
  • hamatoyogi