动态

详情 返回 返回

SvelteKit 最新中文文檔教程(2)—— 路由 - 动态 详情

前言

Svelte,一個語法簡潔、入門容易,面向未來的前端框架。

從 Svelte 誕生之初,就備受開發者的喜愛,根據統計,從 2019 年到 2024 年,連續 6 年一直是開發者最感興趣的前端框架 No.1

image.png

Svelte 以其獨特的編譯時優化機制著稱,具有輕量級高性能易上手等特性,非常適合構建輕量級 Web 項目

為了幫助大家學習 Svelte,我同時搭建了 Svelte 最新的中文文檔站點。

如果需要進階學習,也可以入手我的小冊《Svelte 開發指南》,語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

路由

SvelteKit 的核心是一個基於文件系統的路由器。應用程序的路由(即用户可以訪問的 URL 路徑)由代碼庫中的目錄定義:

  • src/routes 是根路由
  • src/routes/about 創建一個 /about 路由
  • src/routes/blog/[slug] 創建一個帶有參數 slug 的路由,當用户請求類似 /blog/hello-world 的頁面時,可以用它動態加載數據
[!NOTE] 您可以通過編輯項目配置來將 src/routes 更改為其他目錄。

每個路由目錄包含一個或多個路由文件,這些文件可以通過它們的 + 前綴識別。

我們稍後會更詳細地介紹這些文件,但這裏有幾個簡單的規則可以幫助您記住 SvelteKit 的路由是如何工作的:

  • 所有文件都可以在服務端上運行
  • 除了 +server 文件外,所有文件都在客户端運行
  • +layout+error 文件不僅適用於它們所在的目錄,也適用於子目錄

+page

+page.svelte

+page.svelte 組件定義了您應用程序的一個頁面。默認情況下,頁面在初始請求時在服務端渲染(SSR),在後續導航時在瀏覽器中渲染(CSR)。

<!--- file: src/routes/+page.svelte --->
<h1>您好,歡迎來到我的網站!</h1>
<a href="/about">關於我的網站</a>
<!--- file: src/routes/about/+page.svelte --->
<h1>關於本站</h1>
<p>待辦...</p>
<a href="/">首頁</a>

頁面可以通過 data 屬性接收來自 load 函數的數據。

<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
    /** @type {{ data: import('./$types').PageData }} */
    let { data } = $props();
</script>

<h1>{data.title}</h1>
<div>{@html data.content}</div>

[!遺留模式]
在 Svelte 4 中,您需要使用 export let data 代替

[!NOTE] SvelteKit 使用 <a> 元素在路由之間導航,而不是框架特定的 <Link> 組件。

+page.js

通常,頁面在渲染之前需要加載一些數據。為此,我們添加一個 +page.js 模塊,該模塊導出一個 load 函數:

/// file: src/routes/blog/[slug]/+page.js
import { error } from '@sveltejs/kit';

/** @type {import('./$types').PageLoad} */
export function load({ params }) {
    if (params.slug === 'hello-world') {
        return {
            title: 'Hello world!',
            content: 'Welcome to our blog. Lorem ipsum dolor sit amet...'
        };
    }

    error(404, 'Not found');
}

這個函數與 +page.svelte 一起運行,這意味着它在服器端渲染期間在服務端上運行,在客户端導航期間在瀏覽器中運行。有關該 API 的完整詳細信息,請參見 load

除了 load+page.js 還可以導出一些值用於配置頁面行為:

  • export const prerender = truefalse'auto'
  • export const ssr = truefalse
  • export const csr = truefalse

您可以在頁面選項中找到更多相關信息。

+page.server.js

如果您的 load 函數只能在服務端上運行(例如,如果它需要從數據庫獲取數據或需要訪問私有環境變量,如 API 密鑰),那麼您可以將 +page.js 重命名為 +page.server.js,並將 PageLoad 類型更改為 PageServerLoad

/// file: src/routes/blog/[slug]/+page.server.js

// @filename: ambient.d.ts
declare global {
  const getPostFromDatabase: (slug: string) => {
    title: string;
    content: string;
  }
}

export {};

