在面试中,如果被要求手写一个 Webpack 的热更新(Hot Module Replacement,HMR) ,通常并不是真的让你从零开始完整实现一个像 Webpack 那样复杂的热更新系统(那涉及模块打包、依赖图、开发服务器、WebSocket通信等大量内容),而是考察你对 HMR 工作原理的理解 ,以及你是否能手动模拟一个简化版的热替换过程,比如:
- 如何在不刷新页面的情况下替换某个模块;
- 如何通知浏览器某个模块更新了;
- 如何应用更新而不丢失应用状态。

一、面试时如何介绍 HMR(热更新)
在开始写代码之前,先口述解释 HMR 是什么、为什么需要它、Webpack 中是怎么做的 ,这能体现你的理解深度,然后再进入"手写"部分。
1. 什么是热模块替换(HMR)?
Hot Module Replacement(热模块替换),简称 HMR,是 Webpack 提供的一种在应用运行时动态替换、添加或删除模块而无需完全刷新页面的能力。
2. 为什么需要 HMR?
- 提升开发效率:修改代码后,不需要手动刷新页面,更新局部模块,保持应用状态(比如 Redux store、表单输入等)不丢失。
- 更快的反馈循环:只更新你改动的部分,而不是整页刷新。
- 更好的开发体验:特别是对于 SPA(单页应用),保持路由、状态等一致性非常重要。
3. Webpack 中 HMR 的基本流程(简化版)
- 开启 devServer 并启用 hot: true
- Webpack 在编译时注入 HMR runtime 代码到 bundle
- 当文件修改时,Webpack 重新编译变动的模块
- 通过 WebSocket 将更新消息推送到客户端
- 客户端 HMR runtime 接收到更新,检查是否支持 HMR(模块是否有 accept 处理函数)
- 如果有 accept handler,则执行对应的更新逻辑,替换老模块;否则 fallback 到整页刷新
二、面试手写:简化版 HMR 实现思路
面试官让你"手写 HMR",通常是希望你模拟一个最简化的模块热替换过程,比如:
- 模拟一个模块系统
- 模拟模块被更新
- 模拟不刷新页面而替换某个模块的逻辑
我们可以用以下方式来模拟(以浏览器环境为例):
场景设定
假设我们有如下模块结构:
css
// moduleA.jsmodule.exports = { name: 'Module A', render() { console.log('Render from Module A - ', this.name); }};
主文件:
scss
// main.jsconst moduleA = require('./moduleA');
function render() { moduleA.render();}
render();
// 模拟后续接收到了更新后的 moduleA,并进行热替换
手写目标
我们希望模拟:
- 初始加载 moduleA,调用其 render 方法;
- "模块更新了"(比如 moduleA.js 被修改了,导出的 name 变了);
- 不刷新页面,把旧的 moduleA 替换成新的 moduleA;
- 再次调用 render,看到新的输出,证明模块被"热替换"成功。
🛠️ 手写代码示例(简化模拟 HMR)
你可以和面试官说:
"在实际的 Webpack 中,HMR 是通过 webpack-dev-server + HMR runtime + WebSocket 等实现的,但为了简化,我这里手动模拟一个模块热替换的过程:即动态替换一个已加载的模块对象,并重新执行相关逻辑。"
1. 原始模块定义(旧版本)
ini
// 模拟 moduleA 的初始版本let moduleA = { name: 'Module A (old)', render() { console.log(`[Render] ${this.name}`); }};
2. 主程序
ini
// main.js(模拟入口文件)function renderApp() { moduleA.render();}
// 初次渲染renderApp(); // 输出: [Render] Module A (old)
// ===== 模拟模块更新 =====
// 假设经过一段时间,moduleA 被更新了(比如通过新的请求或者 websocket 通知)// 模拟"新版本的 moduleA"被加载进来const newModuleA = { name: 'Module A (hot updated!)', render() { console.log(`[Render] ${this.name}`); }};
// 模拟 HMR 行为:用新模块替换旧模块console.log('\n🔁 模拟接收到 HMR 更新,替换 moduleA...\n');
// 执行"热替换":直接替换引用moduleA = newModuleA;
// 再次渲染,应该看到新的内容renderApp(); // 输出: [Render] Module A (hot updated!)
🧠 你在面试时可以这样讲解:
"在真实项目中,Webpack 的 HMR 是一个比较复杂的机制,它依赖于:
- webpack-dev-server 提供热更新服务;
- webpack 在编译时注入 HMR runtime;
- 文件改动后 webpack 重新编译变动模块,生成更新后的模块代码;
- 通过 WebSocket 将更新推送至浏览器端;
- 客户端的 HMR runtime 接收更新,判断该模块是否注册了 accept handler,如果有则执行模块替换逻辑,否则 fallback 到整页刷新。
但为了简化理解,我这里手动模拟了一个最基础的"模块热替换"过程:
- 我们有一个模块 moduleA,它被主程序引用并调用;
- 然后我们模拟这个模块被更新了(比如你修改了源码,重新构建后得到了新的 moduleA 对象);
- 我们不刷新页面,而是直接将全局变量 moduleA 指向新的模块对象,从而实现"模块热替换";
- 再次调用模块方法时,输出已经改变,代表模块行为已被更新。
当然,这只是非常简化版的模拟。实际中每个模块可能被多个地方引用,还要处理模块依赖图、模块包装、accept 回调、CSS 热更新、React/Vue 组件热替换等复杂逻辑。Webpack 通过 webpack_require.hmr 等机制管理这些。"
三、进一步:如果你想更贴近真实 HMR,可以提及以下点(加分项)
如果面试官感兴趣,你可以继续深入,提到以下真实 HMR 中的关键点:
| 关键点 | 说明 |
|---|---|
| webpack-dev-server | 提供开发时 server 和热更新能力,基于 Express + WebSocket |
| HMR Runtime | Webpack 注入到 bundle 中的一段 JS,负责接收更新、应用模块替换 |
| WebSocket 通信 | devServer 通过 WebSocket 告诉客户端哪些模块更新了 |
| manifest(json)和 chunk 更新 | 更新后的模块内容通常通过 HTTP 请求获取,比如 jsonp 或 fetch |
| 模块接受(module.accept) | 模块可以声明自己支持热更新,提供回调函数处理更新逻辑 |
| HotModuleReplacementPlugin | Webpack 插件,启用 HMR 功能 |
| React/Vue 的 HMR 支持 | 它们基于 HMR 接口做了封装,比如 React Fast Refresh、Vue Loader 等 |
四、总结:面试回答结构建议
当面试官说:"你来手写一个 Webpack 的热更新",你可以按如下结构回答:
- 先解释 HMR 是什么,为什么需要它
-
- 不刷新页面更新模块,保持状态,提高开发效率
- 简述 Webpack 中 HMR 的工作流程
-
- devServer + HMR runtime + WebSocket + 模块热替换策略
- 然后说:为了简化,我手动模拟一个最基础的模块替换过程
-
- 展示代码:旧模块 → 模拟更新 → 替换模块引用 → 新模块生效
- 最后点出真实 HMR 更复杂,包括 runtime、依赖管理、accept handler、插件等
-
- 表现出你对整体原理有了解,不是只会写玩具代码
附:完整可运行的简化模拟代码(供你复习用)
你可以将以下代码保存为 HTML 或在 Node.js 中以适当方式模拟(这里以浏览器全局变量模拟):
xml
<!DOCTYPE html><html><head> <meta charset="UTF-8" /> <title>简化版 HMR 模拟</title></head><body><script> // 模拟原始 moduleA let moduleA = { name: 'Module A (old)', render() { console.log(`[Render] ${this.name}`); } };
// 主程序 function renderApp() { moduleA.render(); }
renderApp(); // [Render] Module A (old)
// ===== 模拟模块热更新 ===== console.log('\n🔁 模拟:收到新模块,进行热替换...\n');
// 模拟从服务器获取了新的 moduleA const newModuleA = { name: 'Module A (HOT UPDATED!)', render() { console.log(`[Render] ${this.name}`); } };
// 执行"热替换":直接替换模块引用 moduleA = newModuleA;
// 再次调用 renderApp(); // [Render] Module A (HOT UPDATED!)</script></body></html>
运行后你会看到控制台先输出旧模块内容,然后模拟更新后输出新模块内容,模块被"热替换"成功。
总结一句话
"手写 HMR" ≠ 写一个完整的 webpack HMR 系统,而是考察你理解模块热替换的核心思想 + 能否用简单代码模拟"模块更新且不刷新页面"的过程。抓住"模块替换 + 状态保持 + 开发效率"几个关键词,同时展示你对 Webpack 原理的了解,就能在面试中脱颖而出。
如你真遇到要写"完整 HMR 系统",那通常是系统设计类题目,而非手写题 😄。