土猛的员外
SvelteKit中文2-核心概念

字数统计: 14.9k阅读时长: 65 min
2023/04/25

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念

Routing

SvelteKit 的核心是基于文件系统的路由器。您应用程序的路由(即用户可以访问的 URL 路径)由代码库中的目录定义:

  • src/routes 是根路径
  • src/routes/about 创建了一个 /about 路径
  • src/routes/blog/[slug] 创建了一个带有参数 slug 的路径,该参数可用于在用户请求像 /blog/hello-world 这样的页面时动态加载数据。

您可以通过编辑项目配置将src/routes更改为不同的目录。

每个路由目录包含一个或多个路由文件,可以通过它们的+前缀进行识别。

+page

+page.svelte

+page.svelte 组件定义了你的应用程序中的一个页面。默认情况下,页面在服务器端(SSR)上进行初始请求渲染,并在浏览器端(CSR)上进行后续导航渲染。

src/routes/+page.svelte

1
2
<h1>Hello and welcome to my site!</h1>
<a href="/about">About my site</a>

src/routes/about/+page.svelte

1
2
3
<h1>About this site</h1>
<p>TODO...</p>
<a href="/">Home</a>

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

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

请注意,SvelteKit 使用<a>元素在路由之间导航,而不是特定于框架的<Link>组件。

+page.js

通常情况下,在页面渲染之前需要加载一些数据。为此,我们添加一个 +page.js 模块,该模块导出一个 load 函数:

src/routes/blog/[slug]/+page.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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...'

};

}


throw 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
src/routes/blog/[slug]/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { error } from '@sveltejs/kit';


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


if (post) {
return post;
}


throw error(404, 'Not found');
}

在客户端导航期间,SvelteKit 将从服务器加载此数据,这意味着返回的值必须可序列化使用 devalue。有关 API 的完整详细信息,请参见 load

+page.js 相似,+page.server.js 可以导出页面选项 - prerenderssrcsr

+page.server.js 文件还可以导出 actions。如果 load 允许您从服务器读取数据,则 actions 允许您使用 <form> 元素将数据写入服务器。要了解如何使用它们,请参见表单操作部分。

+error

如果在load过程中出现错误,SvelteKit 将呈现默认的错误页面。您可以通过添加 +error.svelte 文件来针对每个路由自定义此错误页面:

src/routes/blog/[slug]/+error.svelte

1
2
3
4
5
<script>
import { page } from '$app/stores';
</script>

<h1>{$page.status}: {$page.error.message}</h1>

SvelteKit会“向上遍历树”,寻找最近的错误边界 - 如果上面的文件不存在,它将尝试src/routes/blog/+error.sveltesrc/routes/+error.svelte,在呈现默认错误页面之前。如果失败(或者如果错误是从位于根+error“上方”的根+layout的load函数中抛出的),SvelteKit将退出并呈现静态回退错误页面,您可以通过创建一个src/error.html文件来自定义该页面。

如果在+layout(.server).js中发生错误,则树中最接近该布局(而不是旁边)的+error.svelte文件是最接近的错误边界。

如果找不到路由(404),则使用src/routes/+error.svelte(或默认错误页面,如果该文件不存在)。

当在handle函数或+server.js请求处理程序内发生错误时,不会使用+error.svelte

+layout

到目前为止,我们已经将页面视为完全独立的组件 - 在导航时,现有的+page.svelte组件将被销毁,并且一个新的组件将取代它。

但在许多应用程序中,有些元素应该在每个页面上可见,例如顶级导航或页脚。我们可以将它们放入布局中,而不是在每个+page.svelte中重复它们。

+layout.svelte

要创建适用于每个页面的布局,请创建一个名为src/routes/+layout.svelte的文件。默认布局(如果您没有自己带来)如下所示…

1
<slot></slot>

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

src/routes/+layout.svelte

1
2
3
4
5
6
7
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/settings">Settings</a>
</nav>

<slot></slot>

如果我们为 //about and /settings…创建页面

src/routes/+page.svelte

1
<h1>Home</h1>

src/routes/about/+page.svelte

1
<h1>About</h1>

src/routes/settings/+page.svelte

1
<h1>Settings</h1>

…导航栏将始终可见,单击三个页面之间只会导致h1被替换。

布局可以嵌套。假设我们不仅有单个/settings页面,而是有像/settings/profile/settings/notifications这样的嵌套页面,并且具有共享子菜单(例如,请参见github.com/settings的实际示例)。

我们可以创建一个仅适用于/settings以下页面的布局(同时继承具有顶级导航的根布局):

src/routes/settings/+layout.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
/** @type {import('./$types').LayoutData} */ export let data;
</script>

<h1>Settings</h1>

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

<slot></slot>

默认情况下,每个布局都会继承其上面的布局。有时这并不是您想要的 - 在这种情况下,高级布局可以帮助您。

+layout.js

就像 +page.svelte +page.js 加载数据一样,你的+layout.svelte组件可以从+layout.js中的 load 函数获取数据。
src/routes/settings/+layout.js

1
2
3
4
5
6
7
8
9
/** @type {import('./$types').LayoutLoad} */
export function load() {
return {
sections: [
{ slug: 'profile', title: 'Profile' },
{ slug: 'notifications', title: 'Notifications' }
]
};
}

如果一个名为 +layout.js 的文件导出页面选项 - prerenderssrcsr,它们将作为子页面的默认值。

从布局的load函数返回的数据也可用于所有其子页面:

src/routes/settings/profile/+page.svelte

1
2
3
4
5
<script>
/** @type {import('./$types').PageData} */ export let data;

console.log(data.sections); // [{ slug: 'profile', title: 'Profile' }, ...]
</script>

通常情况下,在页面之间导航时,布局数据不会改变。SvelteKit 将在必要时智能重新运行load函数。

+layout.server.js

将您的布局load函数移动到 +layout.server.js 并将 LayoutLoad 类型更改为 LayoutServerLoad,即可在服务器上运行它。

+layout.js 一样,+layout.server.js 可以导出页面选项 - prerenderssrcsr

+server

除了页面外,您还可以使用+server.js文件(有时称为“API路由”或“端点”)定义路由,从而完全控制响应。您的+server.js文件导出与HTTP动词相对应的函数,如GETPOSTPATCHPUTDELETEOPTIONS,这些函数接受RequestEvent参数并返回一个Response对象。

例如,我们可以创建一个带有GET处理程序的/api/random-number路由:

src/routes/api/random-number/+server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
throw 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,这使得流式传输大量数据或创建服务器发送事件成为可能(除非部署到像 AWS Lambda 这样缓冲响应的平台)。

您可以使用 @sveltejs/kit 中的 errorredirect json 方法来方便地处理错误(但不一定要这样做)。

