Svelte

Глобальное состояние в модулях и подводные камни SSR

Разработчик выносит текущего пользователя в модуль: export const user = `$state({ name: '' })`. На локальной машине всё работает. После деплоя приходит баг-репорт: пользователь Анна иногда видит имя Бориса. Причина в том, что на сервере один процесс обслуживает оба запроса, и переменная модуля одна на всех. Запрос Бориса перезаписал user, а запрос Анны прочитал чужое значение. Это классический подводный камень SSR, и понимание его границ отделяет рабочий код от утечки данных.

  • Утечка сессии: пользовательские данные в переменной модуля смешиваются между одновременными запросами на сервере
  • Кеши: общий кеш в модуле уместен для одинаковых для всех данных, но не для персональных
  • Конфигурация: неизменяемые настройки приложения в модуле безопасны, потому что одинаковы для всех запросов
  • Корзина и предпочтения: персональное состояние держат в контексте на сервере, чтобы оно не утекало между пользователями

Предварительные знания

  • Понимание универсальной реактивности рун: `$state` работает и вне компонента в .svelte.ts
  • Базовое представление о том, что на сервере один процесс обслуживает много запросов одновременно
  • Знание Context API: setContext и getContext для состояния, привязанного к дереву

Общее состояние через руны в .svelte.ts

Руны не ограничены файлами компонентов. В модуле с расширением .svelte.ts (или .svelte.js) компилятор Svelte обрабатывает руны так же, как в компоненте. Это даёт способ вынести реактивное состояние в отдельный модуль и переиспользовать его в нескольких компонентах. На клиенте это удобный способ держать общее состояние приложения в одном месте.

Здесь createCounter это фабрика: каждый её вызов создаёт независимый экземпляр состояния. Это важное отличие от состояния, объявленного прямо на верхнем уровне модуля. Фабрика возвращает объект с геттером и методом, и реактивность сохраняется, потому что геттер читает руну при каждом обращении. Экспортировать саму переменную `$state` напрямую нельзя: при импорте получился бы её снимок, оторванный от реактивности.

Различие между фабрикой и состоянием на уровне модуля критично. Фабрика даёт новый экземпляр на каждый вызов. Состояние, объявленное прямо в модуле, это один экземпляр на всех импортёров. На клиенте это часто и нужно, но именно второй вариант становится опасным на сервере, как разбирается дальше.

Чем отличается состояние, возвращаемое фабрикой createCounter(), от состояния, объявленного через export const appState = `$state(...)` прямо в модуле?

Почему модульное состояние опасно на сервере

В браузере модуль загружается один раз и обслуживает одного пользователя. На сервере картина иная: один процесс Node живёт долго и обслуживает запросы многих пользователей одновременно. Модуль на сервере тоже загружается один раз, и переменная, объявленная на его верхнем уровне, существует в единственном экземпляре на весь процесс. Все одновременные запросы читают и пишут одну и ту же переменную.

Если в load или в серверном коде записать в currentUser имя пользователя текущего запроса, параллельный запрос другого пользователя перезапишет его. Затем первый запрос при рендеринге прочитает уже чужое значение. Результат это утечка персональных данных между пользователями, плавающая и трудно воспроизводимая, потому что зависит от тайминга одновременных запросов.

Не всё модульное состояние опасно. Неизменяемая конфигурация, справочники, общие для всех данные безопасны, потому что одинаковы для каждого запроса и не зависят от того, кто запросил. Опасно именно изменяемое состояние, привязанное к конкретному пользователю или запросу, на уровне модуля на сервере.

Почему export const currentUser = `$state(...)` на уровне модуля приводит к утечке данных на сервере, но не на клиенте?

Состояние, своё для каждого запроса, через контекст

Лекарство от утечки это привязать персональное состояние не к модулю, а к чему-то, что создаётся заново для каждого запроса. На сервере таким якорем служит дерево компонентов: SvelteKit рендерит свежее дерево на каждый запрос. Контекст, установленный в корне этого дерева, виден всем компонентам запроса и не пересекается с другими запросами. Это и есть рекомендованный способ держать пользовательское состояние.

В корневом layout вызывается setUserState с данными текущего запроса, и состояние оказывается привязанным к дереву этого запроса. Любой вложенный компонент достаёт его через getUserState. Поскольку дерево создаётся заново на каждый запрос, состояние одного пользователя физически отдельно от состояния другого, и утечка невозможна. Фабрика обеспечивает новый экземпляр, а контекст привязывает его к запросу.

ПодходБезопасно на сервереДля чего
Состояние на уровне модуляТолько для неизменяемых общих данныхКонфигурация, справочники, одинаковые для всех
Фабрика плюс контекстДа, изолировано по запросамПользователь, корзина, сессия, любые персональные данные
Фабрика без контекста (на клиенте)Неприменимо к серверуОбщее состояние в рамках одной вкладки браузера

Практическое правило для SSR: если состояние различается от пользователя к пользователю, оно не должно жить в переменной на уровне модуля на сервере. Заверните его в фабрику и положите в контекст в корне дерева. Модульное состояние оставьте для того, что действительно одинаково для всех.

Как правильно держать состояние корзины пользователя в приложении с SSR, чтобы избежать утечки между запросами?

Связь с другими темами

Этот урок про границы безопасного глобального состояния. Он опирается на реактивность рун и контекст:

  • Универсальная реактивность — Руны в .svelte.ts дают реактивное состояние вне компонента, на чём и строится модульное состояние
  • Context API — Контекст это безопасная альтернатива модульному состоянию для персональных данных на сервере
  • Тонкая реактивность — Сигнальная модель объясняет, почему руна остаётся реактивной в обычном модуле

Итог

  • Руны работают в .svelte.ts-модулях, что позволяет вынести общее реактивное состояние за пределы одного компонента
  • На сервере один процесс обслуживает много запросов, поэтому изменяемая переменная модуля делится между всеми пользователями
  • Хранение персональных данных в переменной модуля на сервере приводит к их утечке между одновременными запросами
  • Неизменяемая конфигурация и общие для всех данные в модуле безопасны, потому что одинаковы для каждого запроса
  • Персональное состояние, своё у каждого запроса, держат в контексте, который изолирован по дереву и по запросам

Связанные уроки

  • sv-10-universal-reactivity — Глобальное состояние строится на универсальной реактивности рун, доступной и вне компонентов в .svelte.ts
  • sv-31-context-api — Контекст это рекомендованное лекарство от утечки состояния между запросами, в которое упирается весь урок
  • sv-30-fine-grained-reactivity — Сигнальная модель объясняет, почему руна в .svelte.ts остаётся реактивной вне компонента
Глобальное состояние в модулях и подводные камни SSR

0

1

Войти