路由

Qwik City 中的路由基于文件系统,类似于 Next.jsSvelteKitSolidStartRemixsrc/routes 中的文件和目录在应用程序的路由中起着作用。

  • 📂 目录:通过路由器定义要匹配的 URL 段。
  • 📄 index.tsx/mdx 文件:定义一个 页面
  • 📄 index.ts 文件:定义一个 端点
  • 🖼️ layout.tsx 文件:定义嵌套的 布局 和/或 中间件

基于目录的路由

只有目录名称用于将传入的请求与页面/端点/中间件匹配。

例如,如果您在 src/routes/some/path/index.tsx 中有一个文件,它将映射到 URL 路径 https://example.com/some/path

目录布局
src/
└── routes/
    ├── contact/
       └── index.mdx         # https://example.com/contact
    ├── about/
       └── index.md          # https://example.com/about
    ├── docs/
       └── [id]/
           └── index.ts      # https://example.com/docs/1234
                             # https://example.com/docs/anything
    ├── [...catchall]/
       └── index.tsx         # https://example.com/anything/else/that/didnt/match
    
    └── layout.tsx            # This layout is used for all pages
  • [id] 是一个代表动态路由段的目录,在本例中,id 是可以通过 useLocation().params.id 访问的字符串参数。
  • [...catchall] 是一个代表动态通配路由的目录,在本例中,catchall 是可以通过 useLocation().params.catchall 访问的字符串参数。
  • index.tsx|mdx 文件是页面/端点/中间件。
  • layout.tsx 文件是布局。

动态路由段

使用方括号的特殊命名目录,例如 [paramName][...catchAll],可以用于匹配动态的路由段。

