高级路由

404 页面处理

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

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

默认 404 页面

为了避免显示空白页面,Qwik City 默认情况下会为任何未处理的路由提供一个通用的 404 页面。当未找到自定义 404 页面时,默认的 404 页面将作为回退进行渲染。我们建议提供自定义 404 页面以获得更好的用户体验。在自定义 404 页面中包含常见的页眉和导航将有助于用户找到他们要查找的页面。

自定义 404 页面

除了显示通用的(无聊的)404 响应之外,还可以使用与网站其他部分相同的熟悉布局来提供自定义 404 页面。

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

src/
└── routes/
    ├── 404.tsx            # Custom 404
    ├── layout.tsx         # Default layout
    └── 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        # Custom Account 404
       └── index.tsx      # https://example.com/account/
    ├── 404.tsx            # Custom 404
    ├── layout.tsx         # Default layout
    └── index.tsx          # https://example.com/

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

动态 404 页面

渲染页面时,默认情况下始终返回 200 HTTP 状态码,这告诉浏览器一切正常并且路由存在。但是,也可以处理页面的渲染,但手动将响应状态码设置为 200 以外的值,例如 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 }) => {
  // Example database call using the id param
  // The database could return null if the product is not found
  const data = await productDatabase.get(params.id);
 
  if (!data) {
    // Product data was not found
    // Set the status code to 404
    status(404);
  }
 
  // return the data (which may be null)
  return data;
});
 
export default component$(() => {
  // get the product data from the loader
  const product = useProductLoader();
 
  if (!product.value) {
    // no product data found
    // so render our own custom product 404
    return <p>Sorry, looks like we don't have this product.</p>;
  }
 
  // product data was found, so let's render it
  return (
    <div>
      <h1>{product.value.name}</h1>
      <p>{product.value.price}</p>
      <p>{product.value.description}</p>
    </div>
  );
});

分组布局

通用目的路由通常被放置在目录中,以便它们可以共享布局,并且相关的源文件在逻辑上分组在一起。但是,可能需要将用于分组类似文件和共享布局的目录从面向公众的 URL 中排除。这就是“分组”布局的用武之地(也称为“无路径”布局路由)。

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

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

src/
└── routes/
    └── (account)/             # Notice the parentheses
        ├── layout.tsx         # Shared account layout
        └── 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> 为后缀。例如,[email protected] 将使用 layout-narrow.tsx 布局。

src/
└── routes/
    ├── contact/
       └── [email protected]      # https://example.com/contact (Layout: layout-narrow.tsx)
    ├── layout.tsx                # Default layout
    ├── layout-narrow.tsx         # Named layout
    └── index.tsx                 # https://example.com/ (Layout: 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/[email protected]     │  │
    │  │                                            │  │
    │  └────────────────────────────────────────────┘  │
    │                                                  │
    └──────────────────────────────────────────────────┘

嵌套布局

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

src/
└── routes/
    ├── layout.tsx           # Parent layout
    └── about/
        ├── layout.tsx       # Child layout
        └── 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 /> {/* <== Child layout/route inserted here */}
    </main>
  );
});
src/routes/about/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <section>
      <Slot /> {/* <== Child layout/route inserted here */}
    </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@<name>.ts 的插件

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

您可以拥有多个plugin.ts文件,每个文件都有不同的名称。例如,[email protected][email protected]@<name> 是可选的,它仅用于开发人员帮助识别插件。

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

plugin.ts 文件的执行顺序

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

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

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

贡献者

感谢所有帮助改进本文档的贡献者!

  • manucorporat
  • adamdbradley
  • cunzaizhuyi
  • the-r3aper7
  • mhevery
  • jakovljevic-mladen
  • vfshera
  • thejackshelton
  • wtlin1228
  • hamatoyogi
  • jemsco
  • patrickjs