Hot Module Replacement (HMR) — механизм динамической замены модулей без полной перезагрузки страницы. Webpack обновляет изменённый модуль во время работы приложения, сохраняя текущее состояние интерфейса и JavaScript-контекста.
При обычной перезагрузке браузер:
HMR работает иначе:
Основные преимущества:
Для работы HMR требуется:
webpackwebpack-cliwebpack-dev-serverУстановка:
npm install webpack webpack-cli webpack-dev-server --save-dev
Минимальная конфигурация:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
devServer: {
static: path.resolve(__dirname, 'dist'),
hot: true,
open: true
}
};
Ключевой параметр:
hot: true
Он включает поддержку Hot Module Replacement в
webpack-dev-server.
HMR можно активировать напрямую из командной строки:
npx webpack serve --hot
Либо через package.json:
{
"scripts": {
"dev": "webpack serve --hot"
}
}
Во время запуска dev-server создаются:
После изменения файла:
Webpack предоставляет API:
module.hot
Проверка поддержки:
if (module.hot) {
console.log('HMR enabled');
}
Обработка обновления модуля:
if (module.hot) {
module.hot.accept('./module.js', () => {
console.log('Module updated');
});
}
src/
index.js
message.js
message.js
export function message() {
console.log('Hello');
}
index.js
import { message } from './message';
message();
if (module.hot) {
module.hot.accept('./message.js', () => {
console.log('message.js updated');
const updated = require('./message');
updated.message();
});
}
Теперь изменение message.js не вызывает полную
перезагрузку страницы.
Webpack способен заменить модуль только если:
accept;Если один из модулей не поддерживает HMR:
module A -> module B -> module C
и module C не может быть безопасно заменён, Webpack
инициирует:
Full Reload
module.hot.accept('./logger.js', () => {
console.log('logger updated');
});
module.hot.accept([
'./a.js',
'./b.js'
], () => {
console.log('modules updated');
});
if (module.hot) {
module.hot.accept();
}
Метод используется перед удалением старого модуля.
Пример:
let timer = setInterval(() => {
console.log('tick');
}, 1000);
if (module.hot) {
module.hot.dispose(() => {
clearInterval(timer);
});
}
Без очистки возможно:
Главная проблема HMR — потеря локального состояния.
Пример:
let counter = 0;
export function increment() {
counter++;
console.log(counter);
}
После обновления:
counter = 0
Webpack предоставляет механизм хранения данных между обновлениями.
Сохранение состояния:
let state = module.hot?.data?.state || {
counter: 0
};
export function increment() {
state.counter++;
console.log(state.counter);
}
if (module.hot) {
module.hot.dispose((data) => {
data.state = state;
});
}
При использовании:
style-loadercss-loaderHMR для CSS работает автоматически.
Установка:
npm install style-loader css-loader --save-dev
Конфигурация:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
};
style-loader:
<style>;Без HMR:
reload page
С HMR:
replace style tag
Конфигурация:
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
HMR корректно обновляет:
В production часто используется:
MiniCssExtractPlugin
Но в development лучше использовать:
style-loader
Причина:
MiniCssExtractPlugin извлекает CSS в файлы;Типичная схема:
const isDev = process.env.NODE_ENV === 'development';
{
test: /\.css$/,
use: [
isDev
? 'style-loader'
: MiniCssExtractPlugin.loader,
'css-loader'
]
}
Установка:
npm install sass sass-loader --save-dev
Конфигурация:
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}
HMR поддерживается автоматически.
Конфигурация:
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}
Изменения:
обновляются без reload.
Старый механизм:
react-hot-loader
современный подход:
React Refresh
Пакеты:
npm install react-refresh @pmmmwh/react-refresh-webpack-plugin --save-dev
npm install babel-loader @babel/core @babel/preset-react --save-dev
babel.config.js
module.exports = {
presets: [
'@babel/preset-react'
],
plugins: [
require.resolve('react-refresh/babel')
]
};
const ReactRefreshWebpackPlugin =
require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
devServer: {
hot: true
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [
new ReactRefreshWebpackPlugin()
]
};
React Refresh умеет:
Без React Refresh:
reload -> reset useState
С React Refresh:
update component -> preserve state
Пример:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onCl ick={() => setCount(count + 1)}>
{count}
</button>
);
}
Изменение JSX не сбросит состояние.
Состояние может потеряться если:
Пример проблемного изменения:
if (condition) {
useEffect(() => {});
}
Внутри runtime происходит:
Если компонент несовместим:
forced remount
Vue поддерживает HMR через:
vue-loaderwebpack-dev-serverУстановка:
npm install vue-loader vue-template-compiler --save-dev
Для Vue 3:
npm install vue-loader @vue/compiler-sfc --save-dev
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
],
devServer: {
hot: true
}
};
Vue умеет обновлять:
Пример:
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
}
};
</script>
<style scoped>
div {
color: red;
}
</style>
Изменение:
<div>{{ count }}</div>
обновляет:
Состояние компонента сохраняется.
Vue заменяет стили без перезагрузки страницы.
Поддерживаются:
При изменении <script> Vue:
В некоторых случаях локальное состояние теряется.
Для корректной отладки обычно используется:
devtool: 'eval-source-map'
или:
devtool: 'cheap-module-source-map'
HMR без source maps работает, но отладка становится неудобной.
devServer: {
hot: true
}
Включает HMR.
devServer: {
hot: false
}
Только full reload.
devServer: {
hot: 'only'
}
Если HMR невозможен:
не делать reload
Полезно для диагностики проблем HMR.
Это разные механизмы.
change -> reload page
change -> replace module
devServer: {
hot: true,
liveReload: true
}
Если HMR невозможен:
fallback to reload
Причины:
mode: production;hot: true;module.hot.accept.Причины:
Проблема:
button.addEventListener(...)
после HMR выполняется повторно.
Решение:
module.hot.dispose(() => {
cleanup();
});
Причины:
Пример очистки:
module.hot.dispose(() => {
socket.close();
clearInterval(timer);
});
Webpack 5:
cache: {
type: 'filesystem'
}
Ускоряет повторные пересборки.
{
test: /\.js$/,
exclude: /node_modules/
}
Webpack 5 поддерживает:
experiments: {
lazyCompilation: true
}
Модули собираются по требованию.
Для HMR часто используют:
devtool: 'eval-cheap-module-source-map'
Преимущества:
HMR наиболее эффективен при компонентной архитектуре.
Плохой пример:
huge global module
Хороший пример:
small isolated modules
Побочные эффекты усложняют HMR.
Проблемный код:
window.globalData = {};
startServer();
connectSocket();
Лучше:
export function init() {}
export function destroy() {}
HMR проще работает с:
Конфигурация:
npm install ts-loader typescript --save-dev
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/
}
HMR работает аналогично JavaScript.
{
test: /\.(ts|tsx)$/,
use: 'babel-loader'
}
Совместно с:
react-refresh
получается полноценный Fast Refresh.
const path = require('path');
const ReactRefreshWebpackPlugin =
require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
devtool: 'eval-cheap-module-source-map',
cache: {
type: 'filesystem'
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
resolve: {
extensions: ['.js', '.jsx']
},
plugins: [
new ReactRefreshWebpackPlugin()
],
devServer: {
static: path.resolve(__dirname, 'dist'),
hot: true,
open: true,
historyApiFallback: true
}
};