// @filename: index.js
// ---cut---
import { error } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
  const post = await getPostFromDatabase(params.slug);

  if (post) {
    return post;
  }

  error(404, 'Not found');
}

在客户端導航期間,SvelteKit 將從服務端加載此數據,這意味着返回值必須使用 devalue 進行序列化。有關該 API 的完整詳細信息,請參見 load

+page.js 類似,+page.server.js 可以導出頁面選項 — prerenderssrcsr

+page.server.js 文件還可以導出 actions。如果 load 讓您從服務端讀取數據,那麼 actions 讓您使用 <form> 元素向服務端寫入數據。要了解如何使用它們,請參閲 form actions 章節。

+error

如果在 load 期間發生錯誤,SvelteKit 將渲染默認錯誤頁面。您可以通過添加 +error.svelte 文件來自定義每個路由的錯誤頁面:

<!--- file: src/routes/blog/[slug]/+error.svelte --->
<script>
  import { page } from '$app/state';
</script>

<h1>{page.status}: {page.error.message}</h1>
[!LEGACY] > $app/state 是在 SvelteKit 2.12 中添加的。如果你使用的是早期版本或正在使用 Svelte 4,請改用 $app/stores

SvelteKit 會"向上遍歷"尋找最近的錯誤邊界 —— 如果上面的文件不存在,它會嘗試 src/routes/blog/+error.svelte 然後是 src/routes/+error.svelte,之後才會渲染默認錯誤頁面。如果失敗(或者如果錯誤是從根 +layoutload 函數拋出的,該函數位於根 +error 之上),SvelteKit 將退出並渲染一個靜態的後備錯誤頁面,你可以通過創建 src/error.html 文件來自定義它。

如果錯誤發生在 +layout(.server).js 中的 load 函數內,樹中最近的錯誤邊界是該佈局上方的 +error.svelte 文件(而不是在其旁邊)。

如果找不到路由(404),將使用 src/routes/+error.svelte(或者如果該文件不存在,則使用默認錯誤頁面)。

[!NOTE] 當錯誤發生在 handle 或 +server.js 請求處理程序中時,不會使用 +error.svelte

您可以在這裏閲讀更多關於錯誤處理的內容。

+layout

到目前為止,我們將頁面視為完全獨立的組件 —— 在導航時,現有的 +page.svelte 組件將被銷燬,新的組件將取而代之。

但在許多應用中,有些元素應該在每個頁面上都可見,比如頂層導航或頁腳。與其在每個 +page.svelte 中重複它們,我們可以將它們放在佈局中。

+layout.svelte

要創建一個適用於每個頁面的佈局,創建一個名為 src/routes/+layout.svelte 的文件。默認佈局(即當你沒有提供自己的佈局時 SvelteKit 使用的佈局)看起來是這樣的...

<script>
  let { children } = $props();
</script>

{@render children()}

...但我們可以添加任何想要的標記、樣式和行為。唯一的要求是組件必須包含一個用於頁面內容的 @render 標籤。例如,讓我們添加一個導航欄:

<!--- file: src/routes/+layout.svelte --->
<script>
  let { children } = $props();
</script>

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a href="/settings">Settings</a>
</nav>

{@render children()}

如果我們為 //about/settings 創建頁面...

/// file: src/routes/+page.svelte
<h1>Home</h1>
/// file: src/routes/about/+page.svelte
<h1>About</h1>
/// file: src/routes/settings/+page.svelte
<h1>Settings</h1>

...導航欄將始終可見,在這三個頁面之間點擊只會導致 <h1> 被替換。

佈局可以嵌套。假設我們不僅有一個 /settings 頁面,還有像 /settings/profile/settings/notifications 這樣的嵌套頁面,它們共享一個子菜單(實際示例請參見 github.com/settings)。

We can create a layout that only applies to pages below /settings (while inheriting the root layout with the top-level nav):

我們可以創建一個僅用於 /settings 下方頁面的佈局(同時繼承帶有頂級導航的根佈局):

<!--- file: src/routes/settings/+layout.svelte --->
<script>
  /** @type {{ data: import('./$types').LayoutData, children: import('svelte').Snippet }} */
  let { data, children } = $props();
