跳到主要內容

核心概念

狀態管理

在 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 函式中產生副作用,這樣您的應用程式將更容易理解。

使用具有 context 的儲存

您可能會想知道,如果我們無法使用自己的儲存,我們如何能夠使用 $page.data 和其他 應用程式儲存。答案是伺服器上的應用程式儲存使用 Svelte 的 context 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 呈現時,更新較深層級頁面或元件中基於 context 的儲存的值,不會影響父元件的值,因為在儲存值更新時,它已經被呈現。相反地,在用戶端(在預設啟用的 CSR 中),值會被傳播,而層級中較高的元件、頁面和版面會對新值做出反應。因此,為了避免在水化期間狀態更新時值「閃爍」,通常建議將狀態向下傳遞到元件,而不是向上傳遞。

如果您沒有使用 SSR(並且可以保證您未來不需要使用 SSR),則您可以安全地將狀態保存在共用模組中,而無需使用 context 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 prop(以及延伸的 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') 以程式化方式設定它們。它們可以在 load 函式中透過 url 參數存取,並可以在元件中透過 $page.url.searchParams 存取。

在快照中儲存暫時狀態

某些 UI 狀態(例如「手風琴是否開啟?」)是可拋棄的,如果使用者導航離開或重新整理頁面,狀態遺失與否並不重要。在某些情況下,您確實希望資料在使用者導航到不同頁面並返回時仍然存在,但將狀態儲存在 URL 或資料庫中會過於繁瑣。為了解決此問題,SvelteKit 提供了 快照,讓您可以將元件狀態與歷史記錄條目關聯起來。