如果抛出错误(无论是 throw error(...) 还是意外错误),响应将是该错误的 JSON 表示形式或回退错误页面 —— 可以通过 src/error.html 自定义 —— 具体取决于 Accept 标头。在这种情况下,+error.svelte 组件将不会被渲染。您可以在此处阅读有关错误处理的更多信息。

创建 OPTIONS 处理程序时,请注意 Vite 将注入Access-Control-Allow-OriginAccess-Control-Allow-Methods标头 - 这些标头在生产环境中将不存在,除非您添加它们。

Receiving data

通过导出POST/PUT/PATCH/DELETE/OPTIONS处理程序,可以使用+server.js文件创建完整的API:

src/routes/add/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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 on:click={add}>Calculate</button>

src/routes/api/add/+server.js

1
2
3
4
5
6
7
8
import { json } from '@sveltejs/kit';


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

一般来说,表单操作是从浏览器向服务器提交数据的更好方式。

Content negotiation

+server.js文件可以放置在与+page文件相同的目录中,从而允许相同的路由成为页面或API端点。为了确定哪个是哪个,SvelteKit应用以下规则:

  • PUT/PATCH/DELETE/OPTIONS请求始终由+server.js处理,因为它们不适用于页面
  • 如果接受标头优先考虑text/html(换句话说,这是浏览器页面请求),则GET/POST请求被视为页面请求;否则它们将由+server.js处理

$types

在上面的示例中,我们一直从 $types.d.ts 文件中导入类型。如果您使用 TypeScript(或带有 JSDoc 类型注释的 JavaScript)来处理根文件,则 SvelteKit 会在隐藏目录中为您创建此文件,以便在处理根文件时提供类型安全性。

例如,将 export let data 注释为 [PageData](对于 +layout.svelte 文件则是 LayoutData),告诉 TypeScript 数据的类型是从 load 返回的任何内容:

src/routes/blog/[slug]/+page.svelte

1
2
3
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

反过来,使用PageLoadPageServerLoadLayoutLoadLayoutServerLoad(分别用于+page.js+page.server.js+layout.js+layout.server.js)为load函数进行注释可以确保参数和返回值的正确类型。

如果您正在使用VS Code或任何支持语言服务器协议和TypeScript插件的IDE,则可以完全省略这些类型! Svelte的IDE工具将为您插入正确的类型,因此您无需自己编写即可获得类型检查。它还与我们的命令行工具svelte-check一起使用。

您可以在我们关于省略$types的博客文章中了解更多信息。

Other files

路由目录中的任何其他文件都将被 SvelteKit 忽略。这意味着您可以将组件和实用程序模块与需要它们的路由放在一起。

如果多个路由需要使用组件和模块,则最好将它们放在 $lib 中。

Loading data

在渲染 +page.svelte 组件(以及其包含的 +layout.svelte 组件)之前,我们通常需要获取一些数据。这是通过定义load函数来完成的。

Page data

一个 +page.svelte 文件可以有一个兄弟文件 +page.js,该文件导出一个load函数,其返回值可通过 data 属性在页面中使用:

src/routes/blog/[slug]/+page.js

1
2
3
4
5
6
7
8
9
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
return {
post: {
title: `Title for ${params.slug} goes here`,
content: `Content for ${params.slug} goes here`
}
};
}

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

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

由于生成的 $types 模块,我们获得了完整的类型安全。

+page.js 文件中,一个load函数会在服务器和浏览器上都运行。如果您的load函数应该始终在服务器上运行(例如因为它使用私有环境变量或访问数据库),那么它将放置在 +page.server.js 中。

更实际版本的博客文章load函数只在服务器上运行并从数据库中提取数据,可能看起来像这样:

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

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
post: await db.getPost(params.slug)
};
}

请注意,类型已从PageLoad更改为PageServerLoad,因为服务器负载函数可以访问其他参数。要了解何时使用+page.js和何时使用+page.server.js,请参见Universal vs server。

Layout data

您的 +layout.svelte 文件也可以通过 +layout.js+layout.server.js 加载数据。

src/routes/blog/[slug]/+layout.server.js

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
return {
posts: await db.getPostSummaries()
};
}

src/routes/blog/[slug]/+layout.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
/** @type {import('./$types').LayoutData} */ export let data;
</script>

<main>
<!-- +page.svelte is rendered in this <slot> --> <slot />
</main>

