前端工程化 · 82/90
1. Babel 的工作原理 2. body-parser 中间件的作用 3. Babel 转译原理 4. 浏览器和 Node 中的事件循环区别 5. 职责链模式 6. 链模式 7. 命令模式 8. 组件封装设计 9. 数据统计 10. dependencies 和 devDependencies 的区别 11. CommonJS 和 ES6 模块引入的区别 12. 设计模式分类 13. 前端开发中常用的设计模式 14. 设计模式应用场景 15. 设计原则 16. 开发环境搭建要点 17. Electron 理解 18. 前后端分离是什么 19. 工厂模式 20. 前端代码重构 21. 前端组件化 22. 前端工程师职业发展 23. 前端工程化方向 24. 前端工程化的理解 25. 前端工程价值体现 26. 前端工程化 27. Git 常用命令与工作流 28. Gulp 任务自动化工具 29. 图片导出 30. 前端模块化规范 31. 迭代器模式 32. JavaScript 编码规范 33. 前端 CI/CD 流程 34. jQuery 生态对比 35. jQuery 实现原理 36. jQuery 与 Sizzle 选择器集成 37. Koa 中间件异常处理 38. jQuery 源码优秀实践 39. jQuery 与 Zepto 对比 40. jQuery UI 自定义组件 41. Koa 中间件不调用 await next() 的影响 42. Koa 在没有 async/await 时如何实现洋葱模型 43. Koa 和 Express 的区别 44. Koa 洋葱模型 45. 登录实现 46. 中介者模式 47. 模块模式 48. 小程序架构 49. 小程序常见问题 50. Monorepo 概念与工具 51. mpvue 框架 52. MVC vs MVP vs MVVM 53. Node.js ES Module 为什么必须加文件扩展名 54. MVC、MVP 和 MVVM 架构模式 55. Node.js 全局对象 56. Node.js 性能监控与优化 57. Node.js 多进程与进程通讯 58. Node.js 调试方法 59. Node.js 中的 process 对象 60. Node.js 的理解与应用场景 61. npm 是什么? 62. 观察者模式和发布订阅模式的区别 63. 页面重构方法 64. PM2 守护进程原理 65. 分页功能的前后端设计 66. PostCSS 作用 67. 项目管理方法 68. Rollup 打包工具 69. 高质量前端代码 70. JavaScript 单例模式实现 71. SSG 静态网站生成 72. 模板方法模式 73. 设计模式的六大原则 74. Tree Shaking 原理 75. 用户授权信息获取流程 76. Vite 原理与性能优势 77. Web App vs Hybrid App vs Native App 78. Web 前端开发注意事项 79. Web APP 设计原则 80. Webpack 构建流程 81. Hash vs ChunkHash vs ContentHash 82. Webpack 热更新原理 83. Webpack Loader 与 Plugin 区别 84. webpack 的 module、bundle、chunk 是什么 85. Webpack Proxy 工作原理与跨域解决 86. webpack、rollup、parcel 的选择 87. WePy 与 mpvue 对比 88. WXML 和 WXSS 89. Webpack Scope Hoisting 90. Zepto 实现原理

Webpack 热更新原理

HMR 的 WebSocket 通信与模块替换机制

问题

Webpack 热更新 (HMR) 是如何实现的?WebSocket 和 manifest 在其中扮演什么角色?

解答

HMR 整体流程

文件修改 → Webpack 编译 → 生成更新文件 → WebSocket 通知 → 浏览器拉取更新 → 替换模块

1. 建立 WebSocket 连接

webpack-dev-server 启动时,会在浏览器和服务器之间建立 WebSocket 连接:

// webpack-dev-server 简化实现
const WebSocket = require('ws');

class HMRServer {
  constructor(compiler) {
    this.sockets = [];
    
    // 监听 webpack 编译完成
    compiler.hooks.done.tap('HMRServer', (stats) => {
      // 向所有客户端发送更新通知
      this.sockets.forEach(socket => {
        socket.send(JSON.stringify({
          type: 'hash',
          data: stats.hash  // 本次编译的 hash 值
        }));
        socket.send(JSON.stringify({
          type: 'ok'  // 通知客户端可以更新
        }));
      });
    });
  }

