状态管理
如果您习惯于构建仅限客户端的应用程序,则跨越服务器和客户端的应用程序中的状态管理可能看起来令人生畏。本节提供了避免一些常见问题的技巧。
避免在服务器上使用共享状态
浏览器是有状态的——状态存储在内存中,用户与应用程序交互时会发生变化。另一方面,服务器是无状态的——响应的内容完全由请求的内容决定。
从概念上讲是这样的。实际上,服务器通常是长期存在的,并且由多个用户共享。因此,重要的是不要将数据存储在共享变量中。例如,考虑以下代码
let let user: any
user;
/** @type {import('./$types').PageServerLoad} */
export function function load(): {
user: any;
}
load() {
return { user: any
user };
}
/** @satisfies {import('./$types').Actions} */
export const const actions: {
default: ({ request }: {
request: any;
}) => Promise<void>;
}
actions = {
default: ({ request }: {
request: any;
}) => Promise<void>
default: async ({ request: any
request }) => {
const const data: any
data = await request: any
request.formData();
// NEVER DO THIS!
let user: any
user = {
name: any
name: const data: any
data.get('name'),
embarrassingSecret: any
embarrassingSecret: const data: any
data.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: any
user;
export const const load: PageServerLoad
load: 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: any
user };
};
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: FormData
data = await request: Request
The original request object
request.Body.formData(): Promise<FormData>
formData();
// NEVER DO THIS!
let user: any
user = {
name: FormDataEntryValue | null
name: const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('name'),
embarrassingSecret: FormDataEntryValue | null
embarrassingSecret: const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('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 中不使用副作用
出于同样的原因,您的 load
函数应是纯函数——没有副作用(除了偶尔的 console.log(...)
)。例如,您可能会想在 load
函数内部写入存储,以便您可以在组件中使用存储值
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>>
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: Response
response = 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) => void
set(await const response: Response
response.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: PageLoad
load: 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: Response
response = 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) => void
set(await const response: Response
response.Body.json(): Promise<any>
json());
};
与前面的示例一样,这会将一个用户的的信息放在一个由所有用户共享的地方。相反,只需返回数据...
/** @type {import('./$types').PageServerLoad} */
export async function function load({ fetch }: {
fetch: any;
}): Promise<{
user: any;
}>
load({ fetch: any
fetch }) {
const const response: any
response = await fetch: any
fetch('/api/user');
return {
user: any
user: await const response: any
response.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: PageServerLoad
load: 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: Response
response = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch('/api/user');
return {
user: any
user: await const response: Response
response.Body.json(): Promise<any>
json()
};
};
...并将其传递给需要它的组件,或使用 $page.data
。
如果您没有使用 SSR,那么就不会有意外地将一个用户的的数据暴露给另一个用户的风险。但是您仍然应该避免在 load
函数中使用副作用——没有副作用的应用程序将更容易理解。
使用带有上下文的存储
您可能想知道如果我们不能使用自己的存储,我们如何才能使用 $page.data
和其他 应用程序存储。答案是服务器上的应用程序存储使用 Svelte 的 上下文 API——存储使用 setContext
附加到组件树,当您订阅时,您使用 getContext
检索它。我们可以对自己的存储做同样的事情
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
/** @type {{ data: import('./$types').LayoutData }} */
let { data } = $props();
// Create a store and update it when necessary...
const user = writable(data.user);
$effect.pre(() => {
user.set(data.user);
});
// ...and add it to the context for child components to access
setContext('user', user);
</script>
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
import type { LayoutData } from './$types';
let { data }: { data: LayoutData } = $props();
// Create a store and update it when necessary...
const user = writable(data.user);
$effect.pre(() => {
user.set(data.user);
});
// ...and add it to the context for child components to access
setContext('user', user);
</script>
<script>
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 会重用现有的布局和页面组件。例如,如果您有如下所示的路由...
<script>
/** @type {{ data: import('./$types').PageData }} */
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 { PageData } from './$types';
let { data }: { data: PageData } = $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.title
和 data.content
)将更新(就像任何其他 Svelte 组件一样),并且由于代码没有重新运行,因此像 onMount
和 onDestroy
这样的生命周期方法不会重新运行,并且 estimatedReadingTime
不会被重新计算。
相反,我们需要使值 具有响应性
<script>
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();
let wordCount = $state(data.content.split(' ').length);
let estimatedReadingTime = $derived(wordCount / 250);
</script>
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let wordCount = $state(data.content.split(' ').length);
let estimatedReadingTime = $derived(wordCount / 250);
</script>
如果导航后您的
onMount
和onDestroy
中的代码必须再次运行,您可以分别使用 afterNavigate 和 beforeNavigate。
像这样重用组件意味着侧边栏滚动状态等内容将被保留,并且您可以轻松地在变化的值之间进行动画。如果您确实需要在导航时完全销毁和重新挂载组件,则可以使用此模式
{#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 提供了 快照,它允许您将组件状态与历史记录条目关联。