<aside>
<h2>More posts</h2>
<ul>
{#each data.posts as post}
<li>
<a href="/blog/{post.slug}">
{post.title}
</a>
</li>
{/each}
</ul>
</aside>

从布局load函数返回的数据可供子+layout.svelte组件和+page.svelte组件以及其所属的布局使用。

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import { page } from '$app/stores';

/** @type {import('./$types').PageData} */
export let data;

// we can access `data.posts` because it's returned from
// the parent layout `load` function
$: index = data.posts.findIndex(post => post.slug === $page.params.slug);
$: next = data.posts[index - 1];
</script>

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

{#if next}
<p>Next post: <a href="/blog/{next.slug}">{next.title}</a></p>
{/if}

如果多个load函数返回具有相同键的数据,则最后一个“获胜”——布局load返回{ a: 1,b: 2 }和页面加载返回{ b: 3,c: 4 }的结果将是{ a: 1,b:3,c:4}

$page.data

+page.svelte 组件以及它上面的每个 +layout.svelte 组件都可以访问自己的数据和所有父级数据。

在某些情况下,我们可能需要相反的操作 - 父布局可能需要访问页面数据或子布局中的数据。例如,根布局可能想要访问从 +page.js+page.server.js 返回的 title 属性。这可以通过 $page.data 来实现:

src/routes/+layout.svelte

1
2
3
4
5
6
7
<script>
import { page } from '$app/stores';
</script>

<svelte:head>
<title>{$page.data.title}</title>
</svelte:head>

$page.data的类型信息由App.PageData提供。

通用vs服务器

As we’ve seen, there are two types of load function:

  • +page.js and +layout.js files export universal load functions that run both on the server and in the browser
  • +page.server.js and +layout.server.js files export server load functions that only run server-side

Conceptually, they’re the same thing, but there are some important differences to be aware of.

正如我们所看到的,有两种类型的load函数:

  • +page.js 和 +layout.js 文件导出通用的加载函数,在服务器和浏览器上都可以运行

  • +page.server.js 和 +layout.server.js 文件导出仅在服务器端运行的服务器加载函数

从概念上讲,它们是相同的东西,但需要注意一些重要区别。

什么时候运行哪个加载函数?

服务器load函数始终在服务器上运行。

默认情况下,通用的load函数在 SSR 期间首次访问页面时在服务器上运行。然后,在水合作用期间再次运行,并重复使用来自获取请求的任何响应。所有后续调用通用load函数都发生在浏览器中。您可以通过页面选项自定义此行为。如果禁用了服务器端渲染,则会获得 SPA,并且通用load功能始终在客户端上运行。

除非您预渲染页面,否则将在运行时调用load函数- 在这种情况下,它将在构建时间调用。

Input

通用和服务器load函数都可以访问描述请求(paramsroute url)以及各种函数(fetchsetHeadersparent depends)的属性。这些在以下章节中进行了描述。

服务器load函数使用 ServerLoadEvent 调用,该事件从 RequestEvent 继承 clientAddresscookieslocalsplatformrequest 属性。

通用load函数使用 LoadEvent 调用,该事件具有 data 属性。如果您在 +page.js+page.server.js(或 +layout.js+layout.server.js)中都有load函数,则服务器load函数的返回值是通用load函数参数的 data 属性。

Output

一个通用的load函数可以返回一个包含任何值的对象,包括自定义类和组件构造函数等。

服务器load函数必须返回可使用devalue序列化的数据 - 任何可以表示为JSON的内容以及BigIntDateMapSetRegExp之类的内容,或者是重复/循环引用 - 以便它可以通过网络传输。您的数据可能包括promises,在这种情况下,它将被流式传输到浏览器。

在什么时间用哪个函数

服务器load函数在需要直接从数据库或文件系统访问数据,或需要使用私有环境变量时非常方便。

通用的load函数在需要从外部API去fetch数据且不需要私人凭据时非常有用,因为SvelteKit可以直接从API获取数据而无需通过您的服务器。当您需要返回无法序列化的内容(例如Svelte组件构造函数)时,它们也很有用。

在极少数情况下,您可能需要同时使用两者 - 例如,您可能需要返回一个自定义类的实例,并将其初始化为来自服务器的数据。

Using URL data

通常,load函数在某种程度上取决于URL。为此,load函数提供了urlrouteparams参数。

url

一个 URL 实例,包含诸如 originhostnamepathnamesearchParams(其中包含解析后的查询字符串作为 URLSearchParams 对象)等属性。由于服务器上不可用,因此在load期间无法访问 url.hash

在某些环境中,这是在服务器端渲染期间从请求头中派生出来的。例如,如果您正在使用adapter-node,则可能需要配置适配器以使URL正确。

route

包含当前路由目录的名称,相对于 src/routes:

src/routes/a/[b]/[…c]/+page.js

1
2
3
4
/** @type {import('./$types').PageLoad} */
export function load({ route }) {
console.log(route.id); // '/a/[b]/[...c]'
}

params

params是从url.pathnameroute.id派生出来的。

假设route.id/a/[b]/[...c]url.pathname/a/x/y/z,则params对象如下所示:

1
2
3
4
{
"b": "x",
"c": "y/z"
}

进行获取请求

从外部API或+server.js 处理程序获取数据,您可以使用提供的fetch函数,它与native fetch web API具有相同的行为,并具有一些附加功能:

  • 它可用于在服务器上进行凭证请求,因为它继承了页面请求的cookieauthorization标头
  • 它可以在服务器上进行相对请求(通常,在服务器上下文中使用时,fetch需要带有源URL)
  • 内部请求(例如+server.js路由)在运行时直接进入处理程序函数,而无需HTTP调用开销
  • 在服务器端渲染期间,响应将被捕获并通过钩入 Response 对象的 textjson 方法内联到呈现的 HTML 中。请注意,除非通过 filterSerializedResponseHeaders 显式包含,否则不会序列化标头。然后,在水合期间,响应将从 HTML 中读取,确保一致性并防止额外的网络请求 - 如果您在使用浏览器fetch而不是 load fetch 时在浏览器控制台中收到警告,则原因就在于此。

src/routes/items/[id]/+page.js

1
2
3
4
5
6
7
8
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
const res = await fetch(`/api/items/${params.id}`);
const item = await res.json();


return { item };
}

只有当目标主机与 SvelteKit 应用程序相同或是其更具体的子域时,才会传递 Cookie。

Cookies and headers

服务器load函数可以获取和设置cookies

src/routes/+layout.server.js

1
2
3
4
5
6
7
8
9
10
11
12
import * as db from '$lib/server/database';


/** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies }) {
const sessionid = cookies.get('sessionid');


return {
user: await db.getUser(sessionid)
};
}

在设置 cookie 时,请注意path属性。默认情况下,cookie 的path是当前的路径名。例如,在页面 admin/user 上设置 cookie,则默认情况下该 cookie 只能在admin页面中使用。在大多数情况下,您可能希望将路径设置为 ‘/‘,以使 cookie 在整个应用程序中可用。

服务器和通用load函数都可以访问setHeaders函数,该函数在服务器上运行时可以为响应设置标头。(在浏览器中运行时,setHeaders没有效果。) 如果您想要页面被缓存,则这非常有用:

src/routes/products/+page.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, setHeaders }) {
const url = `https://cms.example.com/products.json`;
const response = await fetch(url);


// cache the page for the same length of time
// as the underlying data
setHeaders({
age: response.headers.get('age'),
'cache-control': response.headers.get('cache-control')
});


return response.json();
}

多次设置相同的标题(即使在不同的load函数中)是错误的 - 您只能设置给定标题一次。您不能使用setHeaders添加set-cookie标头 - 请改用cookies.set(name,value,options)

Using parent data

有时候,一个load函数需要访问其父级load函数的数据是很有用的,这可以通过使用 await parent() 来实现:

src/routes/+layout.js

1
2
3
4
/** @type {import('./$types').LayoutLoad} */
export function load() {
return { a: 1 };
}

src/routes/abc/+layout.js

1
2
3
4
5
/** @type {import('./$types').LayoutLoad} */
export async function load({ parent }) {
const { a } = await parent();
return { b: a + 1 };
}

src/routes/abc/+page.js

1
2
3
4
5
/** @type {import('./$types').PageLoad} */
export async function load({ parent }) {
const { a, b } = await parent();
return { c: a + b };
}

src/routes/abc/+page.svelte

1
2
3
4
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>
<!-- renders `1 + 2 = 3` --><p>{data.a} + {data.b} = {data.c}</p>

请注意,在 +page.js 中的load函数接收来自布局load函数的合并数据,而不仅仅是直接父级的数据。

+page.server.js+layout.server.js 中,parent 返回来自父级 +layout.server.js 文件的数据。

+page.js或者 +layout.js 中,它将返回来自父级+layout.js文件的数据。然而,缺少+layout.js被视为一个 ({data}) => data 函数,这意味着它也会返回未被+layout.js文件“遮蔽”的父级+layout.server.js文件中的数据

使用await parent()时要注意不要引入瀑布式渲染。例如,在此处 getData(params) 不依赖于调用parent()的结果,因此我们应该先调用它以避免延迟渲染。

+page.js

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('./$types').PageLoad} */
export async function load({ params, parent }) {
const parentData = await parent();
const data = await getData(params);
const parentData = await parent();

return {
...data
meta: { ...parentData.meta, ...data.meta }
};
}

