狀態管理
如果您習慣建置僅限客戶端的應用程式,那麼跨越伺服器和客戶端的應用程式中的狀態管理可能會讓人感到畏懼。本節提供一些避免常見陷阱的技巧。
避免在伺服器上共享狀態
瀏覽器是有狀態的 - 當使用者與應用程式互動時,狀態會儲存在記憶體中。另一方面,伺服器是無狀態的 - 回應的內容完全由請求的內容決定。
在概念上是如此。實際上,伺服器通常是長期存在的,並由多個使用者共享。因此,重要的是不要將資料儲存在共享變數中。例如,請考慮以下程式碼
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
函式內部寫入 store,以便可以在元件中使用 store 值
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
函式中產生副作用 - 這樣您的應用程式會更容易理解。
在 context 中使用 stores
您可能會想知道,如果我們無法使用自己的 stores,我們如何能夠使用 $page.data
和其他 應用程式 stores。答案是,伺服器上的應用程式 stores 使用 Svelte 的 context API - store 會使用 setContext
附加到元件樹,當您訂閱時,會使用 getContext
擷取它。我們可以使用我們自己的 stores 來執行相同的操作
<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 呈現時,更新較深層頁面或元件中基於 context 的 store 值,不會影響父元件中的值,因為當 store 值更新時,父元件已經被呈現了。相反地,在客戶端(當啟用 CSR 時,這是預設值)該值將被傳播,並且層次結構中較高的元件、頁面和版面配置將會對新值做出反應。因此,為了避免在 hydration 期間狀態更新時出現值「閃爍」,通常建議將狀態向下傳遞到元件中,而不是向上傳遞。
如果您不使用 SSR(並且可以保證將來不需要使用 SSR),則可以安全地將狀態保留在共享模組中,而無需使用 context 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
prop (以及 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 提供了 快照,可讓您將元件狀態與歷史記錄項目相關聯。