12k
All articles

Синглтоны в JavaScript: Полезный инструмент или скрытая ловушка?

Синглтоны на основе ES-модулей ломаются в Jest, микрофронтендах и web workers при наличии изменяемого состояния; статья объясняет, как избежать этих проблем.

OpenReplay Team
OpenReplay Team
Синглтоны в JavaScript: Полезный инструмент или скрытая ловушка?

Вы написали модуль, который экспортирует настроенный логгер или экземпляр API-клиента. Каждый файл импортирует его, и вы предполагаете, что во всём приложении работает ровно один экземпляр. Затем ваши тесты начинают «протекать» состоянием между запусками. Или ваша микрофронтенд-архитектура внезапно получает две копии вашего «синглтона», конфликтующие друг с другом. Что произошло?

Паттерн синглтон в JavaScript работает не так, как предполагает классическая теория проектирования. Понимание того, где ES-модульные синглтоны фактически существуют — и где они ломаются — избавит вас от отладочных сессий, похожих на охоту за призраками.

Ключевые выводы

  • ES-модульные синглтоны кешируются для каждого графа модулей и среды выполнения, а не глобально по всей системе
  • Синглтоны хорошо работают для неизменяемых данных и операций без состояния, таких как утилиты логирования и конфигурация только для чтения
  • Изменяемое состояние в синглтонах становится опасным при серверном рендеринге, в тестовых окружениях и микрофронтенд-архитектурах
  • Перед использованием синглтона подумайте, выполняется ли ваш код в нескольких бандлах, воркерах или серверных запросах, и нужно ли тестам сбрасывать экземпляр

Что на самом деле означает «синглтон» в современном JavaScript

Забудьте на мгновение об определении «Банды четырёх». В современном JavaScript синглтон — это обычно просто модуль, который экспортирует уже созданный объект:

// logger.js
class Logger {
  log(message) {
    console.log(`[${Date.now()}] ${message}`)
  }
}

export const logger = new Logger()

Когда вы импортируете logger из нескольких файлов, вы получаете один и тот же экземпляр — не из-за хитрых трюков с конструктором, а потому что ES-модули кешируются для каждого графа модулей и среды выполнения. Модуль выполняется один раз, экземпляр создаётся один раз, и каждый импорт получает ссылку на этот же объект.

Это основа ES-модульных синглтонов. Это просто и часто именно то, что вам нужно.

Предположение, которое ломается

Вот где возникают подводные камни синглтонов в JavaScript: этот «единственный экземпляр» ограничен конкретным графом модулей в конкретной среде выполнения JavaScript. Он не является магически глобальным для всей вашей системы.

Это предположение ломается в нескольких реальных сценариях:

Множественные бандлы или дублирующиеся пакеты. Если в вашем монорепозитории два пакета, каждый из которых бандлит свою копию зависимости, вы получаете два отдельных графа модулей. Два «синглтона». Ваше общее состояние больше не является общим.

Тест-раннеры. Jest, Vitest и подобные инструменты часто сбрасывают кеш модулей между тестовыми файлами или используют рабочие процессы. Ваш синглтон из одного тестового файла может быть не тем же экземпляром в другом.

Микрофронтенды. Каждый независимо развёрнутый фронтенд обычно имеет свою среду выполнения JavaScript. Синглтон в одном микрофронтенде невидим для другого, если экземпляры не используются совместно явно через сборку или среду выполнения.

Web Workers и Service Workers. Они работают в отдельных контекстах JavaScript. Модуль, импортированный в воркере, — это совершенно другой экземпляр по сравнению с тем же модулем в основном потоке.

Серверные среды выполнения с изоляцией запросов. В фреймворках вроде Next.js или Nuxt, работающих на сервере, синглтон может сохраняться между несколькими пользовательскими запросами в зависимости от среды выполнения и модели развёртывания, создавая риск утечки данных, если он содержит изменяемое состояние, специфичное для запроса.

Где синглтоны работают хорошо

Несмотря на эти подводные камни, ES-модульные синглтоны остаются действительно полезными для определённых фронтенд-утилит и паттернов координации:

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

Общая нить: эти синглтоны либо содержат неизменяемые данные, либо выполняют операции без состояния.

Где синглтоны становятся обузой

Изменяемое состояние — вот где всё становится опасным. Рассмотрим:

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

Современные инструменты усиливают эти проблемы. В React 18+ Strict Mode намеренно дважды вызывает рендеры и определённые эффекты в режиме разработки, обнажая состояние синглтона, которое не изолировано должным образом. Hot Module Replacement в Vite или webpack может сохранять состояние синглтона между изменениями кода, создавая тонкие баги, где ваш «свежий» код работает с устаревшими данными.

Практический подход

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

  1. Действительно ли это состояние неизменяемо или не имеет состояния?
  2. Может ли этот код выполняться в нескольких бандлах, воркерах или серверных запросах?
  3. Нужно ли моим тестам сбрасывать или мокировать этот экземпляр?

Если вы отвечаете «да» на вопросы 2 или 3 с изменяемым состоянием, рассмотрите альтернативы: фабричные функции, внедрение зависимостей или специфичные для фреймворка паттерны, такие как React Context или сервисы, привязанные к запросу.

Заключение

Синглтоны — полезный инструмент, когда вы понимаете их фактическую область действия. Они становятся скрытой ловушкой, когда вы предполагаете, что «единственный экземпляр» означает что-то, чего среда выполнения никогда не обещала. Придерживайтесь неизменяемых данных или данных без состояния для ваших экземпляров на уровне модуля и используйте внедрение зависимостей или фабричные паттерны, когда вам нужна правильная изоляция между тестами, запросами или границами среды выполнения.

Часто задаваемые вопросы

Почему у моего синглтона разное состояние в тестах Jest?

Jest часто сбрасывает кеш модулей между тестовыми файлами или запускает тесты в изолированных рабочих процессах, создавая свежие графы модулей и переинстанцируя ваш синглтон. Используйте jest.resetModules() или jest.isolateModules() где это уместно, или избегайте изменяемого состояния на уровне модуля, внедряя зависимости.

Могу ли я использовать синглтон совместно между Web Worker и основным потоком?

Нет. Web Workers работают в отдельных контекстах JavaScript со своими собственными графами модулей. Модуль, импортированный в воркере, — это совершенно другой экземпляр по сравнению с тем же модулем в основном потоке. Чтобы разделить состояние, вы должны явно передавать данные между контекстами, используя postMessage или SharedArrayBuffer.

Безопасно ли использовать синглтоны при серверном рендеринге в Next.js?

Только для неизменяемых данных или данных без состояния. Синглтоны на сервере могут сохраняться между несколькими пользовательскими запросами в зависимости от среды выполнения и модели развёртывания, потенциально допуская утечку чувствительных данных между пользователями. Для состояния, специфичного для запроса, такого как пользовательские сессии или кеши, используйте паттерны, привязанные к запросу, предоставляемые вашим фреймворком, вместо экземпляров на уровне модуля.

Какие альтернативы паттерну синглтон существуют в JavaScript?

Фабричные функции позволяют создавать свежие экземпляры по требованию. Внедрение зависимостей передаёт экземпляры явно, облегчая тестирование. Специфичные для фреймворка решения, такие как React Context, обеспечивают управление состоянием с определённой областью действия. Для серверных приложений сервисы, привязанные к запросу, обеспечивают правильную изоляцию между запросами, сохраняя при этом удобство общих утилит.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.