Errors

如果在load过程中出现错误,则会呈现最近的 +error.svelte。对于预期的错误,请使用 @sveltejs/kit 中的 error 帮助程序来指定 HTTP 状态代码和可选消息:

src/routes/admin/+layout.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { error } from '@sveltejs/kit';


/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw error(401, 'not logged in');
}


if (!locals.user.isAdmin) {
throw error(403, 'not an admin');
}
}

如果出现意外错误,SvelteKit将调用 handleError 并将其视为 500 内部错误。

Redirects

要重定向用户,请使用@sveltejs/kitredirect 助手,指定应将其重定向到的位置以及3xx状态代码。

src/routes/user/+layout.server.js

1
2
3
4
5
6
7
8
9
import { redirect } from '@sveltejs/kit';


/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
if (!locals.user) {
throw redirect(307, '/login');
}
}

确保您没有捕获被抛出的重定向,否则将阻止 SvelteKit 处理它。

在浏览器中,您还可以使用$app.navigationgoto函数,在load函数之外以编程方式导航。

使用 Promises 进行流式传输

返回对象的顶层承诺将被等待,这使得返回多个承诺而不创建瀑布变得容易。在使用服务器load时,嵌套的承诺将随着它们的解决而流式传输到浏览器中。如果您有缓慢、非必要数据,则此功能很有用,因为您可以在所有数据可用之前开始呈现页面:

src/routes/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('./$types').PageServerLoad} */
export function load() {
return {
one: Promise.resolve(1),
two: Promise.resolve(2),
streamed: {
three: new Promise((fulfil) => {
setTimeout(() => {
fulfil(3)
}, 1000);
})
}
};
}

这对于创建骨架加载状态非常有用,例如:

src/routes/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
/** @type {import('./$types').PageData} */ export let data;
</script>

<p>
one: {data.one}
</p>
<p>
two: {data.two}
</p>
<p>
three:
{#await data.streamed.three}
Loading...
{:then value}
{value}
{:catch error}
{error.message}
{/await}
</p>

在不支持流式传输的平台上,比如 AWS Lambda,响应将被缓冲。这意味着页面只有在所有承诺都解决后才会渲染出来。

只有在启用 JavaScript 时,流数据才能正常工作。如果页面是服务器渲染的,则应避免从通用load函数返回嵌套的 promises,因为这些不会被流式传输 - 相反,在浏览器中重新运行该函数时,promise 将被重新创建。

并行加载

在渲染(或导航到)页面时,SvelteKit 会同时运行所有load函数,避免请求的瀑布流。在客户端导航期间,调用多个服务器load函数的结果被分组为单个响应。一旦所有load函数都返回了,页面就会被呈现出来。

重新运行加载函数

SvelteKit会跟踪每个load函数的依赖关系,以避免在导航期间不必要地重新运行它。

例如,给定这样一对load函数…

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

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {
post: await db.getPost(params.slug)
};
}

src/routes/blog/[slug]/+layout.server.js

1
2
3
4
5
6
7
8
9
import * as db from '$lib/server/database';


/** @type {import('./$types').LayoutServerLoad} */
export async function load() {
return {
posts: await db.getPostSummaries()
};
}

…在 +page.server.js 中,如果我们从/blog/trying-the-raw-meat-diet导航到 /blog/i-regret-my-choicesparams.slug 发生了变化,那么它将重新运行。而在 +layout.server.js 中不会重新运行,因为数据仍然有效。换句话说,我们不会第二次调用 db.getPostSummaries()

调用 await parent() load 函数也会重新运行,如果父级 load 函数被重新运行的话。

依赖跟踪在 load 函数返回后不适用 - 例如,在嵌套的 promise 中访问 params.x 不会导致函数在 params.x 更改时重新运行。(别担心,在开发中如果你意外这样做了会收到警告)。相反,在您的 load 函数主体中访问参数。

Manual invalidation

您还可以使用invalidate(url)重新运行适用于当前页面的加载函数,该函数将重新运行所有依赖于url的load函数,并且可以使用invalidateAll()重新运行每个加载函数。

如果一个load函数调用fetch(url)depends(url),则它依赖于url。请注意,url可以是以[a-z]::开头的自定义标识符。

src/routes/random-number/+page.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, depends }) {
// load reruns when `invalidate('https://api.example.com/random-number')` is called...
const response = await fetch('https://api.example.com/random-number');


// ...or when `invalidate('app:random')` is called
depends('app:random');


return {
number: await response.json()
};
}

src/routes/random-number/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
import { invalidate, invalidateAll } from '$app/navigation';
/** @type {import('./$types').PageData} */ export let data;

function rerunLoadFunction() {
// any of these will cause the `load` function to re-run invalidate('app:random');
invalidate('https://api.example.com/random-number');
invalidate(url => url.href.includes('random-number'));
invalidateAll();
}
</script>

<p>random number: {data.number}</p>
<button on:click={rerunLoadFunction}>Update random number</button>

总结一下,以下情况会导致负载函数重新运行:

  • 它引用了 params 的某个属性,其值已更改
  • 它引用了 url 的某个属性(例如 url.pathnameurl.search),其值已更改
  • 它调用 await parent() 并且父级负载函数重新运行
  • 通过 fetch depends 声明对特定 URL 的依赖关系,并使用invalidate(url)标记该 URL 无效时
  • 所有活动的加载函数都被强制重新运行以使用 invalidateAll()

paramsurl可能会因为 <a href=".."> 链接点击、<form> 交互、goto 调用或redirect而发生变化。

请注意,重新运行load函数将更新相应的 +layout.svelte +page.svelte 中的数据 prop;它不会导致组件被重新创建。因此,内部状态得到保留。如果这不是您想要的结果,则可以在 afterNavigate 回调中重置所需内容,并/或在 {#key ...} 块中包装组件。

Form actions

+page.server.js文件可以导出操作,允许您使用<form>元素将数据POST到服务器。

在使用<form>时,客户端JavaScript是可选的,但您可以轻松地通过JavaScript逐步增强表单交互以提供最佳用户体验。

Default actions

在最简单的情况下,一个页面声明了一个默认操作:

src/routes/login/+page.server.js

1
2
3
4
5
6
/** @type {import('./$types').Actions} */
export const actions = {
default: async (event) => {
// TODO log the user in
}
};

要从/login页面调用此操作,只需添加一个<form>即可,无需JavaScript:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
<form method="POST">
<label>
Email
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
</form>

如果有人点击按钮,浏览器将通过POST请求将表单数据发送到运行默认操作的服务器。

Actions总是使用POST请求,因为GET请求不应该具有副作用。

我们还可以通过添加action属性并指向页面来从其他页面调用该操作(例如,如果根布局中的导航中有登录小部件):

src/routes/+layout.svelte

1
2
<form method="POST" action="/login">
<!-- content --></form>

Named actions

一个页面可以拥有多个命名动作,而不是只有一个default动作:

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
/** @type {import('./$types').Actions} */
export const actions = {
default: async (event) => {
login: async (event) => {
// TODO log the user in
},
register: async (event) => {
// TODO register the user
}
};

要调用命名操作,请添加一个查询参数,名称前缀为 / 字符:

src/routes/login/+page.svelte

1
<form method="POST" action="?/register">

src/routes/+layout.svelte

1
<form method="POST" action="/login?/register">

除了 action 属性外,我们还可以在按钮上使用 formaction 属性将相同的表单数据 POST 到与父 <form> 不同的操作:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
<form method="POST">
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email">
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
<button formaction="?/register">Register</button>
</form>

我们不能在命名操作旁边设置默认操作,因为如果您向未重定向的命名操作POST,则查询参数将保留在URL中,这意味着下一个默认的POST将通过之前的命名操作进行。

解剖action

每个操作都会接收到一个 RequestEvent 对象,允许您使用request.formData()读取数据。在处理请求后(例如通过设置 cookie 登录用户),该操作可以响应数据,这些数据将通过相应页面的form属性和 $page.form 应用程序范围内直到下一次更新可用。

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies }) {
const user = await db.getUserFromSession(cookies.get('sessionid'));
return { user };
}

