路由

Qwik City 中的路由與 Next.jsSvelteKitSolidStartRemix 一樣,都基於檔案系統。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            # 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/admin,將按以下順序執行 onRequest 方法

  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,直到頁面完全呈現完畢。

默認情況下,當用戶將滑鼠懸停在 UI 中的對應連結上時,Link 元件將會開始預先擷取下一頁。因此,如果應用程式在使用者點擊連結時已完成預先擷取,則下一頁將會立即顯示。儘管 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:設定 快取控制 回應標頭的便捷函式。
  • cookie:HTTP 請求和回應 Cookie。使用 get() 方法取得請求 Cookie 值。使用 set() 方法設定回應 Cookie 值。
  • env:平台提供的環境變數。
  • error:呼叫時,回應將立即以給定的狀態碼結束。這對於使用 404 結束回應並使用 routes 目錄中的 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 路徑參數。使用 query 來改為取得查詢字串搜尋參數。
  • parseBody:此方法將檢查請求標頭中是否有 Content-Type 標頭,並據此解析正文。它支援 application/jsonapplication/x-www-form-urlencodedmultipart/form-data 內容類型。如果未設定 Content-Type 標頭,它將返回 null
  • pathname:URL 路徑名稱值。不包含通訊協定、網域、查詢字串(搜尋參數)或雜湊。
  • platform:平台特定的資料和函式。
  • query:URL 查詢字串 URLSearchParams 值。使用 params 來改為取得在網址路徑名稱中找到的路徑參數。
  • 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 或以不同語言翻譯頁面可能很有用。

使用前綴翻譯本地化網址

出於本地化的目的,您可能希望將路由從 /products 翻譯為 /it/prodotti/fr/produits,並將 /products/product-name 翻譯為 /it/prodotti/nome-prodotto/fr/produits/nom-du-produit,而無需為每個地區設定多個路由文件,而是重複使用相同的頁面組件、佈局、中介軟體等。

參數名稱不會更改,因此如果路由文件是 /products/[slug]/index.tsx 且網址是 /products/product-name/it/prodotti/nome-prodotto/fr/produits/nom-du-produit,您將收到具有值 product-namenome-prodottonom-du-produit 的相同路徑參數 slug

重寫沒有前綴的網址

這很罕見,但您可能希望為同一路徑設置別名。例如,您可能希望 /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