</script>

<h1>Settings</h1>

<div class="submenu">
  {#each data.sections as section}
    <a href="/settings/{section.slug}">{section.title}</a>
  {/each}
</div>

{@render children()}

你可以通過查看下方下一節中的 +layout.js 示例來了解如何填充 data

默認情況下,每個佈局都會繼承其上層佈局。有時這並不是你想要的 - 在這種情況下,高級佈局可以幫助你。

+layout.js

就像 +page.svelte+page.js 加載數據一樣,你的 +layout.svelte 組件可以從 +layout.js 中的 load 函數獲取數據。

/// file: src/routes/settings/+layout.js
/** @type {import('./$types').LayoutLoad} */
export function load() {
    return {
        sections: [
            { slug: 'profile', title: 'Profile' },
            { slug: 'notifications', title: 'Notifications' }
        ]
    };
}

如果 +layout.js 導出頁面選項 - prerenderssrcsr - 它們將用作子頁面的默認值。

佈局的 load 函數返回的數據也可用於其所有子頁面:

<!--- file: src/routes/settings/profile/+page.svelte --->
<script>
  /** @type {{ data: import('./$types').PageData }} */
  let { data } = $props();

  console.log(data.sections); // [{ slug: 'profile', title: 'Profile' }, ...]
</script>
[!NOTE] 通常,在頁面之間導航時佈局數據保持不變。SvelteKit 會在必要時智能地重新運行 load 函數。

+layout.server.js

要在服務端上運行佈局的 load 函數,將其移至 +layout.server.js,並將 LayoutLoad 類型更改為 LayoutServerLoad

+layout.js 一樣,+layout.server.js 可以導出頁面選項 — prerender, ssr and csr.

+server

除了頁面之外,你還可以使用 +server.js 文件(有時稱為"API 路由"或"端點")定義路由,這使你可以完全控制響應。你的 +server.js 文件導出對應 HTTP 動詞的函數,如 GET, POST, PATCH, PUT, DELETE, OPTIONSHEAD,它們接受一個 RequestEvent 參數並返回一個 Response 對象。

例如,我們可以創建一個 /api/random-number 路由,帶有一個 GET 處理程序:

/// file: src/routes/api/random-number/+server.js
import { error } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export function GET({ url }) {
    const min = Number(url.searchParams.get('min') ?? '0');
    const max = Number(url.searchParams.get('max') ?? '1');

    const d = max - min;

    if (isNaN(d) || d < 0) {
        error(400, 'min and max must be numbers, and min must be less than max');
    }

    const random = min + Math.random() * d;

    return new Response(String(random));
}

Response 的第一個參數可以是 ReadableStream,這使得可以流式傳輸大量數據或創建 server-sent events(除非部署到像 AWS Lambda 這樣會緩衝響應的平台)。

為了方便起見,你可以使用來自 @sveltejs/kiterrorredirectjson 方法(但這不是必需的)。

如果拋出錯誤(無論是 error(...) 還是意外錯誤),響應將是一個錯誤的 JSON 格式或後備錯誤頁面(可以通過 src/error.html 自定義),具體取決於 Accept 頭部。在這種情況下,+error.svelte 組件將不會被渲染。你可以在這裏閲讀更多關於錯誤處理的信息。

[!NOTE] 創建 OPTIONS 處理程序時,請注意 Vite 將注入Access-Control-Allow-OriginAccess-Control-Allow-Methods 頭部 — 除非你添加它們,否則這些頭部在生產環境中不會出現。

[!NOTE] +layout 文件對 +server.js 文件沒有影響。如果你想在每個請求之前運行一些邏輯,請將其添加到服務端 handle hook 中。

接收數據

通過導出 POST/PUT/PATCH/DELETE/OPTIONS/HEAD 處理程序,+server.js 文件可用於創建完整的 API:

<!--- file: src/routes/add/+page.svelte --->
<script>
  let a = 0;
  let b = 0;
  let total = 0;

  async function add() {
    const response = await fetch('/api/add', {
      method: 'POST',
      body: JSON.stringify({ a, b }),
      headers: {
        'content-type': 'application/json'
      }
    });

    total = await response.json();
  }
</script>

<input type="number" bind:value={a}> +
<input type="number" bind:value={b}> =
{total}

<button onclick={add}>Calculate</button>
/// file: src/routes/api/add/+server.js
import { json } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
    const { a, b } = await request.json();
    return json(a + b);
}