/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');

const user = await db.getUser(email);
cookies.set('sessionid', await db.createSession(user));

return { success: true };
},
register: async (event) => {
// TODO register the user
}
};

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
<script>
/** @type {import('./$types').PageData} */
export let data;
/** @type {import('./$types').ActionData} */
export let form;
</script>

{#if form?.success}
<!-- this message is ephemeral; it exists because the page was rendered in
response to a form submission. it will vanish if the user reloads -->
<p>Successfully logged in! Welcome back, {data.user.name}</p>
{/if}

Validation errors

如果由于无效数据而无法处理请求,您可以将验证错误(以及先前提交的表单值)返回给用户,以便他们可以再次尝试。fail函数允许您返回HTTP状态码(通常是400或422,在验证错误的情况下),以及数据。状态代码可通过$page.status获得,表单数据可通过form获得:

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { fail } from '@sveltejs/kit';

/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');

if (!email) {
return fail(400, { email, missing: true });
}

const user = await db.getUser(email);

if (!user || user.password !== hash(password)) {
return fail(400, { email, incorrect: true });
}

cookies.set('sessionid', await db.createSession(user));

return { success: true };
},
register: async (event) => {
// TODO register the user
}
};

请注意,作为一项预防措施,我们仅将电子邮件返回到页面 —— 而不是密码。

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form method="POST" action="?/login">
{#if form?.missing}<p class="error">The email field is required</p>{/if}
{#if form?.incorrect}<p class="error">Invalid credentials!</p>{/if}
<label>
Email
<input name="email" type="email">
<input name="email" type="email" value={form?.email ?? ''}>
</label>
<label>
Password
<input name="password" type="password">
</label>
<button>Log in</button>
<button formaction="?/register">Register</button>
</form>

返回的数据必须可序列化为JSON。除此之外,结构完全由您决定。例如,如果页面上有多个表单,则可以使用id属性或类似方法来区分返回的表单数据所属于哪个<form>

Redirects

重定向(和错误)的工作方式与load时完全相同:

src/routes/login/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { fail, redirect } from '@sveltejs/kit';

/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ cookies, request, url }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');

const user = await db.getUser(email);
if (!user) {
return fail(400, { email, missing: true });
}

if (user.password !== hash(password)) {
return fail(400, { email, incorrect: true });
}

cookies.set('sessionid', await db.createSession(user));

if (url.searchParams.has('redirectTo')) {
throw redirect(303, url.searchParams.get('redirectTo'));
}

return { success: true };
},
register: async (event) => {
// TODO register the user
}
};

Loading data

当一个操作运行后,页面将被重新渲染(除非发生重定向或意外错误),并且该操作的返回值可用作form属性提供给页面。这意味着您的页面load函数将在操作完成后运行。

请注意,handle 在调用动作之前运行,并且不会在load函数之前重新运行。这意味着如果例如您使用 handle 基于 cookie 来填充 event.locals,则必须在设置或删除 cookie 时更新 event.locals:

src/hooks.server.js

1
2
3
4
5
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
}

src/routes/account/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('./$types').PageServerLoad} */
export function load(event) {
return {
user: event.locals.user
};
}

/** @type {import('./$types').Actions} */
export const actions = {
logout: async (event) => {
event.cookies.delete('sessionid');
event.locals.user = null;
}
};

Progressive enhancement(渐进增强)

在前面的章节中,我们构建了一个可以在没有客户端JavaScript的情况下工作的/login操作——看不到任何fetch。这很棒,但是当JavaScript可用时,我们可以逐步增强我们的表单交互以提供更好的用户体验。

use:enhance

逐步增强表单的最简单方法是添加 use:enhance 操作:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
<script>
import { enhance } from '$app/forms';

/** @type {import('./$types').ActionData} */
export let form;
</script>

<form method="POST" use:enhance>

是的,增强操作和<form action>都被称为“action”,有点令人困惑。这些文档充满了行动。抱歉。

没有参数时,use:enhance将模拟浏览器本机行为,只是没有完整的页面重新加载。它将:

  • 在成功或无效响应时更新form属性 $page.form$page.status,但仅当操作在您提交的页面上时才更新。例如,如果您的表单看起来像 <form action="/somewhere/else" ..>,则不会更新form $page。这是因为在本机表单提交情况下,您将被重定向到操作所在的页面。如果要无论如何更新它们,请使用 applyAction
  • 在成功响应中重置 <form> 元素并使所有数据失效,并使用 invalidateAll
  • 对于重定向响应调用 goto
  • 如果发生错误,则呈现最近的 +error 边界。
  • 将焦点重置为适当的元素。

要自定义行为,您可以提供一个 SubmitFunction,在表单提交之前立即运行,并(可选)返回一个回调函数,该回调函数将与 ActionResult一起运行。请注意,如果您返回了一个回调函数,则不会触发上述默认行为。要恢复它,请调用 update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form
method="POST"
use:enhance={({ form, data, action, cancel, submitter }) => {
// `form` is the `<form>` element
// `data` is its `FormData` object
// `action` is the URL to which the form is posted
// `cancel()` will prevent the submission
// `submitter` is the `HTMLElement` that caused the form to be submitted

return async ({ result, update }) => {
// `result` is an `ActionResult` object
// `update` is a function which triggers the logic that would be triggered if this callback wasn't set
};
}}
>

你可以使用这些函数来显示和隐藏加载界面等。

applyAction

如果您提供自己的回调函数,您可能需要重现默认的 use:enhance 行为的一部分,例如显示最近的 +error 边界。大多数情况下,调用传递给回调函数的 update 就足够了。如果您需要更多定制化,则可以使用 applyAction

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
import { enhance, applyAction } from '$app/forms';

