路由
Qwik City 中的路由是基于文件系统的,类似于 Next.js、SvelteKit、SolidStart 或 Remix。src/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 # 此布局用于所有页面
[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]
。
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.tsx
或 index.ts
导出一个 Qwik 组件作为默认导出时,Qwik City 将渲染该组件并返回一个 HTML 响应作为网页。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <h1>Hello World</h1>;
});
index.ts
端点 index.ts
也可以直接访问 HTTP 请求并返回一个原始的 HTTP 响应,而不涉及任何 Qwik 组件。通过导出一个 onRequest
方法或 onGet
、onPost
、onPut
、onDelete
方法来实现。
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 中所看到的,页面和端点之间没有明确的分离,两者都是导出一个 Qwik 组件或一个 onRequest
方法的 index.tsx
文件。但是,可以将这两种方法结合起来使用。例如,您可以导出一个 onRequest
方法来处理请求,然后渲染一个 Qwik 组件。
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 <-- 此布局用于 /admin/* 下的所有页面
│ └── index.tsx
├── layout.tsx <-- 此布局用于所有页面
└── index.tsx
中间件布局
由于布局可以使用 onRequest
或 onGet
、onPost
、onPut
、onDelete
实现请求处理,因此它们可以用于实现中间件,例如,在渲染页面之前验证请求的 cookie。
对于路由 https://example.com/admin
,onRequest
方法将按以下顺序执行:
src/routes/layout.tsx
的onRequest
src/routes/admin/layout.tsx
的onRequest
src/routes/admin/index.tsx
的onRequest
src/routes/admin/index.tsx
的组件
在 src/routes/index.tsx
中的 onRequest
处理程序不会被执行。
嵌套布局
布局还提供了一种向呈现的页面添加共享 UI的方法。例如,如果要为所有路由添加一个常见的页眉,可以将 Header
组件添加到根布局中。
对于给定的示例,Qwik 组件将按以下顺序呈现:
src/routes/layout.tsx
的组件src/routes/admin/layout.tsx
的组件src/routes/admin/index.tsx
的组件
<RootLayout>
<AdminLayout>
<AdminPage />
</AdminLayout>
</RootLayout>
SPA 导航
使用 Qwik,MPA 和 SPA 之间的区别消失了;每个应用程序可以同时是 MPA 和 SPA。every app can be both at the same time. The choice is no longer an architectural design determined at the beginning of the project, instead, this decision can be made for every link.
Qwik 可以使用 <Link>
组件和 useNavigate()
钩子来发起 SPA 刷新或页面之间的导航。
These can be used to initiate an SPA refresh or navigation between pages.
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(首选)</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$(() => {
// 这将在页面刷新时在服务器上重新执行。
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
组件的 prefetch
属性可用于提高应用程序的感知性能。尽管 Qwik 页面在延迟加载 JavaScript 方面表现出色,但这个功能对于内容密集的页面或需要等待数据库或 API 调用的 SSR 页面非常有用。
<Link prefetch href="/about">About</Link>
只需添加 prefetch
属性,您的 Link
组件将在用户悬停在链接上时立即开始预取页面。如果应用程序在用户单击链接时完成预取,下一个页面将立即显示。
滚动恢复 - need review
Qwik 提供了最佳的滚动恢复功能,几乎与原生浏览器体验相同。 您的用户应该获得与 MPA 中原生体验完全相同的体验, 只是多了 SPA 的所有附加优势。
在使用上述任何一种方法导航后,用户将自动升级为 SPA。 这意味着当前页面和他们来自的页面现在都与 SPA 上下文相关联。
如果用户随后点击常规的 <a>
标签,它们将执行常规导航。这个新页面将没有 SPA 上下文,并且实际上被降级为 MPA。
您可以根据需要在这两者之间切换,用户的体验将在 MPA 和 SPA 之间无缝切换,
就像它们都是相同的一样。
当用户重新访问启用了 SPA 的历史记录条目时,例如刷新、后退/前进按钮、浏览器会话重启等, Qwik 将自动恢复他们的滚动位置,并根据需要将自身引导回 SPA 上下文中。
用于提供这种强大体验的脚本在用户的浏览器中永远不会加载,也永远不会发送给用户的浏览器,除非历史记录条目具有 SPA 上下文。 这就是 Qwik 的魔力所在。
Qwik 的滚动恢复完全基于
history
。这与许多其他框架不同,后者依赖于诸如sessionStorage
等内容。
Qwik 记住并恢复滚动位置的能力非常强大,可以经受从浏览器会话重启到用户清除浏览器数据的一切考验,而这对于许多其他框架来说并非如此。
在 SPA 中使用
pushState()
和replaceState()
的注意事项:
need review.
在具有 SPA 上下文的页面上,Qwik 将在
history
全局对象上修补pushState()
和replaceState()
函数。 这是为了确保您作为开发人员添加的任何自定义状态也接收到 SPA 上下文。在修补这些函数时,您
push
或replace
的状态应始终是一个实际的Object
类型。 这是因为 Qwik 需要能够自动将 SPA 上下文附加到状态作为属性。如果提供的值不是对象,则 Qwik 将为状态创建一个新对象,并将您提供的值添加到一个新的键中:
{ _data: <your_value> }
当发生这种情况时,Qwik 还将在浏览器的控制台中以
dev
模式警告您。
请求事件
每个请求处理程序(例如 onRequest
、onGet
、onPost
等)都作为第一个参数传递给 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-8
。只能调用一次html()
响应。json
:将数据转换为 JSON 字符串并在响应中发送。响应将自动将Content-Type
标头设置为application/json; charset=utf-8
。只能调用一次json()
响应。locale
:内容所在的区域设置。可以使用getLocale()
从选定的方法中检索区域设置值。method
:HTTP 请求的 方法 值。next
:调用下一个请求处理程序。这对于中间件非常有用。params
:从当前 URL 路径段解析出的 URL 路径参数。使用query
来代替检索查询字符串搜索参数。parseBody
:此方法将检查请求标头中的Content-Type
标头并相应地解析请求正文。它支持application/json
、application/x-www-form-urlencoded
和multipart/form-data
内容类型。如果未设置Content-Type
标头,它将返回null
。pathname
:URL 路径名值。不包括协议、域名、查询字符串(搜索参数)或哈希。platform
:特定于平台的数据和函数。query
:URL 查询字符串 URLSearchParams 值。使用params
来代替检索在 URL 路径中找到的路由参数。redirect
:要重定向的 URL。调用时,响应将立即以正确的重定向状态和标头结束。有关应使用哪个状态代码,请参阅重定向。request
:HTTP 请求。send
:发送响应正文。使用send()
时,Content-Type
响应头不会自动设置,必须手动设置。只能调用一次send()
响应。sharedMap
:所有请求处理程序之间共享的共享 Map。每个 HTTP 请求都会获得共享 Map 的新实例。共享 Map 对于在请求处理程序之间共享数据非常有用。status
:HTTP 响应的 状态代码。在调用时设置状态代码。始终返回状态代码,因此可以使用不带参数调用status()
来返回当前状态代码。text
:发送文本响应的便捷方法。响应将自动将Content-Type
标头设置为text/plain; charset=utf-8
。只能调用一次text()
响应。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-name
、nome-prodotto
或nom-du-produit
。
重写没有前缀的 URL:
这很少见,但您可能希望为相同路径具有别名。 例如,您可能希望
/docs
和/documents
都从同一个页面组件呈现, 或者您可能希望将/products
翻译为/prodotti
而不添加/it
前缀。
对于 routes 下的每个文件夹,如果 pathname 中存在 paths
键的出现,
则将复制路由节点,将所有 paths
键的出现替换为相关的 paths
值。
所有路径参数将保留相同的名称。如果有前缀,它将添加到重写的 pathname 的开头。
您可以在 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 还支持:
这些将在后面讨论。