Skip to main content

State management

如果您习惯于构建客户端应用程序,那么在跨越服务器和客户端的应用程序中进行状态管理可能会显得令人生畏。本节提供了一些避免常见问题的技巧。

避免在服务器上共享状态

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

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

+page.server
let let user: anyuser;

/** @type {import('./$types').PageServerLoad} */
export function 
function load(): {
    user: any;
}
@type{import('./$types').PageServerLoad}
load
() {
return { user: anyuser }; } /** @satisfies {import('./$types').Actions} */ export const
const actions: {
    default: ({ request }: {
        request: any;
    }) => Promise<void>;
}
@satisfies{import('./$types').Actions}
actions
= {
default: ({ request }: {
    request: any;
}) => Promise<void>
default
: async ({ request: anyrequest }) => {
const const data: anydata = await request: anyrequest.formData(); // NEVER DO THIS! let user: anyuser = { name: anyname: const data: anydata.get('name'), embarrassingSecret: anyembarrassingSecret: const data: anydata.get('secret') }; } }
import type { 
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
,
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions
} from './$types';
let let user: anyuser; export const const load: PageServerLoadload:
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
= () => {
return { user: anyuser }; }; export const
const actions: {
    default: ({ request }: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions
= {
default: ({ request }: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>default: async ({ request: Request

The original request object.

request
}) => {
const const data: FormDatadata = await request: Request

The original request object.

request
.Body.formData(): Promise<FormData>formData();
// NEVER DO THIS! let user: anyuser = { name: FormDataEntryValue | nullname: const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullget('name'), embarrassingSecret: FormDataEntryValue | nullembarrassingSecret: const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullget('secret') }; } } satisfies
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
    [x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions

用户变量user被所有连接到此服务器的用户共享。如果 Alice 提交了一个令人尴尬的秘密,而 Bob 在她之后访问了页面,Bob 就会知道 Alice 的秘密。此外,当 Alice 在当天稍后返回网站时,服务器可能已经重新启动,导致她丢失数据。

相反,您应该使用身份验证来验证用户,并使用cookies将数据持久化到数据库中。

无副作用

由于相同的原因,您的 load 函数应该是 纯函数 — 没有副作用(除了偶尔的 console.log(...))。例如,您可能会在 load 函数内部写入 store 或全局状态,以便您可以在组件中使用该值:

+page
import { 
const user: {
    set: (value: any) => void;
}
user
} from '$lib/user';
/** @type {import('./$types').PageLoad} */ export async function function load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>
@type{import('./$types').PageLoad}
load
({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here

fetch
}) {
const const response: Responseresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch('/api/user'); // NEVER DO THIS!
const user: {
    set: (value: any) => void;
}
user
.set: (value: any) => voidset(await const response: Responseresponse.Body.json(): Promise<any>json());
}
import { 
const user: {
    set: (value: any) => void;
}
user
} from '$lib/user';
import type { type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad } from './$types'; export const const load: PageLoadload: type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad = async ({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here

fetch
}) => {
const const response: Responseresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch('/api/user'); // NEVER DO THIS!
const user: {
    set: (value: any) => void;
}
user
.set: (value: any) => voidset(await const response: Responseresponse.Body.json(): Promise<any>json());
};

与前面的例子一样,这会将一个用户的信息放在所有用户共享的地方。相反,只需返回数据...

+page
/** @type {import('./$types').PageServerLoad} */
export async function 
function load({ fetch }: {
    fetch: any;
}): Promise<{
    user: any;
}>
@type{import('./$types').PageServerLoad}
load
({ fetch: anyfetch }) {
const const response: anyresponse = await fetch: anyfetch('/api/user'); return { user: anyuser: await const response: anyresponse.json() }; }
import type { 
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
} from './$types';
export const const load: PageServerLoadload:
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad
= async ({
fetch: {
    (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
    (input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}

fetch is equivalent to the native fetch web API, with a few additional features:

  • It can be used to make credentialed requests on the server, as it inherits the cookie and authorization headers for the page request.
  • It can make relative requests on the server (ordinarily, fetch requires a URL with an origin when used in a server context).
  • Internal requests (e.g. for +server.js routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
  • During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the text and json methods of the Response object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
  • During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.

You can learn more about making credentialed requests with cookies here.

fetch
}) => {
const const response: Responseresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch('/api/user'); return { user: anyuser: await const response: Responseresponse.Body.json(): Promise<any>json() }; };

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

如果您不使用 SSR,那么就不会存在意外将一个用户的数据暴露给另一个用户的风险。但您仍然应该避免在您的load函数中出现副作用——没有它们,您的应用程序将更容易理解。

使用状态和存储与上下文

您可能会想知道,如果我们不能使用全局状态,我们是如何使用page.data和其他应用状态(或应用存储)。答案是服务器上的应用状态和应用存储使用 Svelte 的上下文 API——状态(或存储)通过setContext附加到组件树,当您订阅时,您可以使用getContext检索它。我们也可以用我们自己的状态做同样的事情:

src/routes/+layout
<script>
	import { setContext } from 'svelte';

	/** @type {import('./$types').LayoutProps} */
	let { data } = $props();

	// Pass a function referencing our state
	// to the context for child components to access
	setContext('user', () => data.user);
</script>
<script lang="ts">
	import { setContext } from 'svelte';
	import type { LayoutProps } from './$types';
	let { data }: LayoutProps = $props();

	// Pass a function referencing our state
	// to the context for child components to access
	setContext('user', () => data.user);
</script>
src/routes/user/+page
<script>
	import { getContext } from 'svelte';

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

<p>Welcome {user().name}</p>
<script lang="ts">
	import { getContext } from 'svelte';

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

<p>Welcome {user().name}</p>

[!注意] 我们将一个函数传递给setContext以保持边界之间的响应性。了解更多信息这里

[!旧版] 您还使用来自 svelte/store 的存储库,但使用 Svelte 5 时,建议使用通用响应性。

更新在页面通过 SSR 渲染过程中更深层次的页面或组件的基于上下文的状态值时,不会影响父组件中的值,因为父组件在状态值更新之前已经渲染完成。相比之下,在客户端(当启用 CSR 时,这是默认设置)值将被传播,并且层次结构中更高的组件、页面和布局将响应新值。因此,为了避免在数据同步过程中状态更新时值“闪烁”,通常建议将状态向下传递到组件,而不是向上传递。

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

组件和页面状态已保留

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

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

	// 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>
<script lang="ts">
	import type { PageProps } from './$types';

	let { data }: PageProps = $props();

	// 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
<script>
	/** @type {import('./$types').PageProps} */
	let { data } = $props();

	let wordCount = $derived(data.content.split(' ').length);
	let estimatedReadingTime = $derived(wordCount / 250);
</script>
<script lang="ts">
	import type { PageProps } from './$types';

	let { data }: PageProps = $props();

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

[!注意] 如果您在 onMountonDestroy 中的代码在导航后需要再次运行,可以使用 afterNavigatebeforeNavigate 分别。

组件的重用意味着侧边栏滚动状态等类似功能得以保留,并且可以轻松地在不同的值之间进行动画转换。在你确实需要在导航时完全销毁并重新挂载组件的情况下,可以使用此模式:

<script>
	import { page } from '$app/state';
</script>

{#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')程序化设置。它们可以在load函数内部通过url参数访问,也可以在组件内部通过page.url.searchParams访问。

存储临时状态到快照中

某些 UI 状态,例如“手风琴是否打开?”,是可丢弃的——如果用户离开或刷新页面,状态丢失无关紧要。在某些情况下,当用户导航到不同的页面并返回时,您可能希望数据持续存在,但将状态存储在 URL 或数据库中会过于冗余。为此,SvelteKit 提供了快照,允许您将组件状态与历史记录条目关联。

Edit this page on GitHub llms.txt