webpack热更新实现

284 阅读4分钟

热更新(Hot Module Replacement,简称 HMR)是Webpack提供的一项功能,它允许在开发过程中,代码发生变化时,能够自动更新浏览器中的页面,而无需进行完全刷新的技术。

一、实现热更新的关键:webpack-dev-middleware + webpack-hot-middleware

  • webpack-dev-middleware 中间件,主要作用是以监听模式启动 webpack,会监控文件变动,触发 webpack重新编译,将编译后的文件输出到内存,为服务器提供资源服务;

  • webpack-hot-middleware 中间件,用于实现模块热替换(HMR),基于EventSource建立服务器与客户端的通信通道,当 webpack重新编译完成后,通过该通道通知客户端有模块更新,触发客户端的模块替换逻辑。

二、热更新实现流程:

  1. 初始化配置
  • 在 webpack 配置中添加 webpack.HotModuleReplacementPlugin 插件,为每个模块注入 HMR 运行时代码,用于监听代码变化及处理模块替换:

webpack.dev.js:

const webpackConfig = merge.smart(baseConfig, {
  mode: 'development',
  ……
  plugins: [
    new webpack.HotModuleReplacementPlugin({
      multiStep: false
    })
  ]
});
  • 为每个入口文件引入 webpack-hot-middleware/client,建立客户端与服务器的WebSocket/EventSource连接,用于接收更新通知:
// devServer 配置
const DEV_SERVER_CONFIG = {
  HOST: '127.0.0.1',
  PORT: 9002,
  HMR_PATH: '__webpack_hmr',
  TIMEOUT: 20000
};
const {HOST, PORT, HMR_PATH, TIMEOUT} = DEV_SERVER_CONFIG;
// 开发阶段,为每个入口(除了vendor)添加 webpack-hot-middleware/client 的路径
Object.keys(baseConfig.entry).forEach(v => {
  // 第三方包不作为 hmr 入口
  if(v !== 'vendor'){
    baseConfig.entry[v] = [
      // 主入口文件
      baseConfig.entry[v],
      // hmr更新入口,官方指定的hmr路径
      `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}&timeout=${TIMEOUT}&reload=true`
    ];
  }
});
  1. 服务器端集成中间件
  • 使用 Express 为例,将两个中间件接入服务器
// 本地开发启动 devServer
const express = require('express');
const path = require('path');
const consoler = require('consoler');
const webpack = require('webpack');
const devMiddleware = require('webpack-dev-middleware');
const hotMiddleware = require('webpack-hot-middleware');

// 从webpack.dev.js 获取webpack配置 和 devServer 配置
const { webpackConfig, DEV_SERVER_CONFIG } = require('./config/webpack.dev.js');

const app = express()

const compiler = webpack(webpackConfig);

app.use(express.static(path.join(__dirname, '../public/dist/'))); // 指定静态文件目录

// 引入 devMiddleware 中间件(监控文件改动)
app.use(devMiddleware(compiler, {
  writeToDisk: (filePath) => filePath.endsWith('.tpl'), // 落地文件
  publicPath: webpackConfig.output.publicPath,  // 资源路径
  headers: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
    'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
  }
}));

// 引用 hotMiddleware 中间件(实现热更新)
app.use(hotMiddleware(compiler, {
  path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
  log: () => {}
}));

// 启动devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, ()=>{
  console.log(`app listening on port ${port}`);
});
  1. 文件变更触发编译

    当监控的文件(如源码)发生变化时,webpack-dev-middleware 会触发 webpack 重新编译。webpack 会分析变化,仅编译受影响的模块,生成最新打包结果并存储在内存中。

  2. webpack-hot-middleware监听 webpack编译完成事件,通过已建立的 EventSource 连接向客户端发送更新信号,告知有新模块可用。

  3. 客户端的 webpack-hot-middleware/client 接收到更新信号后,通过 HMR runtime 向服务器请求差异模块(仅更新的部分)。获取模块后,HMR runtime 会尝试替换旧模块:

    • 若模块替换成功,页面局部更新,保留应用状态(如表单数据、滚动位置)。
    • 若替换失败(如模块不支持 HMR),则刷新整个页面。

三、关键原理

  1. 内存存储与快速访问

    webpack-dev-middleware 将编译结果存于内存,避免磁盘 I/O 延迟,使浏览器能快速获取最新资源。

  2. EventSource 实时通信

    webpack-hot-middleware 利用 EventSource 实现服务器与客户端的实时通信,及时推送更新通知。

  3. 模块增量更新

    webpack 仅编译变化的模块,webpack-hot-middleware 协助客户端获取并替换这些模块,而非刷新整个页面,提升更新效率。

通过这种协作,两者实现了开发过程中代码修改后快速反馈、页面无刷新更新,显著提升开发体验与效率。

四、webpack-dev-middleware与webpack-dev-server对比

webpack-dev-server是一个完整的开发服务器,提供开箱即用的开发体验。其底层依赖 Expresswebpack-dev-middleware 和 webpack-hot-middleware,本质是对它们的封装,不同的是此模块使用 Websocket 接收服务器发送的事件,不再是 EventSource。

如vue-cli项目中,运行vue-cli-service命令启动开发服务器时,底层以来的webpack-dev-server会默认启动热重载。

webpack-dev-server与webpack-dev-middleware区别:

webpack-dev-serverwebpack-dev-middleware
本质独立的开发服务器,内置 Express + 中间件一个 Express 中间件,需集成到现有服务器中
职责提供完整的开发环境(包括服务器、HMR、路由等)仅处理 Webpack 编译结果的内存化和请求代理
使用方式直接作为命令行工具或 API 调用集成到自定义 Express/Koa 等服务器中
文件系统默认将文件存于内存,可配置写入磁盘强制将文件存于内存(可通过 writeToDisk 例外)
热更新(HMR)内置支持,配置简单需要额外集成 webpack-hot-middleware
路由与代理内置丰富的路由和代理配置需要在宿主服务器中手动配置路由和代理