创见博客
webpack HMR:为什么、是什么、怎么用
七崽爱吃小饼干2026/09/16阅读 0专栏 webpack构建高级

在前端开发里,「改代码 → 看效果」是最高频的动作。如果每次保存都整页刷新,不仅慢,还会把当前页面状态全部清空——填了一半的表单没了,弹窗关了,路由跳回首页。**HMR(Hot Module Replacement,模块热替换)**就是为了让这个过程既快又不丢状态。

一、为什么需要 HMR

1. 整页刷新会丢失状态

传统开发靠「自动刷新」(Live Reload):文件一变,浏览器整页重载。它解决了手动刷新的问题,但代价是:

  • 页面所有 JS 重新执行,应用状态全部重置。
  • 单页应用会跳回初始路由。
  • 需要重新点击、重新输入,才能回到刚才调试的位置。

调试一个三层弹窗里的逻辑时,改一行动样式就要重新点开三次,效率极低。

2. 整页刷新太慢

页面越复杂,重新加载、重新渲染的时间越长。状态丢失 + 重建成本,让「改一下看看」变得很痛苦。

3. 我们其实只改了一个模块

绝大多数时候,一次改动只涉及一个模块(一个组件、一段样式、一个工具函数)。没必要让整个应用重来——只要把变化的那一个模块替换掉,其它保持不动就好。这正是 HMR 的思路。

二、HMR 是什么

HMR 全称 Hot Module Replacement,直译是「热模块替换」。它的定义是:

在应用运行过程中,用新版本的模块替换掉旧模块,而不刷新整个页面。

它和「自动刷新」的区别:

自动刷新(Live Reload)热替换(HMR)
更新范围整个页面仅变更的模块
应用状态丢失保留
速度慢快
体验回到初始状态停留在原地

它背后的机制

webpack 的 HMR 由三部分协作完成:

  1. webpack-dev-server(HMR Server):通过 WebSocket 与浏览器保持长连接,监听文件变化并触发重新编译。
  2. HMR Runtime:被打进产物、运行在浏览器里的一段代码,负责接收更新、替换模块。
  3. 编译产出的更新包:每次改动会生成 hot-update.json(清单)和 hot-update.js(新模块代码)。

一次热更新的完整流程大致是:

文件改动
  → dev-server 重新编译
  → 通过 WebSocket 把新的 hash 推给浏览器
  → HMR Runtime 请求 hot-update.json,得知哪些 chunk 变了
  → 下载对应的 hot-update.js
  → 用新模块替换旧模块,执行 accept 回调
  → 页面局部更新,其它状态保持不变

理解这条链路,后面排查「HMR 为什么不生效」就会轻松很多。

三、怎么用

1. 开启 HMR

在 webpack-dev-server 中打开 hot:

js
module.exports = {
  mode: 'development',
  devServer: {
    hot: true,
  },
};

启动:

bash
npx webpack serve

从 webpack-dev-server v4 起,hot 默认就是 true,但显式写出来更清晰。

2. CSS 的热更新是自动的

只要用了 style-loader(开发环境),CSS 的 HMR 开箱即用。改样式时会直接替换页面上的 <style>,不刷新、不丢状态,这也是为什么开发环境的样式建议用 style-loader 而不是 MiniCssExtractPlugin。

3. JS 模块需要「接受」更新

对普通 JS 模块,webpack 并不知道「替换后该做什么」。如果没有任何模块声明「我能处理这个更新」,更新会一路上抛,最终触发整页刷新。要让某个模块支持热替换,需要用 module.hot.accept 声明:

js
// src/index.js
import { render } from './render.js';

render();

if (module.hot) {
  module.hot.accept('./render.js', () => {
    // render.js 更新后重新执行
    const { render: nextRender } = require('./render.js');
    nextRender();
  });
}

module.hot 提供了几个常用 API:

API作用
module.hot.accept(deps, cb)声明本模块接受依赖更新
module.hot.dispose(cb)模块被替换前做清理(如移除事件监听)
module.hot.decline()声明本模块不可热替换
module.hot.invalidate()主动让自己失效,触发上层处理
module.hot.status()查询当前 HMR 状态

手动写 accept 比较繁琐,实际项目里通常交给框架插件。

4. 框架的 HMR 方案

  • React:使用 react-refresh + @pmmmwh/react-refresh-webpack-plugin,组件更新时保留状态,体验接近完美。
bash
npm install -D react-refresh @pmmmwh/react-refresh-webpack-plugin
js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: ['react-refresh/babel'],
          },
        },
      },
    ],
  },
  plugins: [
    new ReactRefreshWebpackPlugin(),
  ],
};
  • Vue:vue-loader 内置了 HMR 支持,配置好 loader 即可。
  • 通用场景:也可以接入 hot-loader 或框架官方脚手架(如 CRA、Vue CLI)里已有的方案。

5. 只允许 HMR,禁止整页刷新

如果希望「HMR 失败就不要偷偷刷新页面」,可以设为 hot: 'only':

js
devServer: {
  hot: 'only',
}

这样更新无法被接受时会报错提示,而不是悄悄整页刷新——有助于发现哪些模块没有正确处理热替换。

四、HMR 不生效?常见原因

  1. mode 不是 development:生产模式会关掉相关能力。
  2. 没用 webpack serve:直接 webpack build 不会启动 HMR。
  3. CSS 用了 MiniCssExtractPlugin:抽离成 link 后 HMR 支持有限,通常会退化为整页刷新。开发环境请用 style-loader。
  4. JS 模块没有 accept 处理,也没有框架插件:更新无人接收,只能整页刷新。
  5. 产物文件名带 [contenthash]:HMR 依赖稳定的文件名,开发环境不要加 hash。
  6. 被 devServer.hot: false 或 liveReload 配置覆盖:确认没有冲突设置。

五、最小可用示例

js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].js', // 开发环境不加 contenthash
  },
  module: {
    rules: [
      { test: /\.css$/i, use: ['style-loader', 'css-loader'] },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
  ],
  devServer: {
    hot: true,
    open: true,
    port: 8080,
    client: { overlay: true },
  },
};

配合 package.json:

json
{
  "scripts": {
    "dev": "webpack serve --mode development"
  }
}

运行 npm run dev 后,改 CSS 会即时生效且不刷新,改 JS 组件(配好框架插件后)也会保留状态。

小结

围绕三个问题回顾:

  • 为什么:整页刷新会丢失应用状态、重建慢,而一次改动往往只涉及一个模块。
  • 是什么:HMR 在运行时用新模块替换旧模块,不刷新页面,从而保留状态;它由 dev-server、HMR Runtime、更新包三方协作完成。
  • 怎么用:开发环境设 devServer.hot: true;CSS 自动热更新;JS 用 module.hot.accept 或框架插件(React Refresh / vue-loader);必要时用 hot: 'only' 禁止降级刷新。

用好 HMR,能把「改一下看看」的时间从几秒压缩到几乎无感,是提升开发体验最划算的一笔投入。

评论
0/100