/** @type {import('./$types').ActionData} */
export let form;
</script>

<form
method="POST"
use:enhance={({ form, data, action, cancel }) => {
// `form` is the `<form>` element
// `data` is its `FormData` object
// `action` is the URL to which the form is posted
// `cancel()` will prevent the submission

return async ({ result }) => {
// `result` is an `ActionResult` object
if (result.type === 'error') {
await applyAction(result);
}
};
}}
>

applyAction(result) 的行为取决于 result.type

  • success, failure — 将 $page.status 设置为 result.status,并将表单和 $page.form 更新为 result.data(与从 enhance 提交的位置无关,不同于 update
  • redirect — 调用 goto(result.location)
  • error — 使用 result.error 渲染最近的 +error 边界

在所有情况下,焦点都会被重置。

自定义事件监听器

我们也可以自己实现渐进增强,而不使用 use:enhance,在 <form> 上使用普通的事件监听器:

src/routes/login/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
/** @type {import('./$types').ActionData} */ export let form;
/** @type {any} */ let error;

async function handleSubmit(event) {
const data = new FormData(this);

const response = await fetch(this.action, {
method: 'POST',
body: data
});
/** @type {import('@sveltejs/kit').ActionResult} */ const result = deserialize(await response.text());

if (result.type === 'success') {
// re-run all `load` functions, following the successful update await invalidateAll();
}

applyAction(result);
}
</script>

<form method="POST" on:submit|preventDefault={handleSubmit}>
<!-- content --></form>

请注意,在使用 $app/forms 中相应的方法进一步处理响应之前,您需要对其进行反序列化。JSON.parse() 不足以支持表单操作(如load函数)返回 Date BigInt 对象。

如果您在 +page.server.js 旁边还有一个 +server.js,则默认情况下 fetch 请求将被路由到该位置。要将 POST 请求发送到 +page.server.js 中的操作,请使用自定义 x-sveltekit-action 标头:

1
2
3
4
5
6
7
const response = await fetch(this.action, {
method: 'POST',
body: data,
headers: {
'x-sveltekit-action': 'true'
}
});

可选方案

表单操作是向服务器发送数据的首选方式,因为它们可以逐步增强,但您也可以使用 ·+server.js· 文件来公开(例如)JSON API。以下是这种交互可能看起来像的方式:

send-message/+page.svelte

1
2
3
4
5
6
7
8
9
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>

<button on:click={rerun}>Rerun CI</button>

api/ci/+server.js

1
2
3
4
/** @type {import('./$types').RequestHandler} */
export function POST() {
// do something
}

GET vs POST

正如我们所见,要调用表单操作,必须使用 method="POST"

有些表单不需要将数据 POST 到服务器 - 比如搜索输入框。对于这些表单,您可以使用 method="GET"(或者等效地根本不指定方法),SvelteKit 将像 <a> 元素一样处理它们,使用客户端路由器而不是完整的页面导航:

1
2
3
4
5
6
<form action="/search">
<label>
Search
<input name="q">
</label>
</form>

提交此表单将导航到/search?q=...并调用您的加载函数,但不会触发任何操作。与<a>元素一样,您可以在 <form> 上设置 data-sveltekit-reloaddata-sveltekit-replacestatedata-sveltekit-keepfocus data-sveltekit-noscroll 属性来控制路由器的行为。

Page options

默认情况下,SvelteKit 会在服务器上首先渲染(或预渲染)任何组件,并将其作为 HTML 发送到客户端。然后它会再次在浏览器中呈现该组件,以使其交互,在一个称为水合的过程中。因此,您需要确保组件可以在两个地方运行。然后 SvelteKit 将初始化一个路由器来接管随后的导航。

您可以通过从 +page.js +page.server.js 导出选项来逐页控制这些内容,也可以使用共享的 +layout.js+layout.server.js 来控制一组页面。要定义整个应用程序的选项,请从根布局导出它们。子布局和页面覆盖父布局设置的值,因此例如您可以启用整个应用程序的预渲染功能,然后禁用需要动态呈现的页面。

您可以混合和匹配这些选项在应用程序不同区域内使用。例如你可以对营销页面进行最大速度预渲染、对动态页面进行服务器端呈现以提高 SEO 和可访问性,并通过仅在客户端上呈现将管理部分转换成 SPA 。这使得 SvelteKit 非常灵活多变。

预渲染

你的应用程序中至少有一些路由可以表示为在构建时生成的简单HTML文件。这些路由可以进行预渲染。

+page.js/+page.server.js/+server.js

1
export const prerender = true;

或者,您可以在根目录下的 +layout.js +layout.server.js 中设置 export const prerender = true,并预渲染除明确标记为不可预渲染的页面之外的所有内容:

+page.js/+page.server.js/+server.js

1
export const prerender = false;

具有 prerender = true 的路由将从用于动态 SSR 的清单中排除,使您的服务器(或无服务器/边缘函数)更小。在某些情况下,您可能希望预渲染一个路由,但也要将其包含在清单中(例如,在像 /blog/[slug] 这样的路由上,您希望预渲染最近/流行的内容但是对长尾进行服务器呈现)- 对于这些情况,有第三个选项,“auto”:

+page.js/+page.server.js/+server.js

1
export const prerender = 'auto';

如果您的整个应用程序适合预渲染,您可以使用adapter-static,它将输出适用于任何静态 Web 服务器的文件。

预渲染器将从您的应用程序根目录开始,并为任何可预渲染页面或+server.js路由生成文件。每个页面都会被扫描,以查找指向其他候选预渲染页面的<a>元素 - 因此,通常不需要指定应访问哪些页面。如果确实需要指定预渲染器应访问哪些页面,则可以在prerender配置中使用entries选项进行设置。

在预渲染期间,从$app/environment导入的building值将为true

预渲染服务器路由

与其他页面选项不同,预渲染也适用于 +server.js 文件。这些文件不受布局的影响,但如果有任何从它们获取数据的页面,则会继承默认值。例如,如果一个 +page.js 包含此load函数…

+page.js

1
2
3
4
5
6
7
export const prerender = true;

/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
const res = await fetch('/my-server-route.json');
return await res.json();
}

….那么如果 src/routes/my-server-route.json/+server.js 不包含自己的 export const prerender = false,则将视为可预渲染。

什么时候不做预渲染

基本规则是:对于一个可以预渲染的页面,任何两个直接访问它的用户必须从服务器获取相同的内容。

并非所有页面都适合预渲染。任何预渲染的内容都将被所有用户看到。当然,您可以在预渲染页面中的 ·onMount· 中获取个性化数据,但这可能会导致较差的用户体验,因为它将涉及空白初始内容或加载指示器。

