import.meta.hot API и ручная реализация HMR

## Механизм import.meta.hot в Vite `import.meta.hot` является ключевой частью системы Hot Module Replacement (HMR) в Vite и представляет собой встроенный API, доступный только в режиме разработки. Этот объект инкапсулирует связь между модулем и dev-сервером Vite, позволяя модулю реагировать на обновления без полной перезагрузки страницы. Важная особенность заключается в том, что `import.meta.hot` существует только при выполнении кода в окружении Vite dev server. В production-сборке данный объект отсутствует, а все связанные с ним блоки кода должны быть корректно устранены сборщиком. ### Условная доступность API Для безопасного использования необходимо учитывать возможность отсутствия HMR: ```js if (import.meta.hot) { // HMR-логика } ``` Такой подход предотвращает ошибки при выполнении в production. --- ## Жизненный цикл HMR-модуля Каждый модуль, поддерживающий HMR, проходит через несколько этапов: 1. Инициализация модуля 2. Регистрация HMR-хендлеров 3. Получение обновления от dev-сервера 4. Принятие или отклонение обновления 5. Применение или полная перезагрузка Vite использует ESM-граф модулей и отслеживает зависимости на уровне импортов. При изменении файла пересобирается только затронутый модуль и его цепочка зависимостей. --- ## Основные методы import.meta.hot ### accept Метод `accept` определяет, что модуль способен самостоятельно обработать обновление без перезагрузки страницы. ```js if (import.meta.hot) { import.meta.hot.accept((newModule) => { // обработка обновлённого модуля }); } ``` Также возможен вариант с явным указанием зависимостей: ```js import.meta.hot.accept(['./dep.js'], (deps) => { // обновление зависимостей }); ``` При этом Vite заменяет только указанные зависимости, не затрагивая остальные части графа. --- ### dispose Метод `dispose` позволяет выполнить очистку перед заменой модуля. ```js import.meta.hot.dispose((data) => { // очистка ресурсов }); ``` Типичные сценарии: * удаление DOM-элементов * остановка интервалов * закрытие WebSocket соединений * освобождение памяти Объект `data` сохраняется между обновлениями и может использоваться для передачи состояния. --- ### data как механизм сохранения состояния Vite предоставляет возможность сохранять состояние между обновлениями: ```js import.meta.hot.dispose((data) => { data.counter = counter; }); ``` ```js import.meta.hot.accept((newModule) => { counter = import.meta.hot.data.counter; }); ``` Этот механизм позволяет реализовать мягкое обновление состояния без потери данных. --- ### decline Метод `decline` отключает HMR для модуля и заставляет Vite выполнять полную перезагрузку страницы при его изменении. ```js import.meta.hot.decline(); ``` Используется в случаях, когда корректное частичное обновление невозможно. --- ### invalidate Метод `invalidate` сообщает Vite, что модуль нужно пересобрать заново. ```js import.meta.hot.invalidate(); ``` Это приводит к повторному прохождению всего HMR-пайплайна для данного модуля. --- ## Принцип работы HMR в Vite Vite реализует HMR поверх нативного ESM и WebSocket-соединения. Основные этапы: 1. Изменение файла фиксируется файловой системой (chokidar) 2. Vite пересобирает только изменённый модуль 3. Формируется HMR update payload 4. Payload отправляется через WebSocket клиенту 5. Клиентский runtime анализирует граф модулей 6. Выполняется поиск boundary модулей (accept boundaries) 7. Производится замена модулей без перезагрузки страницы Ключевая идея заключается в том, что обновление распространяется не «вверх», а по дереву зависимостей до ближайшего обработчика accept. --- ## Boundary-модули и распространение обновлений Boundary-модуль — это модуль, который явно объявил поддержку HMR через `accept`. Если обновляется модуль без boundary, Vite поднимается по графу импортов до ближайшего родителя, который способен принять обновление. Если такой модуль не найден, происходит full reload страницы. --- ## Ручная реализация HMR-поведения поверх import.meta.hot Несмотря на встроенную поддержку Vite, возможно построение собственной логики управления обновлениями. ### Сохранение состояния вручную ```js let state = { value: 0 }; if (import.meta.hot) { import.meta.hot.dispose((data) => { data.state = state; }); import.meta.hot.accept((newModule) => { state = import.meta.hot.data.state; newModule.render(state); }); } ``` Здесь реализуется классическая схема: * сохранение состояния при dispose * восстановление при accept --- ### Ручное управление DOM При работе с UI без фреймворков требуется явное управление DOM: ```js let root = document.getElementById('app'); function render(state) { root.innerHTML = ''; const el = document.createElement('div'); el.textContent = state.value; root.appendChild(el); } if (import.meta.hot) { import.meta.hot.dispose(() => { root.innerHTML = ''; }); import.meta.hot.accept((newModule) => { newModule.render(state); }); } ``` Такой подход предотвращает дублирование элементов при повторной загрузке модуля. --- ### Координация нескольких модулей При сложных приложениях состояние распределяется между модулями: ```js // store.js export let store = { count: 0 }; if (import.meta.hot) { import.meta.hot.accept((newModule) => { store = newModule.store; }); } ``` ```js // counter.js import { store } from './store.js'; if (import.meta.hot) { import.meta.hot.accept(); } ``` Vite при этом сохраняет связь между модулями, но обновление store должно быть явно обработано. --- ## Ограничения HMR в Vite ### Потеря состояния при full reload Если цепочка accept не найдена, происходит полная перезагрузка, и состояние теряется. ### Невозможность частичного обновления некоторых модулей Модули с побочными эффектами верхнего уровня часто требуют decline. ### Неочевидность границ обновления Глубокие цепочки импортов могут приводить к неожиданным точкам инвалидации. --- ## Низкоуровневый механизм WebSocket HMR Vite клиент подключается к dev-серверу через WebSocket: * сервер отправляет событие `update` * клиент получает список изменённых модулей * выполняется динамический import нового ESM-кода * происходит замена ссылок в module graph Пример условной обработки: ```js socket.onmess age = async (event) => { const data = JSON.parse(event.data); if (data.type === 'update') { for (const mod of data.updates) { await import(mod.url + '?t=' + Date.now()); } } }; ``` Реальная реализация Vite значительно сложнее и включает управление графом зависимостей и кэшированием модулей. --- ## Инвалидация цепочек зависимостей При обновлении одного модуля Vite вычисляет затронутые зависимости: * direct imports * dynamic imports * side-effect imports Если хотя бы один модуль в цепочке не поддерживает HMR, происходит расширение области обновления до ближайшего безопасного boundary. --- ## Паттерны использования import.meta.hot ### Локальный stateful-модуль Используется для компонентов UI: * хранение состояния внутри модуля * автоматическое восстановление через data * минимальная логика обновления ### Глобальные сторы Используют централизованное состояние с контролируемым accept: * единая точка обновления * синхронизация между модулями * явная пересборка store ### Side-effect модули Для модулей с побочными эффектами: ```js if (import.meta.hot) { import.meta.hot.dispose(() => { cleanup(); }); import.meta.hot.decline(); } ``` --- ## Стратегии стабильной HMR архитектуры Эффективная структура модулей строится вокруг следующих принципов: * минимизация side effects на верхнем уровне * явное управление состоянием через dispose * локализация accept boundary * избегание скрытых глобальных изменений * предсказуемая структура импортов Такая архитектура позволяет добиться стабильного поведения HMR даже в сложных приложениях с множеством зависимостей.