Управление состоянием
Если вы привыкли создавать клиентские приложения, управление состоянием в приложении, которое работает и на сервере, и на клиенте, может показаться сложным. В этом разделе приведены советы, как избежать некоторых распространённых проблем.
Избегайте общего состояния на сервере
Заголовок раздела «Избегайте общего состояния на сервере»Браузеры отслеживают состояние — оно сохраняется в памяти по мере взаимодействия пользователя с приложением. Серверы же, напротив, не имеют состояния — содержимое ответа полностью определяется содержимым запроса.
По крайней мере, в теории. На практике серверы часто работают длительное время и используются множеством пользователей. Поэтому важно не хранить данные в общих переменных. Например, рассмотрим этот код:
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') }; }}
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();
// НЕ ДЕЛАЙТЕ ТАК! user = { name: data.get('name'), embarrassingSecret: data.get('secret') }; }} satisfies Actions
Переменная user
является общей для всех, кто подключается к этому серверу. Если Алиса отправит неловкий секрет, а Боб посетит страницу после неё, Боб узнает секрет Алисы. Кроме того, когда Алиса вернётся на сайт позже в тот же день, сервер может перезапуститься, и её данные будут потеряны.
Вместо этого вам следует аутентифицировать пользователя с помощью куки
и сохранять данные в базе данных.
Отсутствие побочных эффектов в load
Заголовок раздела «Отсутствие побочных эффектов в load»По той же причине ваши функции load
должны быть чистыми — без побочных эффектов (за исключением разве что случайных console.log(...)
). Например, у вас может возникнуть соблазн записать данные в хранилище или глобальное состояние внутри функции load
, чтобы затем использовать это значение в ваших компонентах:
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());}
import { user } from '$lib/user';import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => { const response = await fetch('/api/user');
// НЕ ДЕЛАЙТЕ ТАК! user.set(await response.json());};
Как и в предыдущем примере, это помещает информацию одного пользователя в место, доступное всем пользователям. Вместо этого просто возвращайте данные…
/** @type {import('./$types').PageServerLoad} */export async function load({ fetch }) { const response = await fetch('/api/user');
return { user: await response.json() };}
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch }) => { const response = await fetch('/api/user');
return { user: await response.json() };};
…и передавайте их в компоненты, которым они нужны, или используйте page.data
.
Если вы не используете SSR (рендеринг на стороне сервера), то нет риска случайно предоставить данные одного пользователя другому. Однако вам всё равно следует избегать побочных эффектов в функциях load
— ваше приложение станет гораздо проще для понимания без них.
Использование состояния и хранилищ с контекстом
Заголовок раздела «Использование состояния и хранилищ с контекстом»Возможно, вам интересно, как мы можем использовать page.data
и другие состояния приложения (или хранилища приложения), если глобальное состояние использовать нельзя. Ответ в том, что состояние и хранилища приложения на сервере используют Context API Svelte — состояние (или хранилище) прикрепляется к дереву компонентов с помощью setContext
, а при подписке извлекается через getContext
. Мы можем сделать то же самое с нашим собственным состоянием:
<script> import { setContext } from 'svelte';
/** @type {import('./$types').LayoutProps} */ let { data } = $props();
// Передаем функцию, ссылающуюся на наше состояние, // в контекст для доступа дочерних компонентов setContext('user', () => data.user);</script>
<script lang="ts"> import { setContext } from 'svelte'; import type { LayoutProps } from './$types'; let { data }: LayoutProps = $props();
// Передаем функцию, ссылающуюся на наше состояние, // в контекст для доступа дочерних компонентов setContext('user', () => data.user);</script>
<script> import { getContext } from 'svelte';
// Получаем хранилище пользователя из контекста const user = getContext('user');</script>
<p>Приветствуем, {user().name}</p>
<script lang="ts"> import { getContext } from 'svelte';
// Получаем хранилище пользователя из контекста const user = getContext('user');</script>
<p>Приветствуем, {user().name}</p>
Обновление значения состояния на основе контекста на страницах или в компонентах более глубокого уровня во время рендеринга через SSR не повлияет на значение в родительском компоненте, так как он уже был отрендерен к моменту обновления значения состояния. В отличие от этого, на клиенте (когда включен CSR, что является значением по умолчанию) значение будет распространяться, и компоненты, страницы и макеты выше в иерархии будут реагировать на новое значение. Поэтому, чтобы избежать «мигания» значений во время обновления состояния при гидратации, обычно рекомендуется передавать состояние вниз в компоненты, а не вверх.
Если вы не используете SSR (и можете гарантировать, что не будете использовать SSR в будущем), то можете безопасно хранить состояние в общем модуле, не используя Context API.
Состояние компонентов и страниц сохраняется
Заголовок раздела «Состояние компонентов и страниц сохраняется»Когда вы перемещаетесь по приложению, SvelteKit повторно использует существующие компоненты макетов и страниц. Например, если у вас есть маршрут вида…
<script> /** @type {import('./$types').PageProps} */ let { data } = $props();
// ЭТОТ КОД СОДЕРЖИТ ОШИБКИ! const wordCount = data.content.split(' ').length; const estimatedReadingTime = wordCount / 250;</script>
<header> <h1>{data.title}</h1> <p>Время чтения: {Math.round(estimatedReadingTime)} мин.</p></header>
<div>{@html data.content}</div>
<script lang="ts"> import type { PageProps } from './$types';
let { data }: PageProps = $props();
// ЭТОТ КОД СОДЕРЖИТ ОШИБКИ! const wordCount = data.content.split(' ').length; const estimatedReadingTime = wordCount / 250;</script>
<header> <h1>{data.title}</h1> <p>Время чтения: {Math.round(estimatedReadingTime)} мин.</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 {import('./$types').PageProps} */ let { data } = $props();
let wordCount = $derived(data.content.split(' ').length); let estimatedReadingTime = $derived(wordCount / 250);</script>
<script lang="ts"> import type { PageProps } from './$types';
let { data }: PageProps = $props();
let wordCount = $derived(data.content.split(' ').length); let estimatedReadingTime = $derived(wordCount / 250);</script>
Повторное использование компонентов таким образом означает, что такие вещи как состояние прокрутки боковой панели сохраняются, и вы можете легко анимировать изменения значений. В случае, если вам действительно нужно полностью уничтожить и пересоздать компонент при навигации, вы можете использовать этот подход:
<script> import { page } from '$app/state';</script>
{#key page.url.pathname} <BlogPost title={data.title} content={data.title} />{/key}
Хранение состояния в URL
Заголовок раздела «Хранение состояния в URL»Если у вас есть состояние, которое должно сохраняться при перезагрузке и/или влиять на SSR (например, фильтры или правила сортировки в таблице), параметры URL (например, ?sort=price&order=ascending
) — это хорошее место для их хранения. Вы можете поместить их в атрибуты <a href="...">
или <form action="...">
, либо установить программно через goto('?key=value')
. Доступ к ним можно получить внутри функций load
через параметр url
, а в компонентах — через page.url.searchParams
.
Хранение временного состояния в снимках
Заголовок раздела «Хранение временного состояния в снимках»Некоторые состояния интерфейса, такие как «открыт ли аккордеон?», являются временными — если пользователь уйдёт со страницы или обновит её, не страшно, если состояние будет потеряно. В некоторых случаях вам нужно, чтобы данные сохранялись, если пользователь перейдёт на другую страницу и вернётся обратно, но хранение состояния в URL или базе данных было бы излишним. Для этого SvelteKit предоставляет снимки состояния, которые позволяют связать состояние компонента с записью в истории.