[!NOTE] 一般來説,form actions 是從瀏覽器向服務端提交數據的更好方式。

[!NOTE] 如果導出了 GET 處理程序,HEAD 請求將返回 GET 處理程序響應體的content-length

後備方法處理程序

導出 fallback 處理程序將匹配任何未處理的請求方法,包括像 MOVE 這樣沒有從 +server.js 專門導出的方法。

// @errors: 7031
/// file: src/routes/api/add/+server.js
import { json, text } from '@sveltejs/kit';

export async function POST({ request }) {
    const { a, b } = await request.json();
    return json(a + b);
}

// This handler will respond to PUT, PATCH, DELETE, etc.
/** @type {import('./$types').RequestHandler} */
export async function fallback({ request }) {
    return text(`I caught your ${request.method} request!`);
}
[!NOTE] 對於 HEAD 請求,GET 處理程序優先於 fallback 處理程序。

內容協商

+server.js 文件可以與 +page 文件放在同一目錄中,使同一路由既可以是頁面也可以是 API 端點。為了確定是哪一種,SvelteKit 應用以下規則:

  • PUT/PATCH/DELETE/OPTIONS 請求總是由 +server.js 處理,因為它們不適用於頁面
  • GET / POST /HEAD 請求在 accept 頭優先考慮 text/html 時被視為頁面請求(換句話説,這是瀏覽器的頁面請求),否則由 +server.js 處理。
  • GET 請求的響應將包含 Vary: Accept 標頭,以便代理和瀏覽器分別緩存 HTML 和 JSON 響應。

$types

在上述所有示例中,我們一直在從 $types.d.ts 文件導入類型。如果您使用 TypeScript(或帶有 JSDoc 類型註釋的 JavaScript),SvelteKit 會在隱藏目錄中為您創建這個文件,以便在處理根文件時提供類型安全。

例如,用 PageData(或者對於 +layout.svelte 文件使用 LayoutData)註釋 let { data } = $props() 告訴 TypeScript,data 的類型就是從 load 返回的類型:

<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
  /** @type {{ data: import('./$types').PageData }} */
  let { data } = $props();
</script>

反過來,使用 load 函數並用 PageLoadPageServerLoadLayoutLoadLayoutServerLoad(分別對應 +page.js+page.server.js+layout.js+layout.server.js)進行註解,可以確保 params 和返回值被正確類型化。

如果你使用 VS Code 或任何支持語言服務協議和 TypeScript 插件的 IDE,那麼你可以完全省略這些類型!Svelte 的 IDE 工具會為你插入正確的類型,所以你無需自己編寫就能獲得類型檢查。它也可以與我們的命令行工具 svelte-check 一起使用。

你可以在我們關於省略 $types 的博客文章中瞭解更多信息。

其他文件

Any other files inside a route directory are ignored by SvelteKit. This means you can colocate components and utility modules with the routes that need them.

SvelteKit 會忽略路由目錄中的任何其他文件。這意味着你可以將組件和工具模塊與需要它們的路由放在一起。

If components and modules are needed by multiple routes, it's a good idea to put them in $lib.

如果多個路由都需要這些組件和模塊,最好將它們放在 $lib 中。

拓展閲讀

  • 教程:路由
  • 教程:API 路由
  • 文檔:高級路由

Svelte 中文文檔

點擊查看中文文檔 - SvelteKit 路由。

系統學習 Svelte,歡迎入手小冊《Svelte 開發指南》。語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

此外我還寫過 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答讀者問等 14 個系列文章, 全系列文章目錄:https://github.com/mqyqingfeng/Blog

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

user avatar qian5201314 头像 huanjinliu 头像
点赞 2 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.