核心概念
状态管理
在 GitHub 上编辑此页面如果你习惯构建仅限客户端的应用,那么跨越服务器和客户端的应用中的状态管理可能看起来令人生畏。本节提供了避免一些常见陷阱的提示。
避免在服务器上共享状态永久链接
浏览器是有状态的 — 当用户与应用程序交互时,状态存储在内存中。另一方面,服务器是无状态的 — 响应的内容完全由请求的内容决定。
从概念上来说是这样。实际上,服务器通常是长期存在的,并且由多个用户共享。因此,重要的是不要将数据存储在共享变量中。例如,考虑以下代码
ts
letuser ;/** @type {import('./$types').PageServerLoad} */export functionload () {return {user };}/** @type {import('./$types').Actions} */export constactions = {default : async ({request }) => {constdata = awaitrequest .formData ();// NEVER DO THIS!user = {name :data .get ('name'),embarrassingSecret :data .get ('secret')};}}
ts
import type {PageServerLoad ,Actions } from './$types';letuser ;export constload :PageServerLoad = () => {return {user };};export constactions = {default : async ({request }) => {constdata = awaitrequest .formData ();// NEVER DO THIS!user = {name :data .get ('name'),embarrassingSecret :data .get ('secret'),};},} satisfiesActions ;
user
变量由连接到此服务器的所有人共享。如果 Alice 提交了一个令人尴尬的秘密,而 Bob 在她之后访问了该页面,那么 Bob 将知道 Alice 的秘密。此外,当 Alice 在当天晚些时候返回该网站时,服务器可能已经重新启动,从而丢失了她的数据。
相反,你应该使用 cookies
验证用户并持久化数据到数据库。
load 中没有副作用永久链接
出于同样的原因,你的 load
函数应该是纯函数 — 没有副作用(除了偶尔的 console.log(...)
)。例如,你可能会想在 load
函数中写入一个存储,以便可以在组件中使用存储值
ts
import {user } from '$lib/user';/** @type {import('./$types').PageLoad} */export async functionload ({fetch }) {constresponse = awaitfetch ('/api/user');// NEVER DO THIS!user .set (awaitresponse .json ());}
ts
import {user } from '$lib/user';import type {PageLoad } from './$types';export constload :PageLoad = async ({fetch }) => {constresponse = awaitfetch ('/api/user');// NEVER DO THIS!user .set (awaitresponse .json ());};
与之前的示例一样,这会将一个用户的信息放入所有用户共享的位置。相反,只需返回数据...
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
检索它。我们可以对我们自己的存储执行相同操作
<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>
<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>
<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>
在通过 SSR 渲染页面时,更新更深层级页面或组件中基于上下文的存储的值不会影响父组件中的值,因为在存储值更新时它已经渲染完毕。相比之下,在客户端(在 CSR 启用时,这是默认设置),该值将被传播,并且层次结构中较高的组件、页面和布局将对新值做出反应。因此,为了避免在水化期间状态更新时值“闪烁”,通常建议将状态向下传递到组件中,而不是向上传递。
如果你没有使用 SSR(并且可以保证将来不需要使用 SSR),那么你可以安全地将状态保存在共享模块中,而不使用上下文 API。
组件和页面状态被保留永久链接
当你浏览应用程序时,SvelteKit 会重复使用现有的布局和页面组件。例如,如果你有这样的路由...
<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>
<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.title
和 data.content
)将更新(就像任何其他 Svelte 组件一样),并且由于代码没有重新运行,生命周期方法(如 onMount
和 onDestroy
)不会重新运行,并且 estimatedReadingTime
不会被重新计算。
相反,我们需要使该值 reactive
<script>
/** @type {import('./$types').PageData} */
export let data;
$: wordCount = data.content.split(' ').length;
$: estimatedReadingTime = 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')
以编程方式设置它们。它们可以通过 url
参数在 load
函数内部访问,并且可以通过 $page.url.searchParams
在组件内部访问。
在快照中存储临时状态永久链接
某些 UI 状态(例如“手风琴是否打开?”)是可丢弃的——如果用户导航离开或刷新页面,则状态丢失无关紧要。在某些情况下,您确实希望数据在用户导航到不同页面并返回时仍然存在,但在 URL 或数据库中存储状态将是过度的。为此,SvelteKit 提供了 快照,它允许您将组件状态与历史记录条目关联起来。