Skip to main content

状态管理

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

¥If you’re used to building client-only apps, state management in an app that spans server and client might seem intimidating. This section provides tips for avoiding some common gotchas.

避免在服务器上共享状态(Avoid shared state on the server)

¥Avoid shared state on the server

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

¥Browsers are stateful — state is stored in memory as the user interacts with the application. Servers, on the other hand, are stateless — the content of the response is determined entirely by the content of the request.

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

¥Conceptually, that is. In reality, servers are often long-lived and shared by multiple users. For that reason it’s important not to store data in shared variables. For example, consider this code:

+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 当天晚些时候返回网站时,服务器可能已重新启动,从而丢失了她的数据。

¥The user variable is shared by everyone who connects to this server. If Alice submitted an embarrassing secret, and Bob visited the page after her, Bob would know Alice’s secret. In addition, when Alice returns to the site later in the day, the server may have restarted, losing her data.

相反,你应该使用 cookies 对用户进行身份验证,并将数据保存到数据库中。

¥Instead, you should authenticate the user using cookies and persist the data to a database.

加载时无副作用(No side-effects in load)

¥No side-effects in load

出于同样的原因,你的 load 函数应该是纯粹的 — 没有副作用(偶尔的 console.log(...) 除外)。例如,你可能想在 load 函数内写入存储或全局状态,以便可以在组件中使用该值:

¥For the same reason, your load functions should be pure — no side-effects (except maybe the occasional console.log(...)). For example, you might be tempted to write to a store or global state inside a load function so that you can use the value in your components:

+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());
};

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

¥As with the previous example, this puts one user’s information in a place that is shared by all users. Instead, just return the data...

+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

¥...and pass it around to the components that need it, or use page.data.

如果你不使用 SSR,那么就不会有意外将一个用户的数据暴露给另一个用户的风险。但你仍应避免 load 函数中的副作用 - 如果没有它们,你的应用将更容易推断。

¥If you’re not using SSR, then there’s no risk of accidentally exposing one user’s data to another. But you should still avoid side-effects in your load functions — your application will be much easier to reason about without them.

使用状态和存储上下文(Using state and stores with context)

¥Using state and stores with context

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

¥You might wonder how we’re able to use page.data and other app state (or app stores) if we can’t use global state. The answer is that app state and app stores on the server use Svelte’s context API — the state (or store) is attached to the component tree with setContext, and when you subscribe you retrieve it with getContext. We can do the same thing with our own state:

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 以保持跨边界的反应性。阅读有关它的更多信息 此处

¥[!NOTE] We’re passing a function into setContext to keep reactivity across boundaries. Read more about it here

Legacy mode

你也可以为此使用来自 svelte/store 的存储,但在使用 Svelte 5 时,建议改用通用反应性。

¥[!LEGACY] You also use stores from svelte/store for this, but when using Svelte 5 it is recommended to make use of universal reactivity instead.

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

¥Updating the value of context-based state in deeper-level pages or components while the page is being rendered via SSR will not affect the value in the parent component because it has already been rendered by the time the state value is updated. In contrast, on the client (when CSR is enabled, which is the default) the value will be propagated and components, pages, and layouts higher in the hierarchy will react to the new value. Therefore, to avoid values ‘flashing’ during state updates during hydration, it is generally recommended to pass state down into components rather than up.

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

¥If you’re not using SSR (and can guarantee that you won’t need to use SSR in future) then you can safely keep state in a shared module, without using the context API.

组件和页面状态被保留(Component and page state is preserved)

¥Component and page state is preserved

当你在应用中导航时,SvelteKit 会重用现有的布局和页面组件。例如,如果你有这样的路由……

¥When you navigate around your application, SvelteKit reuses existing layout and page components. For example, if you have a route like this...

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 prop(以及扩展的 data.titledata.content)将更新(就像任何其他 Svelte 组件一样),并且由于代码没有重新运行,因此生命周期方法(如 onMountonDestroy)不会重新运行,estimatedReadingTime 也不会重新计算。

¥...then navigating from /blog/my-short-post to /blog/my-long-post won’t cause the layout, page and any other components within to be destroyed and recreated. Instead the data prop (and by extension data.title and data.content) will update (as it would with any other Svelte component) and, because the code isn’t rerunning, lifecycle methods like onMount and onDestroy won’t rerun and estimatedReadingTime won’t be recalculated.

相反,我们需要将值设为 reactive

¥Instead, we need to make the value 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

¥[!NOTE] If your code in onMount and onDestroy has to run again after navigation you can use afterNavigate and beforeNavigate respectively.

重复使用这样的组件意味着侧边栏滚动状态等内容会被保留,并且你可以轻松地在更改值之间进行动画处理。如果你确实需要在导航时完全销毁并重新安装组件,你可以使用这种模式:

¥Reusing components like this means that things like sidebar scroll state are preserved, and you can easily animate between changing values. In the case that you do need to completely destroy and remount a component on navigation, you can use this pattern:

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

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

将状态存储在 URL(Storing state in the URL)

¥Storing state in the URL

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

¥If you have state that should survive a reload and/or affect SSR, such as filters or sorting rules on a table, URL search parameters (like ?sort=price&order=ascending) are a good place to put them. You can put them in <a href="..."> or <form action="..."> attributes, or set them programmatically via goto('?key=value'). They can be accessed inside load functions via the url parameter, and inside components via page.url.searchParams.

将短暂状态存储在快照中(Storing ephemeral state in snapshots)

¥Storing ephemeral state in snapshots

某些 UI 状态(例如 ‘它使用’)是一次性的 - 如果用户离开或刷新页面,则状态丢失并不重要。在某些情况下,如果用户导航到其他页面并返回,你确实希望数据能够持久,但将状态存储在 URL 或数据库中会有些过头。为此,SvelteKit 提供了 snapshots,它允许你将组件状态与历史记录条目关联起来。

¥Some UI state, such as ‘is the accordion open?’, is disposable — if the user navigates away or refreshes the page, it doesn’t matter if the state is lost. In some cases, you do want the data to persist if the user navigates to a different page and comes back, but storing the state in the URL or in a database would be overkill. For this, SvelteKit provides snapshots, which let you associate component state with a history entry.

上一页 下一页