Hot Module Replacement (HMR) — механизм, позволяющий обновлять отдельные модули приложения во время разработки без полной перезагрузки страницы. Вместо повторной загрузки всего приложения изменённый модуль подменяется на лету, сохраняя текущее состояние интерфейса, данные форм, содержимое переменных и другую информацию, находящуюся в памяти.
В Parcel поддержка HMR встроена по умолчанию и не требует отдельной настройки. При запуске сервера разработки Parcel отслеживает изменения файлов и автоматически инициирует обновление соответствующих модулей.
Для управления процессом обновления используется объект module.hot, предоставляющий набор методов жизненного цикла HMR. Наиболее важными являются:
module.hot.accept() — обработка обновления модуля;module.hot.dispose() — очистка состояния перед заменой
модуля.Эти методы позволяют не просто принимать обновления, а полностью контролировать сохранение и восстановление состояния приложения.
При работе в режиме разработки Parcel внедряет в каждый модуль специальный объект:
module.hot
Наличие этого объекта означает, что модуль поддерживает горячую замену.
Обычно код HMR заключается в условную проверку:
if (module.hot) {
// HMR логика
}
Это предотвращает ошибки в окружениях, где HMR отсутствует, например при production-сборке.
Метод accept() сообщает Parcel, что модуль способен
самостоятельно обработать своё обновление без полной перезагрузки
страницы.
Простейший вариант:
if (module.hot) {
module.hot.accept();
}
После сохранения файла Parcel заменит текущую версию модуля новой.
Рассмотрим модуль:
export function getMessage() {
return "Версия 1";
}
Использование:
import { getMessage } from "./message";
console.log(getMessage());
if (module.hot) {
module.hot.accept();
}
После изменения:
export function getMessage() {
return "Версия 2";
}
Parcel загружает новую версию модуля без полной перезагрузки страницы.
Часто после обновления необходимо выполнить дополнительную логику.
Для этого используется функция обратного вызова:
if (module.hot) {
module.hot.accept(() => {
console.log("Модуль обновлён");
});
}
Каждый раз после замены модуля колбэк будет вызван автоматически.
Предположим, имеется функция рендера:
export function render() {
document.body.innerHTML = `
<h1>Привет, Parcel</h1>
`;
}
Главный модуль:
import { render } from "./render";
render();
if (module.hot) {
module.hot.accept(() => {
render();
});
}
После изменения содержимого функции render() страница
будет обновляться без полной перезагрузки.
Во многих случаях требуется получить новую версию зависимого модуля.
Например:
import { render } from "./render";
render();
if (module.hot) {
module.hot.accept(async () => {
const module = await import("./render");
module.render();
});
}
Здесь после HMR-заменены выполняется динамический импорт актуальной версии файла.
Главная проблема горячей замены заключается в том, что модуль уничтожается и создаётся заново.
Рассмотрим пример:
let counter = 0;
export function increment() {
counter++;
console.log(counter);
}
После обновления файла значение:
counter
снова станет равно нулю.
Для сохранения данных используется механизм передачи состояния между версиями модуля.
Метод dispose() вызывается непосредственно перед
удалением старой версии модуля.
Он позволяет:
Синтаксис:
module.hot.dispose((data) => {
// действия перед удалением
});
Параметр data представляет объект для передачи
информации новой версии модуля.
При изменении файла происходит следующая последовательность:
dispose.accept.Схематично процесс выглядит так:
Старая версия
│
▼
dispose()
│
▼
Удаление модуля
│
▼
Загрузка нового кода
│
▼
accept()
│
▼
Новая версия
Исходный модуль:
let counter = 0;
export function increment() {
counter++;
console.log(counter);
}
Перед удалением:
if (module.hot) {
module.hot.dispose((data) => {
data.counter = counter;
});
}
Теперь значение будет записано в объект состояния.
Новая версия модуля может получить данные через:
module.hot.data
Пример:
let counter = module.hot?.data?.counter || 0;
export function increment() {
counter++;
console.log(counter);
}
Полный код:
let counter = module.hot?.data?.counter || 0;
export function increment() {
counter++;
console.log(counter);
}
if (module.hot) {
module.hot.dispose((data) => {
data.counter = counter;
});
module.hot.accept();
}
Теперь значение счётчика сохраняется между обновлениями.
Одна из наиболее распространённых задач для dispose() —
удаление подписок и слушателей.
Неправильный вариант:
button.addEventListener("click", handler);
После нескольких HMR-обновлений количество обработчиков может увеличиться.
В результате один клик приведёт к множественным вызовам.
const handler = () => {
console.log("Click");
};
button.addEventListener("click", handler);
if (module.hot) {
module.hot.dispose(() => {
button.removeEventListener("click", handler);
});
module.hot.accept();
}
Перед заменой старый обработчик удаляется.
Часто модули запускают интервалы:
const timer = setInterval(() => {
console.log("tick");
}, 1000);
Без очистки после нескольких обновлений появятся десятки активных таймеров.
Использование dispose():
const timer = setInterval(() => {
console.log("tick");
}, 1000);
if (module.hot) {
module.hot.dispose(() => {
clearInterval(timer);
});
module.hot.accept();
}
Создание соединения:
const socket = new WebSocket("ws://localhost:8080");
Перед заменой:
if (module.hot) {
module.hot.dispose(() => {
socket.close();
});
module.hot.accept();
}
Это предотвращает накопление лишних соединений после множества изменений кода.
Иногда необходимо сохранить сложную структуру данных.
Пример:
let store = {
theme: "dark",
language: "ru",
notifications: true
};
Сохранение:
module.hot.dispose((data) => {
data.store = store;
});
Восстановление:
let store = module.hot?.data?.store || {
theme: "dark",
language: "ru",
notifications: true
};
Допускается регистрация нескольких обработчиков:
module.hot.dispose(() => {
clearInterval(timer);
});
module.hot.dispose(() => {
socket.close();
});
module.hot.dispose(() => {
console.log("Очистка завершена");
});
Все функции будут вызваны перед заменой модуля.
Наиболее распространённый шаблон выглядит следующим образом:
let state = module.hot?.data?.state || {};
initialize();
if (module.hot) {
module.hot.dispose((data) => {
data.state = state;
cleanup();
});
module.hot.accept(() => {
initialize();
});
}
Здесь реализованы сразу три задачи:
Допустим, имеется поле ввода:
<input id="name" />
Получение значения:
const input = document.querySelector("#name");
Сохранение перед заменой:
if (module.hot) {
module.hot.dispose((data) => {
data.value = input.value;
});
}
Восстановление:
if (module.hot?.data?.value) {
input.value = module.hot.data.value;
}
После изменения кода введённый текст останется в поле.
let state = module.hot?.data?.state || {
count: 0
};
document.body.innerHTML = `
<button id="btn">${state.count}</button>
`;
const button = document.querySelector("#btn");
button.addEventListener("click", () => {
state.count++;
button.textContent = state.count;
});
if (module.hot) {
module.hot.dispose((data) => {
data.state = state;
});
module.hot.accept();
}
Даже после сохранения файла текущее значение счётчика не потеряется.
Не все изменения могут быть применены через горячую замену.
Полная перезагрузка страницы обычно происходит при:
В таких случаях Parcel автоматически выполняет reload страницы.
К внешним ресурсам относятся:
Любой ресурс, создаваемый модулем, должен быть освобождён перед его удалением.
Объект:
module.hot.data
предназначен для временной передачи состояния между двумя версиями одного модуля.
Не следует помещать туда:
Лучше сохранять только минимальный набор данных, необходимый для восстановления состояния.
Все вызовы должны выполняться через проверку:
if (module.hot) {
module.hot.accept();
}
или
if (module.hot) {
module.hot.dispose(() => {});
}
Такой подход гарантирует корректную работу как в режиме разработки, так и в production-сборке.
let state = module.hot?.data?.state || {
value: 0
};
function init() {
console.log("Инициализация");
}
function destroy() {
console.log("Очистка");
}
init();
if (module.hot) {
module.hot.dispose((data) => {
data.state = state;
destroy();
});
module.hot.accept(() => {
init();
});
}
Данный шаблон отражает базовую архитектуру работы с HMR в Parcel:
сохранение состояния через module.hot.dispose(),
восстановление данных через module.hot.data и принятие
обновлений посредством module.hot.accept(). Именно такая
комбинация обеспечивает быструю разработку без полной перезагрузки
приложения и потери пользовательского состояния.