请注意,您仍然可以预渲染基于页面参数加载数据的页面,例如 src/routes/blog/[slug]/+page.svelte 路由。

在预渲染期间访问 url.searchParams 是被禁止的。如果需要使用它,请确保只在浏览器中这样做(例如在 onMount 中)。

具有操作的页面无法进行预渲染,因为服务器必须能够处理操作 POST 请求。

预渲染和服务器端渲染

如果将 ssr 选项设置为 false,则每个请求都会导致相同的空 HTML shell。由于这会导致不必要的工作,SvelteKit 默认预渲染任何找到的页面,其中prerender没有明确设置为 false

路由冲突

由于预渲染会写入文件系统,因此不可能有两个端点导致目录和文件具有相同的名称。例如,src/routes/foo/+server.js src/routes/foo/bar/+server.js 将尝试创建 foo foo/bar,这是不可能的。

因此,建议您始终包括文件扩展名 - src/routes/foo.json/+server.jssrc/routes/foo/bar.json/+server.js将导致foo.jsonfoo/bar.json文件和谐地并存。

对于页面,我们通过编写foo/index.html而不是foo来避免这个问题。

故障排除

如果你遇到了像“以下路由被标记为可预渲染,但未进行预渲染”这样的错误,那是因为相关的路由(或者如果它是一个页面,则是其父布局)具有 export const prerender = true ,但该页面实际上没有被预渲染,因为它没有被预渲染爬虫访问到。

由于这些路由无法动态服务器端呈现,在人们尝试访问相关路由时会导致错误。解决方法有两种:

  • 确保 SvelteKit 可以通过从 config.kit.prerender.entries 中跟随链接找到该路线。如果不能通过爬行其他入口点找到带参数的动态路径(即包含[parameters]的页面),请将其添加到此选项中;否则,它们不会被预渲染,因为 SvelteKit 不知道参数应该具有什么值。未标记为可预渲染的页面将被忽略,并且它们与其他页面之间的链接也不会被爬行。
  • export const prerender = true 更改为 export const prerender ='auto' 。使用'auto'的路线可以进行动态服务器端呈现。

ssr

通常情况下,SvelteKit 首先在服务器上呈现您的页面,并将该 HTML 发送到客户端进行水合作用。如果您将 ssr 设置为 false,则会呈现一个空的“外壳”页面。如果您的页面无法在服务器上呈现(例如,因为使用了仅限于浏览器的全局变量,如 document),则这很有用,但在大多数情况下不建议这样做(请参见附录)。

+page.js

1
export const ssr = false;

如果你在根 +layout.js 中添加 export const ssr = false,那么整个应用程序将只在客户端渲染 —— 这基本上意味着你把应用程序变成了单页应用。

csr

通常情况下,SvelteKit会将服务器渲染的HTML转换为交互式客户端渲染(CSR)页面。有些页面根本不需要JavaScript - 许多博客文章和“关于”页面属于这一类别。在这些情况下,您可以禁用CSR:

+page.js

1
export const csr = false;

如果 ssr and csr 都设置为 false, 那么不会有任何内容被预渲染!!!

trailingSlash

默认情况下,SvelteKit 会从 URL 中删除尾随斜杠 —— 如果您访问 /about/,它将响应重定向到 /about。您可以使用 trailingSlash 选项更改此行为,该选项可以是 'never'(默认值)、'always' 'ignore'

与其他页面选项一样,您可以从 +layout.js+layout.server.js 导出此值,并将其应用于所有子页面。您还可以从 +server.js 文件中导出配置。

src/routes/+layout.js

1
export const trailingSlash = 'always';

此选项也会影响预渲染。如果 trailingSlashalways,则像/about这样的路由将生成一个 about/index.html 文件,否则它将创建 about.html,反映静态 Web 服务器约定。

忽略尾部斜杠不是推荐的做法——相对路径的语义在两种情况下有所不同(从/x./y/y,但从/x/./y/x/y),而且/x/x/被视为不同的URL,这对SEO有害。

配置

通过适配器的概念,SvelteKit 可以在各种平台上运行。每个平台可能都有特定的配置来进一步调整部署 - 例如,在 Vercel 上,您可以选择将应用程序的某些部分部署在边缘,而将其他部分部署在无服务器环境中。

config 是一个具有键值对的对象,在顶层。除此之外,具体形状取决于您使用的适配器。每个适配器都应该提供 Config 接口进行导入以实现类型安全性。请查阅您所使用适配器的文档获取更多信息。

src/routes/+page.js

1
2
3
4
/** @type {import('some-adapter').Config} */
export const config = {
runtime: 'edge'
};

config对象在顶层合并(但不是更深的级别)。这意味着,如果您只想覆盖上面+layout.js中的某些值,则无需在+page.js中重复所有值。例如,此布局配置…

src/routes/+layout.js

1
2
3
4
5
6
7
export const config = {
runtime: 'edge',
regions: 'all',
foo: {
bar: true
}
}

…被此页面配置覆盖…

src/routes/+page.js

1
2
3
4
5
6
export const config = {
regions: ['us1', 'us2'],
foo: {
baz: true
}
}

这导致该页面的配置值为 { runtime: 'edge', regions: ['us1', 'us2'], foo: { baz: true } } for that page.

状态管理

如果你习惯于构建仅客户端应用程序,那么在跨服务器和客户端的应用程序中进行状态管理可能会让人感到害怕。本节提供了一些避免常见陷阱的提示。

避免在服务器上使用共享状态

浏览器是有状态的——随着用户与应用程序的交互,状态存储在内存中。另一方面,服务器是无状态的——响应内容完全由请求内容确定。

从概念上讲,就是这样。但实际上,服务器通常具有长寿命并且被多个用户共享。因此,在共享变量中不要存储数据非常重要。例如,请考虑以下代码:

+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let user;

/** @type {import('./$types').PageServerLoad} */
export function load() {
return { user };
}

/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const data = await request.formData();

// NEVER DO THIS!
user = {
name: data.get('name'),
embarrassingSecret: data.get('secret')
};
}
}

user变量是由连接到此服务器的所有人共享的。如果爱丽丝提交了一个令人尴尬的秘密,而鲍勃在她之后访问了该页面,那么鲍勃将知道爱丽丝的秘密。此外,当爱丽丝在同一天晚些时候返回网站时,服务器可能已经重新启动,导致她的数据丢失。

相反地,您应该使用 cookie 对用户进行身份验证,并将数据持久化到数据库中。

负载无副作用

出于同样的原因,你的load函数应该是纯函数——没有副作用(除了偶尔使用 console.log(...))。例如,你可能会想在load函数中写入一个 store,以便在组件中使用该 store 值:

+page.js

1
2
3
4
5
6
7
8
9
import { user } from '$lib/user';

/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
const response = await fetch('/api/user');

// NEVER DO THIS!
user.set(await response.json());
}

