動態

詳情 返回 返回

SvelteKit 最新中文文檔教程(6)—— 狀態管理 - 動態 詳情

前言

Svelte,一個語法簡潔、入門容易,面向未來的前端框架。

從 Svelte 誕生之初,就備受開發者的喜愛,根據統計,從 2019 年到 2024 年,連續 6 年一直是開發者最感興趣的前端框架 No.1

image.png

Svelte 以其獨特的編譯時優化機制著稱,具有輕量級高性能易上手等特性,非常適合構建輕量級 Web 項目

為了幫助大家學習 Svelte,我同時搭建了 Svelte 最新的中文文檔站點。

如果需要進階學習,也可以入手我的小冊《Svelte 開發指南》,語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

狀態管理

如果您習慣於構建僅客户端的應用程序,在跨服務端和客户端的應用中進行狀態管理可能會讓人感到望而生畏。本節提供了一些避免常見陷阱的建議。

避免在服務端共享狀態

瀏覽器是有狀態的 — 狀態在用户與應用程序交互時存儲在內存中。相反,服務端是無狀態的 — 響應的內容完全取決於請求的內容。

從概念上來説是這樣的。實際上,服務端通常是長期運行的,並由多個用户共享。因此,避免在共享變量中存儲數據非常重要。例如,考慮以下代碼:

// @errors: 7034 7005
/// file: +page.server.js
let user;

/** @type {import('./$types').PageServerLoad} */
export function load() {
	return { user };
}

/** @satisfies {import('./$types').Actions} */
export const actions = {
	default: async ({ request }) => {
		const data = await request.formData();

		// 永遠不要這樣做!
		user = {
			name: data.get('name'),
			embarrassingSecret: data.get('secret')
		};
	}
};

user 變量被所有連接到這個服務器的人共享。如果 Alice 提交了一個尷尬的秘密,而 Bob 在她之後訪問頁面,Bob 就會知道 Alice 的秘密。此外,當 Alice 當天晚些時候返回網站時,服務器可能已經重啓,丟失了她的數據。

相反,您應該使用 cookies 對用户進行認證,並將數據持久化到數據庫中。

load 函數中不要有副作用

出於同樣的原因,您的 load 函數應該是純函數 — 沒有副作用(除了偶爾的 console.log(...))。例如,您可能會想在 load 函數中寫入 store 或全局狀態,以便在組件中使用這個值:

/// file: +page.js
// @filename: ambient.d.ts
declare module '$lib/user' {
	export const user: { set: (value: any) => void };
}

// @filename: index.js
// ---cut---
import { user } from '$lib/user';

/** @type {import('./$types').PageLoad} */
export async function load({ fetch }) {
	const response = await fetch('/api/user');

	// 永遠不要這樣做!
	user.set(await response.json());
}

與前面的例子一樣,這將一個用户的信息放在了所有用户共享的地方。相反,應該直接返回數據...

/// file: +page.js
/** @type {import('./$types').PageServerLoad} */
export async function load({ fetch }) {
	const response = await fetch('/api/user');

+++	return {
		user: await response.json()
	};+++
}

...然後將它傳遞給需要它的組件,或使用 page.data

如果您不使用 SSR,那麼就不會有意外將一個用户數據暴露給另一個用户的風險。但您仍然應該避免在 load 函數中產生副作用 — 這樣您的應用程序會更容易理解。

使用帶上下文的狀態和 stores

您可能會疑惑,如果我們不能使用全局狀態,我們如何使用 page.data 和其他 app 狀態(或 app stores)。答案是 app 狀態和 app stores 在服務端使用 Svelte 的 context API — 狀態(或 store)通過 setContext 附加到組件樹上,當您訂閲時,通過 getContext 檢索它。我們可以用同樣的方式處理我們自己的狀態:

<!--- file: src/routes/+layout.svelte --->
<script>
	import { setContext } from 'svelte';

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

	// 將引用我們狀態的函數
	// 傳遞給上下文,供子組件訪問
	setContext('user', () => data.user);
</script>
<!--- file: src/routes/user/+page.svelte --->
<script>
	import { getContext } from 'svelte';

	// 從上下文中獲取 user store
	const user = getContext('user');
</script>

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

[!NOTE] 我們傳遞一個函數到 setContext 以保持跨邊界的響應性。在這裏閲讀更多相關信息

[!LEGACY]
您也可以使用 svelte/store 中的 stores 來實現這一點,但在使用 Svelte 5 時,建議使用通用響應性。

在通過 SSR 渲染頁面時,在更深層次的頁面或組件中更新基於上下文的狀態值不會影響父組件中的值,因為在狀態值更新時父組件已經被渲染完成。

相比之下,在客户端(當啓用 CSR 時,這是默認設置)這個值會被傳播,層級更高的組件、頁面和佈局會對新值作出反應。因此,為了避免在水合過程中狀態更新時值"閃爍",通常建議將狀態向下傳遞給組件,而不是向上傳遞。

如果您不使用 SSR(並且可以保證將來也不需要使用 SSR),那麼您可以安全地將狀態保存在共享模塊中,而無需使用 context API。

組件和頁面狀態會被保留

當您在應用程序中導航時,SvelteKit 會複用現有的佈局和頁面組件。例如,如果您有這樣的路由...

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

	// 這段代碼有 BUG!
	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 也不會重新計算。

相反,我們需要使這個值變成響應式

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

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

[!NOTE] 如果您需要在導航後重新運行 onMountonDestroy 中的代碼,您可以分別使用 afterNavigate 和 beforeNavigate。

像這樣複用組件意味着側邊欄滾動狀態等會被保留,您可以輕鬆地在變化的值之間實現動畫效果。如果您確實需要在導航時完全銷燬並重新掛載一個組件,您可以使用這種模式:

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

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

在 URL 中存儲狀態

如果您有需要讓狀態能夠在頁面重新加載後依然保持,比如表格上的過濾器或排序規則,URL 搜索參數(如 ?sort=price&order=ascending)是存儲它們的好地方。您可以把它們放在 <a href="..."><form action="..."> 屬性中,或通過 goto('?key=value') 以編程的方式設置它們。它們可以在 load 函數中通過 url 參數訪問,在組件中通過 page.url.searchParams 訪問。

在快照中存儲臨時狀態

某些 UI 狀態,比如"列表是否展開?",是可以丟棄的 — 如果用户導航離開或刷新頁面,狀態丟失並不要緊。在某些情況下,您確實希望在用户導航到另一個頁面並返回時數據能夠保持,但將狀態存儲在 URL 或數據庫中會顯得過度。對於這種情況,SvelteKit 提供了 快照,讓您可以將組件狀態與歷史記錄條目關聯起來。

Svelte 中文文檔

點擊查看中文文檔 - SvelteKit 狀態管理。

系統學習 Svelte,歡迎入手小冊《Svelte 開發指南》。語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

此外我還寫過 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答讀者問等 14 個系列文章, 全系列文章目錄:https://github.com/mqyqingfeng/Blog

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

user avatar juanerma 頭像 freeman_tian 頭像 jingdongkeji 頭像 kobe_fans_zxc 頭像 guixiangyyds 頭像 Z-HarOld 頭像 romanticcrystal 頭像 joe235 頭像 yulong1992 頭像 it1042290135 頭像 aser1989 頭像 haixiudezhusun 頭像
點贊 35 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.