目录布局
src/routes/blog/index.tsx  /blog
src/routes/user/[username]/index.tsx  /user/:username (/user/foo)
src/routes/post/[...all]/index.tsx  /post/* (/post/2020/id/title)
目录布局
src/
└── routes/
    ├── blog/
       └── index.tsx         # https://example.com/blog
    ├── post/
       └── [...all]/
           └── index.tsx     # https://example.com/post/2020/id/title
    └── user/
        └── [username]/
            └── index.tsx     # https://example.com/user/foo

文件夹 [username] 可以是您数据库中的数千个用户中的任何一个。为每个用户创建路由是不切实际的。相反,您需要定义一个路由参数(URL 的一部分),它将用于提取 [username]

src/routes/user/[username]/index.tsx
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
 
export default component$(() => {
  const loc = useLocation();
  return <div>Hello {loc.params.username}!</div>;
});

index 文件

src/routes 目录中,所有名为 index 的文件都被视为页面/端点/中间件,Qwik 支持以下扩展名:.ts.tsx.md.mdx

页面/端点/中间件是路由树的叶节点,即 **处理请求并返回 HTTP 响应的模块**。

页面 index.tsx

当一个 index.tsxindex.ts 文件将 Qwik 组件导出为默认导出时,Qwik City 将渲染组件并返回一个 HTML 响应作为网页。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return <h1>Hello World</h1>;
});

端点 index.ts

一个 index.ts 文件可以直接访问 HTTP 请求并返回一个原始 HTTP 响应,而无需涉及任何 Qwik 组件。这是通过导出以下任何方法来完成的:onRequestonGetonPostonPutonDelete,具体取决于您希望如何处理特定的 HTTP 请求。

src/routes/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = ({ json }) => {
  json(200, { message: 'Hello World' });
};

请注意,在最后一个示例中,没有默认导出。这是因为您不是渲染 Qwik 组件,而是直接处理请求并返回 JSON 响应。这对于实现 RESTful API 或任何其他类型的 HTTP 端点很有用。

页面 + 端点

在 Qwik City 中,页面和端点之间没有明确的界限。一个 index.tsx 文件同时处理两者,并导出一个 Qwik 组件或一个 onRequest 方法。但是,可以将这两种方法结合起来。例如,您可以导出一个 onRequest 方法来处理请求,然后渲染一个 Qwik 组件。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = ({ headers, query, json }) => {
  headers.set('Cache-Control', 'private');
  if (query.get('format') === 'json') {
    json(200, { message: 'Hello World' });
  }
};
 
export default component$(() => {
  return <h1>Hello World</h1>;
});

在本例中,请求处理程序将始终将 Cache-Control 标头设置为 private,并且页面将作为 HTML 页面渲染,但如果请求包含 format=json 查询参数,则端点将返回 JSON 响应。

layout.tsx 文件

布局模块与 index 文件非常相似。两者都可以处理请求并渲染 Qwik 组件。但是,布局旨在像中间件一样工作,允许 **将 UI 和请求处理(中间件)共享** 到一组路由。

通常,不同的页面需要一些通用的请求处理并共享一些 UI。例如,想象一个仪表板网站,其中所有页面都在 /admin/* 目录下。

  • 共享请求处理:在渲染页面之前,需要验证请求 cookie,否则渲染一个空白的 401 页面。
  • 共享 UI:所有页面共享一个通用的标题,显示用户的姓名和个人资料图片。

与其在每个路由中重复相同的代码,不如使用布局来自动重用公共部分。布局还支持向路由添加中间件。

以这个 src/routes 目录为例

目录布局
src/
└── routes/
    ├── admin/
       ├── layout.tsx  <-- This layout is used for all pages under /admin/*
       └── index.tsx
    ├── layout.tsx      <-- This layout is used for all pages
    └── index.tsx

中间件布局

布局可以使用以下任何方法实现请求处理:onRequestonGetonPostonPutonDelete。这意味着它们可以用来实现中间件。例如,它们可以用来在渲染页面之前验证请求 cookie。

对于路由 https://example.com/adminonRequest 方法将按以下顺序执行

  1. src/routes/layout.tsxonRequest
  2. src/routes/admin/layout.tsxonRequest
  3. src/routes/admin/index.tsxonRequest
  4. src/routes/admin/index.tsx 的组件

src/routes/index.tsx 中的 onRequest 处理程序不会执行。

嵌套布局

布局还提供了一种向渲染页面添加通用 UI 的方法。例如,如果您想向所有路由添加一个通用页眉,请将一个页眉组件添加到根布局中。

对于给定的示例,Qwik 组件将按以下顺序渲染

  1. src/routes/layout.tsx 的组件
  2. src/routes/admin/layout.tsx 的组件
  3. src/routes/admin/index.tsx 的组件
<RootLayout>
  <AdminLayout>
    <AdminPage />
  </AdminLayout>
</RootLayout>

SPA 导航

使用 Qwik,MPA 和 SPA 之间的区别消失了;每个应用程序都可以同时是两者。这个选择不再是项目开始时确定的架构设计,而是可以为每个链接做出这个决定。

Qwik 提供了一个 <Link> 组件和 useNavigate() 钩子。这些可以用来启动 SPA 刷新或页面之间的导航。

Link 组件是推荐的导航方式,因为它使用 HTML <a> 标签,这是在页面之间移动的最易访问的方式。但是,如果您需要以编程方式导航,可以使用 useNavigate() 钩子。

import { component$ } from '@builder.io/qwik';
import { Link, useNavigate } from '@builder.io/qwik-city';
 
export default component$(() => {
  const nav = useNavigate();
  return (
    <div>
      <Link href="/about">About (preferred)</Link>
      <button onClick$={() => nav('/about')}>About</button>
    </div>
  );
});

Link 组件在内部使用 useNavigate() 钩子 内部

带有 reload 属性的 Link 组件可以一起使用来刷新当前页面。您也可以从 useNavigate() 钩子中调用 nav() 函数,不带参数。

import { component$ } from '@builder.io/qwik';
import { Link, routeLoader$, useNavigate } from '@builder.io/qwik-city';
 
export const useServerTime = routeLoader$(() => {
  // This will re-execute in the server when the page refreshes.
  return Date.now();
});
 
export default component$(() => {
  const nav = useNavigate();
  const serverTime = useServerTime();
 
  return (
    <div>
      <Link reload>Refresh (better accessibility)</Link>
      <button onClick$={() => nav()}>Refresh</button>
      <p>Server time: {serverTime.value}</p>
    </div>
  );
});

当页面刷新时,所有匹配的 routeLoader$ 和服务器处理程序 (onRequest) 将在服务器中重新执行,并且 UI 将相应地重新渲染。

在刷新页面时,useLocation() 中的 isNavigating 布尔值将为 true,直到页面完全渲染。

默认情况下,Link 组件将在用户将鼠标悬停在 UI 中的相应链接上时立即开始预取下一页。因此,如果应用程序在用户单击链接时完成了预取,下一页将立即出现。虽然 Qwik 应用程序在延迟加载 javascript 方面已经非常出色,但这种行为对于内容丰富的页面或需要等待数据库或 API 调用的 SSR 页面非常有用。

如果这不是您想要的行为,可以将 prefetch 属性设置为 false。

 <Link prefetch={false} href="/about">About</Link>

滚动恢复

Qwik 为 SPA 提供了最佳的滚动恢复功能,它与本机浏览器体验非常相似。您的用户应该获得与他们从 MPA 中获得的完全相同的体验,只是增加了 SPA 的所有额外好处。

在您使用上述方法之一进行导航后,用户会自动升级到 SPA。这意味着当前页面以及他们来自的页面现在都与一个 SPA 上下文相关联。

如果用户随后单击一个普通的 <a> 标签,他们将执行一个常规的导航。这个新页面将没有 SPA 上下文,并且实际上会降级回 MPA。您可以根据需要在这些之间切换,用户的体验将在 MPA 和 SPA 之间无缝切换,就好像它们都是一样的。

当用户重新访问已启用 SPA 的历史记录条目时,例如通过刷新、后退/前进按钮、浏览器会话重新启动等,Qwik 将自动恢复其滚动位置并将自身引导回 SPA 上下文(如果需要)。

提供这种强大体验所需的脚本永远不会加载,甚至不会发送到用户的浏览器,除非历史记录条目具有 SPA 上下文。纯 MPA 页面永远不会加载此脚本。这就是 Qwik 的魔力。

Qwik 中的滚动恢复始终与渲染同步发生。当与 Qwik 的可恢复和一流的 SSR/MPA 特性相结合时,用户永远不会遇到滚动闪烁。

Qwik 的滚动恢复完全基于 history。这与许多其他依赖于 sessionStorage 等事物的框架不同。

Qwik 记住和恢复滚动位置的能力非常强大,它可以承受从浏览器会话重新启动到用户清除浏览器数据的所有情况,而许多其他框架则做不到这一点。

关于在 SPA 期间使用 pushState()replaceState() 的说明

在具有 SPA 上下文的页面上,Qwik 将修补全局 history 上的 pushState()replaceState() 函数。这是为了确保您作为开发人员添加的任何自定义状态也接收 SPA 上下文。

虽然这些函数被修补了,但您 pushreplace 的状态始终应该是实际的 Object 类型。这是因为 Qwik 需要能够自动将 SPA 上下文作为属性附加到状态。

如果您提供的值不是对象,Qwik 将为状态创建一个新对象,并将您提供的值添加到一个新键:{ _data: <your_value> }

当发生这种情况时,Qwik 还会在 dev 模式下在浏览器的控制台中向您发出警告。

请求事件

每个请求处理程序,例如 onRequestonGetonPost 等,都会在处理程序的第一个参数中传递一个 RequestEvent 对象。RequestEvent 对象包含实用函数和属性,用于获取和设置服务器请求和响应的值。此对象包含以下属性

  • basePathname:请求的基路径名,可以在构建时配置。默认为 /
  • cacheControl:用于设置 Cache-Control 响应头的便捷函数。
  • cookie:HTTP 请求和响应 cookie。使用 get() 方法检索请求 cookie 值。使用 set() 方法设置响应 cookie 值。
  • env:平台提供的环境变量。
  • error:当被调用时,响应将立即以给定的状态代码结束。这对于以 404 结束响应并使用路由目录中的 404 处理程序可能很有用。有关应使用哪个状态代码,请参见 状态代码
  • getWritableStream:用于写入 HTTP 响应流的低级访问权限。一旦调用 getWritableStream(),状态和标头将不再能够修改,并将通过网络发送。
  • headers:HTTP 响应标头
  • html:用于发送 HTML 主体响应的便捷方法。响应将自动将 Content-Type 标头设置为 text/html; charset=utf-8html() 响应只能调用一次。
  • json:用于将数据 JSON 字符串化并将其发送到响应中的便捷方法。响应将自动将 Content-Type 标头设置为 application/json; charset=utf-8json() 响应只能调用一次。
  • locale:内容所在的语言环境。语言环境值可以从使用 getLocale() 的选定方法中检索。
  • method:HTTP 请求 方法 值。
  • next:调用下一个请求处理程序。这对于中间件很有用。
  • params:已从当前 url 路径名段解析的 URL 路径参数。使用 query 来检索查询字符串搜索参数。
  • parseBody:此方法将检查请求标头以获取 Content-Type 标头并相应地解析主体。它支持 application/jsonapplication/x-www-form-urlencodedmultipart/form-data 内容类型。如果未设置 Content-Type 标头,它将返回 null
  • pathname:URL 路径名值。不包括协议、域、查询字符串(搜索参数)或哈希。
  • platform:平台特定数据和函数。
  • query:URL 查询字符串 URLSearchParams 值。使用 params 来检索在 url 路径名中找到的路由参数。
  • redirect:要重定向到的 URL。当被调用时,响应将立即以正确的重定向状态和标头结束。有关应使用哪个状态代码,请参见 重定向
  • request:HTTP 请求
  • send:发送主体响应。使用 send() 时,Content-Type 响应标头不会自动设置,必须手动设置。send() 响应只能调用一次。
  • sharedMap:所有请求处理程序之间共享的映射。每个 HTTP 请求都将获得一个新的共享映射实例。共享映射对于在请求处理程序之间共享数据很有用。
  • status:HTTP 响应 状态代码。在使用参数调用时设置状态代码。始终返回状态代码,因此在不带参数的情况下调用 status() 可用于返回当前状态代码。
  • text:用于发送文本主体响应的便捷方法。响应将自动将 Content-Type 标头设置为 text/plain; charset=utf-8text() 响应只能调用一次。
  • url:HTTP 请求 URL

重写路由

您可以重写路径名,以便使用一个页面组件及其自己的中间件和布局来重用多个页面。这对于 SEO 目的或将页面翻译成不同的语言可能很有用。

使用前缀翻译本地化 URL

出于本地化目的,您可能希望将路由从 /products 翻译为 /it/prodotti/fr/produits,并将 /products/product-name 翻译为 /it/prodotti/nome-prodotto/fr/produits/nom-du-produit,而无需为每个语言环境创建多个路由文件,而是重用相同的页面组件、布局、中间件等。

参数名称不会更改,因此如果路由文件是 /products/[slug]/index.tsx 并且 URL 是 /products/product-name/it/prodotti/nome-prodotto/fr/produits/nom-du-produit,您将收到相同的路径参数 slug,其值为 product-namenome-prodottonom-du-produit

重写没有前缀的 URL

这种情况很少见,但您可能希望为同一路径创建别名。例如,您可能希望 /docs/documents 都从同一个页面组件渲染,或者您可能希望将 /products 翻译为 /prodotti,而无需添加 /it 前缀。

在路由目录中包含路径键实例的每个文件夹中,路由节点将被复制,并且所有路径键的出现都将被替换为其相应的路径值。所有路径参数将保留相同的名称。如果存在前缀,它将添加到重写路径名的开头。

您可以在 vite.config.ts 中按如下方式设置重写规则

import { defineConfig } from 'vite';
import { qwikCity } from '@builder.io/qwik-city/vite';
 
export default defineConfig(async () => {
  return {
    plugins: [
      qwikCity({
        rewriteRoutes: [
            {
              paths: {
                  'docs': 'documentation'
              },
            },
            {
              prefix: 'it',
              paths: {
                'docs': 'documentazione',
                'getting-started': 'per-iniziare',
                'products': 'prodotti',
              },
            },
          ],
      }),
    ],
  };
});

高级路由

Qwik City 还支持

类型安全路由

这些将在后面讨论。

贡献者

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

  • manucorporat
  • nnelgxorz
  • the-r3aper7
  • Oyemade
  • mhevery
  • adamdbradley
  • wtlin1228
  • AnthonyPAlicea
  • hamatoyogi
  • jakovljevic-mladen
  • claudioshiver
  • maiieul
  • igorbabko
  • jordanw66
  • mrhoodz
  • chsanch
  • RumNCodeDev