  // 创建 WebSocket 服务
  createServer(server) {
    const wss = new WebSocket.Server({ server });
    wss.on('connection', (socket) => {
      this.sockets.push(socket);
    });
  }
}

2. 客户端接收更新通知

浏览器端的 HMR runtime 监听 WebSocket 消息:

// HMR 客户端代码(简化版)
const socket = new WebSocket('ws://localhost:8080');
let currentHash = __webpack_hash__;  // 当前模块的 hash

socket.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.type === 'hash') {
    // 保存最新的 hash
    currentHash = message.data;
  }
  
  if (message.type === 'ok') {
    // 检查是否需要更新
    if (currentHash !== __webpack_hash__) {
      hotCheck();
    }
  }
};

// 检查并应用更新
async function hotCheck() {
  try {
    // 拉取 manifest 文件
    const update = await module.hot.check(true);
    if (update) {
      console.log('HMR 更新成功:', update);
    }
  } catch (err) {
    // 更新失败,刷新页面
    window.location.reload();
  }
}

3. Manifest 和更新文件

Webpack 编译后生成两个关键文件:

// [hash].hot-update.json (manifest 文件)
// 描述哪些 chunk 需要更新
{
  "c": { "main": true },  // 需要更新的 chunk
  "r": [],                // 需要移除的 chunk
  "m": []                 // 需要移除的模块
}

// [chunkId].[hash].hot-update.js (更新的模块代码)
self["webpackHotUpdate"]("main", {
  "./src/app.js": (module, exports, __webpack_require__) => {
    // 新的模块代码
    module.exports = function() {
      console.log('Updated!');
    };
  }
});

4. 模块替换过程

// HMR runtime 核心逻辑(简化版)
const installedModules = {};  // 已安装的模块缓存

function hotApply(options) {
  // 1. 找出过期模块
  const outdatedModules = getOutdatedModules();
  
  // 2. 从缓存中移除过期模块
  outdatedModules.forEach(moduleId => {
    delete installedModules[moduleId];
  });
  
  // 3. 执行新模块代码
  for (const moduleId in updatedModules) {
    // 将新模块添加到 modules 对象
    modules[moduleId] = updatedModules[moduleId];
  }
  
  // 4. 重新执行父模块,触发依赖更新
  outdatedModules.forEach(moduleId => {
    const module = installedModules[moduleId];
    if (module && module.hot && module.hot._acceptedDependencies[moduleId]) {
      // 执行 accept 回调
      module.hot._acceptedDependencies[moduleId]();
    }
  });
}

5. 业务代码中使用 HMR

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

render();

// 声明该模块支持热更新
if (module.hot) {
  module.hot.accept('./render', () => {
    // 当 render.js 更新时,重新执行
    render();
  });
}

完整流程图

┌─────────────┐    文件变化    ┌─────────────┐
│   文件系统   │ ───────────→ │   Webpack   │
└─────────────┘               └──────┬──────┘
                                     │ 编译

                              ┌─────────────┐
                              │ 生成更新文件 │
                              │ - manifest  │
                              │ - update.js │
                              └──────┬──────┘

        WebSocket 推送 hash          │
┌─────────────┐ ←────────────────────┘
│   浏览器    │
│ HMR Runtime │
└──────┬──────┘
       │ JSONP 请求 manifest

┌─────────────┐
│ 拉取更新文件 │
└──────┬──────┘
       │ 替换模块

┌─────────────┐
│ 执行 accept │
│   回调函数   │
└─────────────┘

关键点

  • WebSocket 通信:用于服务器向浏览器推送更新通知,只传递 hash 值,不传输代码
  • Manifest 文件:JSON 格式,描述哪些 chunk/模块需要更新
  • JSONP 拉取:浏览器通过 JSONP 方式请求更新的模块代码
  • 模块缓存替换:删除旧模块缓存,执行新模块代码,触发 accept 回调
  • 降级处理:如果模块没有声明 accept,或更新失败,会冒泡到入口模块,最终刷新页面