与前面的例子一样,这将一个用户的信息放在所有用户共享的位置。相反,只需返回数据即可…

+page.js

1
2
3
4
5
6
7
export async function load({ fetch }) {
const response = await fetch('/api/user');

return {
user: await response.json()
};
}

…并将其传递给需要它的组件,或使用 $page.data

如果您没有使用 SSR,则不会意外地将一个用户的数据暴露给另一个用户。但是,您仍应避免在load函数中产生副作用-这样您的应用程序就更容易理解了。

使用带有上下文的存储

你可能会想知道,如果我们不能使用自己的存储库,我们如何能够使用 $page.data 和其他应用商店。答案是服务器上的应用商店使用 Svelte 的上下文 API - 存储库附加到组件树中,并通过setContext进行订阅时检索 getContext。我们可以对自己的存储库执行相同的操作:

src/routes/+layout.svelte

1
2
3
4
5
6
7
8
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
/** @type {import('./$types').LayoutData} */ export let data;
// Create a store and update it when necessary... const user = writable();
$: user.set(data.user);
// ...and add it to the context for child components to access setContext('user', user);
</script>

src/routes/user/+page.svelte

1
2
3
4
5
6
<script>
import { getContext } from 'svelte';
// Retrieve user store from context const user = getContext('user');
</script>

<p>Welcome {$user.name}</p>

如果您不使用 SSR(并且可以保证将来不需要使用SSR),那么您可以安全地在共享模块中保留状态,而无需使用上下文API。

组件状态被保留

当您在应用程序中导航时,SvelteKit 会重复使用现有的布局和页面组件。例如,如果您有这样一个路由…

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
/** @type {import('./$types').PageData} */
export let data;
// THIS CODE IS BUGGY!
const wordCount = data.content.split(' ').length;
const estimatedReadingTime = wordCount / 250;
</script>

<header>
<h1>{data.title}</h1>
<p>Reading time: {Math.round(estimatedReadingTime)} minutes</p>
</header>

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

…然后从 /blog/my-short-post 导航到 /blog/my-long-post 不会导致组件被销毁和重新创建。数据属性(以及data.titledata.content)将发生更改,但由于代码没有重新运行,estimatedReadingTime 将不会被重新计算。

相反,我们需要使该值具有响应性:

src/routes/blog/[slug]/+page.svelte

1
2
3
4
5
6
7
<script>
/** @type {import('./$types').PageData} */
export let data;

$: wordCount = data.content.split(' ').length;
$: estimatedReadingTime = wordCount / 250;
</script>

像这样重复使用组件意味着侧边栏滚动状态等内容得以保留,而且您可以轻松地在不同值之间进行动画处理。但是,如果您确实需要在导航时完全销毁和重新挂载组件,则可以使用此模式:

1
2
3
{#key $page.url.pathname}
<BlogPost title={data.title} content={data.title} />
{/key}

在URL中存储状态

如果您有应该在重新加载后保留并/或影响SSR的状态,例如表格上的过滤器或排序规则,则URL搜索参数(如?sort=price&order=ascending)是放置它们的好地方。您可以将它们放在<a href="..."><form action="...">属性中,也可以通过goto('?key=value')以编程方式设置它们。可以通过url参数在load函数内部访问它们,在组件内部通过$page.url.searchParams访问它们。

在快照中存储短暂状态

一些 UI 状态,例如“手风琴是否打开?”,是可丢弃的——如果用户导航离开或刷新页面,则状态丢失并不重要。在某些情况下,如果用户导航到另一页然后返回,您确实希望数据保持不变,但将状态存储在 URL 或数据库中会过度复杂化。为此,SvelteKit 提供了快照功能,让您将组件状态与历史记录条目关联起来。

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念



关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


联系我


我的公众号

我的Twitter

  1. 关于我的个人简介


热门文章

  1. LangFlow--可视化的LangChain
  2. 部署本地大模型(基于Langchain)
  3. LLM品质核心—Embedding Model
  4. 当知识图谱遇上大模型——多跳问答
  5. 实用Prompt工程讲解

原文作者:yuanwai

原文链接:https://www.luxiangdong.com/2023/04/25/sveltekit2/

发表日期:四月 25日 2023, 11:41:36 晚上

更新日期:May 30th 2023, 11:31:24 am

版权声明:

CATALOG
  1. 1. Routing
    1. 1.1. +page
      1. 1.1.1. +page.svelte
      2. 1.1.2. +page.js
      3. 1.1.3. +page.server.js
    2. 1.2. +error
    3. 1.3. +layout
      1. 1.3.1. +layout.svelte
      2. 1.3.2. +layout.js
      3. 1.3.3. +layout.server.js
    4. 1.4. +server
      1. 1.4.1. Receiving data
      2. 1.4.2. Content negotiation
    5. 1.5. $types
    6. 1.6. Other files
  2. 2. Loading data
    1. 2.1. Page data
    2. 2.2. Layout data
    3. 2.3. $page.data
    4. 2.4. 通用vs服务器
      1. 2.4.1. 什么时候运行哪个加载函数?
      2. 2.4.2. Input
      3. 2.4.3. Output
      4. 2.4.4. 在什么时间用哪个函数
    5. 2.5. Using URL data
      1. 2.5.1. url
      2. 2.5.2. route
      3. 2.5.3. params
    6. 2.6. 进行获取请求
    7. 2.7. Cookies and headers
    8. 2.8. Using parent data
    9. 2.9. Errors
    10. 2.10. Redirects
    11. 2.11. 使用 Promises 进行流式传输
    12. 2.12. 并行加载
    13. 2.13. 重新运行加载函数
      1. 2.13.1. Manual invalidation
  3. 3. Form actions
    1. 3.1. Default actions
    2. 3.2. Named actions
    3. 3.3. 解剖action
      1. 3.3.1. Validation errors
      2. 3.3.2. Redirects
    4. 3.4. Loading data
    5. 3.5. Progressive enhancement(渐进增强)
      1. 3.5.1. use:enhance
      2. 3.5.2. applyAction
      3. 3.5.3. 自定义事件监听器
    6. 3.6. 可选方案
    7. 3.7. GET vs POST
  4. 4. Page options
    1. 4.1. 预渲染
      1. 4.1.1. 预渲染服务器路由
      2. 4.1.2. 什么时候不做预渲染
      3. 4.1.3. 预渲染和服务器端渲染
      4. 4.1.4. 路由冲突
      5. 4.1.5. 故障排除
    2. 4.2. ssr
    3. 4.3. csr
    4. 4.4. trailingSlash
    5. 4.5. 配置
  5. 5. 状态管理
    1. 5.1. 避免在服务器上使用共享状态
    2. 5.2. 负载无副作用
    3. 5.3. 使用带有上下文的存储
    4. 5.4. 组件状态被保留
    5. 5.5. 在URL中存储状态
    6. 5.6. 在快照中存储短暂状态