跳至主要内容

核心概念

状态管理

在 GitHub 上编辑此页面

如果你习惯构建仅限客户端的应用,那么跨越服务器和客户端的应用中的状态管理可能看起来令人生畏。本节提供了避免一些常见陷阱的提示。

避免在服务器上共享状态

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

从概念上来说是这样。实际上,服务器通常是长期存在的,并且由多个用户共享。因此,重要的是不要将数据存储在共享变量中。例如,考虑以下代码

+page.server.js
ts
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')
};
}
}
+page.server.ts
ts
import type { PageServerLoad, Actions } from './$types';
let user;
export const load: PageServerLoad = () => {
return { user };
};
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
// NEVER DO THIS!
user = {
name: data.get('name'),
embarrassingSecret: data.get('secret'),
};
},
} satisfies Actions;

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

相反,你应该使用 cookies验证用户并持久化数据到数据库。

load 中没有副作用

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

+page.js
ts
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.ts
ts
import { user } from '$lib/user';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/user');
// NEVER DO THIS!
user.set(await response.json());
};

与之前的示例一样,这会将一个用户的信息放入所有用户共享的位置。相反,只需返回数据...

+page.js
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
<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/+layout.svelte
<script lang="ts">
	import { setContext } from 'svelte';
	import { writable } from 'svelte/store';
	
	import type { LayoutData } from './$types';
	
	export let data: LayoutData;
	
	// 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
<script>
	import { getContext } from 'svelte';

	// Retrieve user store from context
	const user = getContext('user');
</script>

<p>Welcome {$user.name}</p>
src/routes/user/+page.svelte
<script lang="ts">
	import { getContext } from 'svelte';
	
	// Retrieve user store from context
	const user = getContext('user');
</script>

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

在通过 SSR 渲染页面时,更新更深层级页面或组件中基于上下文的存储的值不会影响父组件中的值,因为在存储值更新时它已经渲染完毕。相比之下,在客户端(在 CSR 启用时,这是默认设置),该值将被传播,并且层次结构中较高的组件、页面和布局将对新值做出反应。因此,为了避免在水化期间状态更新时值“闪烁”,通常建议将状态向下传递到组件中,而不是向上传递。

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

组件和页面状态被保留

当你浏览应用程序时,SvelteKit 会重复使用现有的布局和页面组件。例如,如果你有这样的路由...

src/routes/blog/[slug]/+page.svelte
<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>
src/routes/blog/[slug]/+page.svelte
<script lang="ts">
	import type { PageData } from './$types';
	
	export let data: PageData;
	
	// 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 属性(以及扩展的 data.titledata.content)将更新(就像任何其他 Svelte 组件一样),并且由于代码没有重新运行,生命周期方法(如 onMountonDestroy)不会重新运行,并且 estimatedReadingTime 不会被重新计算。

相反,我们需要使该值 reactive

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

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

如果 onMountonDestroy 中的代码在导航后必须再次运行,则你可以分别使用 afterNavigatebeforeNavigate

像这样重复使用组件意味着诸如侧边栏滚动状态之类的内容将被保留,并且您可以在更改的值之间轻松执行动画。如果您确实需要在导航时完全销毁并重新挂载组件,则可以使用此模式

{#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 提供了 快照,它允许您将组件状态与历史记录条目关联起来。

上一个 页面选项