SvelteKit 最新中文文档教程(2)—— 路由

前言

Svelte,一个语法简洁、入门容易,面向未来的前端框架。

从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1

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)。

svelte 复制代码
<!--- file: src/routes/+page.svelte --->
<h1>您好,欢迎来到我的网站!</h1>
<a href="/about">关于我的网站</a>
svelte 复制代码
<!--- file: src/routes/about/+page.svelte --->
<h1>关于本站</h1>
<p>待办...</p>
<a href="/">首页</a>

页面可以通过 data 属性接收来自 load 函数的数据。

svelte 复制代码
<!--- 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 函数:

js 复制代码
/// 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

js 复制代码
/// 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 文件来自定义每个路由的错误页面:

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 使用的布局)看起来是这样的...

svelte 复制代码
<script>
  let { children } = $props();
</script>

{@render children()}

...但我们可以添加任何想要的标记、样式和行为。唯一的要求是组件必须包含一个用于页面内容的 @render 标签。例如,让我们添加一个导航栏:

svelte 复制代码
<!--- 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 创建页面...

html 复制代码
/// file: src/routes/+page.svelte
<h1>Home</h1>
html 复制代码
/// file: src/routes/about/+page.svelte
<h1>About</h1>
html 复制代码
/// 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 下方页面的布局(同时继承带有顶级导航的根布局):

svelte 复制代码
<!--- 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 函数获取数据。

js 复制代码
/// 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 函数返回的数据也可用于其所有子页面:

svelte 复制代码
<!--- 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 处理程序:

js 复制代码
/// 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:

svelte 复制代码
<!--- 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>
js 复制代码
/// 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 专门导出的方法。

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 返回的类型:

svelte 复制代码
<!--- 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 中。

拓展阅读

Svelte 中文文档

点击查看中文文档 - SvelteKit 路由

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列TypeScript 系列React 系列Next.js 系列冴羽答读者问等 14 个系列文章, 全系列文章目录:github.com/mqyqingfeng...

欢迎围观我的"网页版朋友圈"、加入"冴羽·成长陪伴社群",踏上"前端大佬成长之路"

相关推荐
用户2587141932637 分钟前
Vue3使用多线程处理文件分片任务
前端
不懂装懂的不懂9 分钟前
【vue3】中断请求、取消请求
前端·javascript·vue.js
鱼樱前端14 分钟前
React18+pnpm+Ts+React-Router v6从0-1搭建后台系统
前端·javascript·react.js
Epicurus15 分钟前
ES6箭头函数
前端
掘金0116 分钟前
手把手教你使用 FLV.js 在 Vue 项目中播放 FLV 视频
前端
前端没钱16 分钟前
vue3怎么和大模型交互?
前端
狗头大军之江苏分军18 分钟前
移动端直播卡顿如何实时检测且告知用户
java·前端·后端
1024小神20 分钟前
拦截网页中的 Fetch 和 XMLHttpRequest 请求方式方法
前端·javascript
谎言西西里20 分钟前
Vue组件化实战🧐:手把手教你打造模块化组件树🌳
前端
远舟巴卡21 分钟前
事件循环详解
前端·javascript·面试