SvelteKit 最新中文文档教程(2)—— 路由
<h2 id="前言">前言</h2><p>Svelte,一个语法简洁、入门容易,面向未来的前端框架。</p>
<p>从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,<strong>从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1</strong>:</p>
<p><img src="https://yayujs-blog.oss-cn-beijing.aliyuncs.com/405488775-48df16b1-939c-489b-8d52-6071869893f0.png"></p>
<p>Svelte 以其独特的编译时优化机制著称,具有<strong>轻量级</strong>、<strong>高性能</strong>、<strong>易上手</strong>等特性,<strong>非常适合构建轻量级 Web 项目</strong>。</p>
<p>为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。</p>
<p>如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p>
<h2 id="路由">路由</h2>
<p>SvelteKit 的核心是一个基于文件系统的路由器。应用程序的路由(即用户可以访问的 URL 路径)由代码库中的目录定义:</p>
<ul>
<li><code>src/routes</code> 是根路由</li>
<li><code>src/routes/about</code> 创建一个 <code>/about</code> 路由</li>
<li><code>src/routes/blog/</code> 创建一个带有参数 <code>slug</code> 的路由,当用户请求类似 <code>/blog/hello-world</code> 的页面时,可以用它动态加载数据</li>
</ul>
<blockquote>
<p>[!NOTE] 您可以通过编辑项目配置来将 <code>src/routes</code> 更改为其他目录。</p>
</blockquote>
<p>每个路由目录包含一个或多个路由文件,这些文件可以通过它们的 <code>+</code> 前缀识别。</p>
<p>我们稍后会更详细地介绍这些文件,但这里有几个简单的规则可以帮助您记住 SvelteKit 的路由是如何工作的:</p>
<ul>
<li>所有文件都可以在服务端上运行</li>
<li>除了 <code>+server</code> 文件外,所有文件都在客户端运行</li>
<li><code>+layout</code> 和 <code>+error</code> 文件不仅适用于它们所在的目录,也适用于子目录</li>
</ul>
<h2 id="page">+page</h2>
<h3 id="pagesvelte">+page.svelte</h3>
<p><code>+page.svelte</code> 组件定义了您应用程序的一个页面。默认情况下,页面在初始请求时在服务端渲染(SSR),在后续导航时在浏览器中渲染(CSR)。</p>
<pre><code class="language-svelte"><!--- file: src/routes/+page.svelte --->
<h1>您好,欢迎来到我的网站!</h1>
<a href="/about">关于我的网站</a>
</code></pre>
<pre><code class="language-svelte"><!--- file: src/routes/about/+page.svelte --->
<h1>关于本站</h1>
<p>待办...</p>
<a href="/">首页</a>
</code></pre>
<p>页面可以通过 <code>data</code> 属性接收来自 <code>load</code> 函数的数据。</p>
<pre><code class="language-svelte"><!--- file: src/routes/blog//+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();
</script>
<h1>{data.title}</h1>
<div>{@html data.content}</div>
</code></pre>
<blockquote>
<p>[!遗留模式]<br>
在 Svelte 4 中,您需要使用 <code>export let data</code> 代替</p>
</blockquote>
<blockquote>
<p>[!NOTE] SvelteKit 使用 <code><a></code> 元素在路由之间导航,而不是框架特定的 <code><Link></code> 组件。</p>
</blockquote>
<h3 id="pagejs">+page.js</h3>
<p>通常,页面在渲染之前需要加载一些数据。为此,我们添加一个 <code>+page.js</code> 模块,该模块导出一个 <code>load</code> 函数:</p>
<pre><code class="language-js">/// file: src/routes/blog//+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');
}
</code></pre>
<p>这个函数与 <code>+page.svelte</code> 一起运行,这意味着它在服器端渲染期间在服务端上运行,在客户端导航期间在浏览器中运行。有关该 API 的完整详细信息,请参见 <code>load</code>。</p>
<p>除了 <code>load</code>,<code>+page.js</code> 还可以导出一些值用于配置页面行为:</p>
<ul>
<li><code>export const prerender = true</code> 或 <code>false</code> 或 <code>'auto'</code></li>
<li><code>export const ssr = true</code> 或 <code>false</code></li>
<li><code>export const csr = true</code> 或 <code>false</code></li>
</ul>
<p>您可以在页面选项中找到更多相关信息。</p>
<h3 id="pageserverjs">+page.server.js</h3>
<p>如果您的 <code>load</code> 函数只能在服务端上运行(例如,如果它需要从数据库获取数据或需要访问私有环境变量,如 API 密钥),那么您可以将 <code>+page.js</code> 重命名为 <code>+page.server.js</code>,并将 <code>PageLoad</code> 类型更改为 <code>PageServerLoad</code>。</p>
<pre><code class="language-js">/// file: src/routes/blog//+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');
}
</code></pre>
<p>在客户端导航期间,SvelteKit 将从服务端加载此数据,这意味着返回值必须使用 devalue 进行序列化。有关该 API 的完整详细信息,请参见 <code>load</code>。</p>
<p>与 <code>+page.js</code> 类似,<code>+page.server.js</code> 可以导出页面选项 — <code>prerender</code>、<code>ssr</code> 和 <code>csr</code>。</p>
<p><code>+page.server.js</code> 文件还可以导出 <em>actions</em>。如果 <code>load</code> 让您从服务端读取数据,那么 <code>actions</code> 让您使用 <code><form></code> 元素向服务端写入数据。要了解如何使用它们,请参阅 form actions 章节。</p>
<h2 id="error">+error</h2>
<p>如果在 <code>load</code> 期间发生错误,SvelteKit 将渲染默认错误页面。您可以通过添加 <code>+error.svelte</code> 文件来自定义每个路由的错误页面:</p>
<pre><code class="language-svelte"><!--- file: src/routes/blog//+error.svelte --->
<script>
import { page } from '$app/state';
</script>
<h1>{page.status}: {page.error.message}</h1>
</code></pre>
<blockquote>
<p>[!LEGACY] > <code>$app/state</code> 是在 SvelteKit 2.12 中添加的。如果你使用的是早期版本或正在使用 Svelte 4,请改用 <code>$app/stores</code>。</p>
</blockquote>
<p>SvelteKit 会"向上遍历"寻找最近的错误边界 —— 如果上面的文件不存在,它会尝试 <code>src/routes/blog/+error.svelte</code> 然后是 <code>src/routes/+error.svelte</code>,之后才会渲染默认错误页面。如果失败(或者如果错误是从根 <code>+layout</code> 的 <code>load</code> 函数抛出的,该函数位于根 <code>+error</code> 之上),SvelteKit 将退出并渲染一个静态的后备错误页面,你可以通过创建 <code>src/error.html</code> 文件来自定义它。</p>
<p>如果错误发生在 <code>+layout(.server).js</code> 中的 <code>load</code> 函数内,树中最近的错误边界是该布局上方的 <code>+error.svelte</code> 文件(而不是在其旁边)。</p>
<p>如果找不到路由(404),将使用 <code>src/routes/+error.svelte</code>(或者如果该文件不存在,则使用默认错误页面)。</p>
<blockquote>
<p>[!NOTE] 当错误发生在 <code>handle</code> 或 +server.js 请求处理程序中时,不会使用 <code>+error.svelte</code>。</p>
</blockquote>
<p>您可以在这里阅读更多关于错误处理的内容。</p>
<h2 id="layout">+layout</h2>
<p>到目前为止,我们将页面视为完全独立的组件 —— 在导航时,现有的 <code>+page.svelte</code> 组件将被销毁,新的组件将取而代之。</p>
<p>但在许多应用中,有些元素应该在每个页面上都可见,比如顶层导航或页脚。与其在每个 <code>+page.svelte</code> 中重复它们,我们可以将它们放在布局中。</p>
<h3 id="layoutsvelte">+layout.svelte</h3>
<p>要创建一个适用于每个页面的布局,创建一个名为 <code>src/routes/+layout.svelte</code> 的文件。默认布局(即当你没有提供自己的布局时 SvelteKit 使用的布局)看起来是这样的...</p>
<pre><code class="language-svelte"><script>
let { children } = $props();
</script>
{@render children()}
</code></pre>
<p>...但我们可以添加任何想要的标记、样式和行为。唯一的要求是组件必须包含一个用于页面内容的 <code>@render</code> 标签。例如,让我们添加一个导航栏:</p>
<pre><code class="language-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()}
</code></pre>
<p>如果我们为 <code>/</code>、<code>/about</code> 和 <code>/settings</code> 创建页面...</p>
<pre><code class="language-html">/// file: src/routes/+page.svelte
<h1>Home</h1>
</code></pre>
<pre><code class="language-html">/// file: src/routes/about/+page.svelte
<h1>About</h1>
</code></pre>
<pre><code class="language-html">/// file: src/routes/settings/+page.svelte
<h1>Settings</h1>
</code></pre>
<p>...导航栏将始终可见,在这三个页面之间点击只会导致 <code><h1></code> 被替换。</p>
<p>布局可以嵌套。假设我们不仅有一个 <code>/settings</code> 页面,还有像 <code>/settings/profile</code> 和 <code>/settings/notifications</code> 这样的嵌套页面,它们共享一个子菜单(实际示例请参见 github.com/settings)。</p>
<p>We can create a layout that only applies to pages below <code>/settings</code> (while inheriting the root layout with the top-level nav):</p>
<p>我们可以创建一个仅用于 <code>/settings</code> 下方页面的布局(同时继承带有顶级导航的根布局):</p>
<pre><code class="language-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()}
</code></pre>
<p>你可以通过查看下方下一节中的 <code>+layout.js</code> 示例来了解如何填充 <code>data</code>。</p>
<p>默认情况下,每个布局都会继承其上层布局。有时这并不是你想要的 - 在这种情况下,高级布局可以帮助你。</p>
<h3 id="layoutjs">+layout.js</h3>
<p>就像 <code>+page.svelte</code> 从 <code>+page.js</code> 加载数据一样,你的 <code>+layout.svelte</code> 组件可以从 <code>+layout.js</code> 中的 <code>load</code> 函数获取数据。</p>
<pre><code class="language-js">/// file: src/routes/settings/+layout.js
/** @type {import('./$types').LayoutLoad} */
export function load() {
return {
sections: [
{ slug: 'profile', title: 'Profile' },
{ slug: 'notifications', title: 'Notifications' }
]
};
}
</code></pre>
<p>如果 <code>+layout.js</code> 导出页面选项 - <code>prerender</code>、<code>ssr</code>和 <code>csr</code> - 它们将用作子页面的默认值。</p>
<p>布局的 <code>load</code> 函数返回的数据也可用于其所有子页面:</p>
<pre><code class="language-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>
</code></pre>
<blockquote>
<p>[!NOTE] 通常,在页面之间导航时布局数据保持不变。SvelteKit 会在必要时智能地重新运行 <code>load</code> 函数。</p>
</blockquote>
<h3 id="layoutserverjs">+layout.server.js</h3>
<p>要在服务端上运行布局的 <code>load</code> 函数,将其移至 <code>+layout.server.js</code>,并将 <code>LayoutLoad</code> 类型更改为 <code>LayoutServerLoad</code>。</p>
<p>与 <code>+layout.js</code> 一样,<code>+layout.server.js</code> 可以导出页面选项 — <code>prerender</code>, <code>ssr</code> and <code>csr</code>.</p>
<h2 id="server">+server</h2>
<p>除了页面之外,你还可以使用 <code>+server.js</code> 文件(有时称为"API 路由"或"端点")定义路由,这使你可以完全控制响应。你的 <code>+server.js</code> 文件导出对应 HTTP 动词的函数,如 <code>GET</code>, <code>POST</code>, <code>PATCH</code>, <code>PUT</code>, <code>DELETE</code>, <code>OPTIONS</code> 和 <code>HEAD</code>,它们接受一个 <code>RequestEvent</code> 参数并返回一个 <code>Response</code> 对象。</p>
<p>例如,我们可以创建一个 <code>/api/random-number</code> 路由,带有一个 <code>GET</code> 处理程序:</p>
<pre><code class="language-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));
}
</code></pre>
<p><code>Response</code> 的第一个参数可以是 <code>ReadableStream</code>,这使得可以流式传输大量数据或创建 server-sent events(除非部署到像 AWS Lambda 这样会缓冲响应的平台)。</p>
<p>为了方便起见,你可以使用来自 <code>@sveltejs/kit</code> 的 <code>error</code>、<code>redirect</code> 和 <code>json</code> 方法(但这不是必需的)。</p>
<p>如果抛出错误(无论是 <code>error(...)</code> 还是意外错误),响应将是一个错误的 JSON 格式或后备错误页面(可以通过 <code>src/error.html</code> 自定义),具体取决于 <code>Accept</code> 头部。在这种情况下,<code>+error.svelte</code> 组件将不会被渲染。你可以在这里阅读更多关于错误处理的信息。</p>
<blockquote>
<p>[!NOTE] 创建 <code>OPTIONS</code> 处理程序时,请注意 <code>Vite</code> 将注入<code>Access-Control-Allow-Origin</code> 和 <code>Access-Control-Allow-Methods</code> 头部 — 除非你添加它们,否则这些头部在生产环境中不会出现。</p>
</blockquote>
<blockquote>
<p>[!NOTE] <code>+layout</code> 文件对 <code>+server.js</code> 文件没有影响。如果你想在每个请求之前运行一些逻辑,请将其添加到服务端 <code>handle</code> hook 中。</p>
</blockquote>
<h3 id="接收数据">接收数据</h3>
<p>通过导出 <code>POST</code>/<code>PUT</code>/<code>PATCH</code>/<code>DELETE</code>/<code>OPTIONS</code>/<code>HEAD</code> 处理程序,<code>+server.js</code> 文件可用于创建完整的 API:</p>
<pre><code class="language-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>
</code></pre>
<pre><code class="language-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);
}
</code></pre>
<blockquote>
<p>[!NOTE] 一般来说,form actions 是从浏览器向服务端提交数据的更好方式。</p>
</blockquote>
<blockquote>
<p>[!NOTE] 如果导出了 <code>GET</code> 处理程序,<code>HEAD</code> 请求将返回 <code>GET</code> 处理程序响应体的<code>content-length</code>。</p>
</blockquote>
<h3 id="后备方法处理程序">后备方法处理程序</h3>
<p>导出 <code>fallback</code> 处理程序将匹配任何未处理的请求方法,包括像 <code>MOVE</code> 这样没有从 <code>+server.js</code> 专门导出的方法。</p>
<pre><code class="language-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!`);
}
</code></pre>
<blockquote>
<p>[!NOTE] 对于 <code>HEAD</code> 请求,<code>GET</code> 处理程序优先于 <code>fallback</code> 处理程序。</p>
</blockquote>
<h3 id="内容协商">内容协商</h3>
<p><code>+server.js</code> 文件可以与 <code>+page</code> 文件放在同一目录中,使同一路由既可以是页面也可以是 API 端点。为了确定是哪一种,SvelteKit 应用以下规则:</p>
<ul>
<li><code>PUT</code>/<code>PATCH</code>/<code>DELETE</code>/<code>OPTIONS</code> 请求总是由 <code>+server.js</code> 处理,因为它们不适用于页面</li>
<li><code>GET</code> / <code>POST</code> /<code>HEAD</code> 请求在 <code>accept</code> 头优先考虑 <code>text/html</code> 时被视为页面请求(换句话说,这是浏览器的页面请求),否则由 <code>+server.js</code> 处理。</li>
<li>对 <code>GET</code> 请求的响应将包含 <code>Vary: Accept</code> 标头,以便代理和浏览器分别缓存 HTML 和 JSON 响应。</li>
</ul>
<h2 id="types">$types</h2>
<p>在上述所有示例中,我们一直在从 <code>$types.d.ts</code> 文件导入类型。如果您使用 TypeScript(或带有 JSDoc 类型注释的 JavaScript),SvelteKit 会在隐藏目录中为您创建这个文件,以便在处理根文件时提供类型安全。</p>
<p>例如,用 <code>PageData</code>(或者对于 <code>+layout.svelte</code> 文件使用 <code>LayoutData</code>)注释 <code>let { data } = $props()</code> 告诉 TypeScript,<code>data</code> 的类型就是从 <code>load</code> 返回的类型:</p>
<pre><code class="language-svelte"><!--- file: src/routes/blog//+page.svelte --->
<script>
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();
</script>
</code></pre>
<p>反过来,使用 <code>load</code> 函数并用 <code>PageLoad</code>、<code>PageServerLoad</code>、<code>LayoutLoad</code> 或 <code>LayoutServerLoad</code>(分别对应 <code>+page.js</code>、<code>+page.server.js</code>、<code>+layout.js</code> 和 <code>+layout.server.js</code>)进行注解,可以确保 <code>params</code> 和返回值被正确类型化。</p>
<p>如果你使用 VS Code 或任何支持语言服务协议和 TypeScript 插件的 IDE,那么你可以完全省略这些类型!Svelte 的 IDE 工具会为你插入正确的类型,所以你无需自己编写就能获得类型检查。它也可以与我们的命令行工具 <code>svelte-check</code> 一起使用。</p>
<p>你可以在我们关于省略 <code>$types</code> 的博客文章中了解更多信息。</p>
<h2 id="其他文件">其他文件</h2>
<p>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.</p>
<p>SvelteKit 会忽略路由目录中的任何其他文件。这意味着你可以将组件和工具模块与需要它们的路由放在一起。</p>
<p>If components and modules are needed by multiple routes, it's a good idea to put them in <code>$lib</code>.</p>
<p>如果多个路由都需要这些组件和模块,最好将它们放在 <code>$lib</code> 中。</p>
<h2 id="拓展阅读">拓展阅读</h2>
<ul>
<li>教程:路由</li>
<li>教程:API 路由</li>
<li>文档:高级路由</li>
</ul>
<h2 id="svelte-中文文档">Svelte 中文文档</h2>
<p>点击查看中文文档 - SvelteKit 路由。</p>
<p>系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p><br><br>
来源:https://www.cnblogs.com/yayujs/p/18770641
頁:
[1]