1. 什么是HMR

  • Hot Module Replacement是指当你对代码修改并保存后,webpack将会对代码进行得新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面。
  • 相对于live reload刷新页面的方案,HMR的优点在于可以保存应用的状态,提高了开发效率

2. 搭建HMR项目

2.1 安装依赖的模块

  1. cnpm i webpack webpack-cli webpack-dev-server mime html-webpack-plugin express socket.io events -S

2.2 package.json

package.json

  1. {
  2. "name": "hot_hmr",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "index.js",
  6. "scripts": {
  7. "build": "webpack",
  8. "dev": "webpack-dev-server"
  9. },
  10. "keywords": [],
  11. "author": "",
  12. "license": "ISC",
  13. "dependencies": {
  14. },
  15. "devDependencies": {
  16. "html-webpack-plugin": "^4.5.2",
  17. "webpack": "4.39.1",
  18. "webpack-cli": "3.3.6",
  19. "webpack-dev-server": "3.7.2"
  20. }
  21. }

2.2 webpack.config.js

webpack.config.js

  1. const path = require('path');
  2. const webpack = require('webpack');
  3. const HtmlWebpackPlugin = require('html-webpack-plugin');
  4. module.exports = {
  5. mode:'development',
  6. entry: './src/index.js',
  7. output: {
  8. filename: 'main.js',
  9. path: path.join(__dirname, 'dist')
  10. },
  11. devServer: {
  12. contentBase:path.join(__dirname, 'dist')
  13. },
  14. plugins:[
  15. new HtmlWebpackPlugin({
  16. template:'./src/index.html',
  17. filename:'index.html'
  18. })
  19. ]
  20. }

2.3 src\index.js

src\index.js

  1. let root = document.getElementById('root');
  2. function render(){
  3. let title = require('./title').default;
  4. root.innerHTML= title;
  5. }
  6. render();

2.4 src\title.js

src\title.js

  1. export default 'hello';

2.5 src\index.html

src\index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>webpack热更新</title>
  8. </head>
  9. <body>
  10. <div id="root"></div>
  11. </body>
  12. </html>

3.流程图

模块热加载HML - 图1
链接 : http://img.zhufengpeixun.cn/webpackhmr.png

4.实现

4.1 webpack.config.js

  1. const path = require("path");
  2. const webpack = require("webpack");
  3. const HtmlWebpackPlugin = require("html-webpack-plugin");
  4. module.exports = {
  5. mode: "development",
  6. entry: "./src/index.js",
  7. output: {
  8. filename: "main.js",
  9. path: path.join(__dirname, "dist"),
  10. },
  11. devServer: {
  12. hot: true, // 开启模块热更新
  13. contentBase: path.join(__dirname, "dist"),
  14. },
  15. plugins: [
  16. new HtmlWebpackPlugin({
  17. template: "./src/index.html",
  18. filename: "index.html",
  19. }),
  20. new webpack.HotModuleReplacementPlugin()
  21. ]
  22. };

4.2 index.js

src\index.js

  1. import '../webpackHotDevClient';
  2. let root = document.getElementById("root");
  3. function render() {
  4. let title = require("./title");
  5. root.innerHTML = title;
  6. }
  7. render();
  8. if(module.hot){
  9. module.hot.accept(['./title'],()=>{
  10. render();
  11. });
  12. }

4.2 src\title.js

src\title.js

  1. module.exports = 'title7';

4.3 src\index.html

src\index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>webpack热更新</title>
  8. </head>
  9. <body>
  10. <div id="root"></div>
  11. <script src="/socket.io/socket.io.js"></script>
  12. </body>
  13. </html>

4.4 webpack-dev-server.js

webpack-dev-server.js

  1. const path = require("path");
  2. const fs = require("fs");
  3. const express = require("express");
  4. const mime = require("mime");
  5. const webpack = require("webpack");
  6. let config = require("./webpack.config");
  7. let compiler = webpack(config);
  8. //1. 创建webpack实例
  9. //2. 启动webpack-dev-server服务器
  10. class Server {
  11. constructor(compiler) {
  12. //4. 添加webpack的`done`事件回调,在编译完成后会向浏览器发送消息
  13. let lastHash;
  14. let sockets = [];
  15. compiler.hooks.done.tap("webpack-dev-server", (stats) => {
  16. lastHash = stats.hash;
  17. sockets.forEach((socket) => {
  18. socket.emit("hash", stats.hash);
  19. socket.emit("ok");
  20. });
  21. });
  22. let app = new express();
  23. compiler.watch({}, (err) => {
  24. console.log("编译成功");
  25. });
  26. //3. 添加webpack-dev-middleware中间件
  27. const webpackDevMiddleware = (req, res, next) => {
  28. if (req.url === "/favicon.ico") {
  29. return res.sendStatus(404);
  30. }
  31. let filename = path.join(config.output.path, req.url.slice(1));
  32. try {
  33. let stats = fs.statSync(filename);
  34. if (stats.isFile()) {
  35. let content = fs.readFileSync(filename);
  36. res.header("Content-Type", mime.getType(filename));
  37. res.send(content);
  38. } else {
  39. next();
  40. }
  41. } catch (error) {
  42. return res.sendStatus(404);
  43. }
  44. };
  45. app.use(webpackDevMiddleware);
  46. this.server = require("http").createServer(app);
  47. //4. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
  48. //将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些`socket`消息进行不同的操作
  49. //当然服务端传递的最主要信息还是新模块的`hash`值,后面的步骤根据这一`hash`值来进行模块热替换
  50. let io = require("socket.io")(this.server);
  51. io.on("connection", (socket) => {
  52. sockets.push(socket);
  53. if (lastHash) {
  54. //5.发送hash值
  55. socket.emit("hash", lastHash);
  56. socket.emit("ok");
  57. }
  58. });
  59. }
  60. //9. 创建http服务器并启动服务
  61. listen(port) {
  62. this.server.listen(port, () => {
  63. console.log(port + "服务启动成功!");
  64. });
  65. }
  66. }
  67. //3. 创建Server服务器
  68. let server = new Server(compiler);
  69. server.listen(8000);

4.5 webpackHotDevClient.js

webpackHotDevClient.js

  1. let socket = io("/");
  2. let currentHash;
  3. let hotCurrentHash; // lastHash 上一次的hash值
  4. class Emitter {
  5. constructor() {
  6. this.listeners = {};
  7. }
  8. on(type, listener) {
  9. this.listeners[type] = listener;
  10. }
  11. emit(type) {
  12. this.listeners[type] && this.listeners[type]();
  13. }
  14. }
  15. let hotEmitter = new Emitter();
  16. const onConnected = () => {
  17. console.log("客户端连接成功");
  18. };
  19. //6. 客户端会监听到此hash消息
  20. socket.on("hash", (hash) => {
  21. currentHash = hash;
  22. });
  23. //7. 客户端收到`ok`的消息 当收到ok事件后 刷新App
  24. socket.on("ok", () => {
  25. debugger;
  26. reloadApp(true);
  27. });
  28. socket.on("connect", onConnected);
  29. hotEmitter.on("webpackHotUpdate", function () {
  30. if (!hotCurrentHash || hotCurrentHash == currentHash) {
  31. return (hotCurrentHash = currentHash);
  32. }
  33. hotCheck();
  34. });
  35. //8.执行hotCheck方法进行更新
  36. function hotCheck() {
  37. // 询问服务器 这次编译相对于上一次编译 改变了哪些
  38. hotDownloadManifest().then((update) => {
  39. let chunkIds = Object.keys(update.c);
  40. chunkIds.forEach((chunkId) => {
  41. //10. 通过JSONP请求获取到最新的模块代码块
  42. hotDownloadUpdateChunk(chunkId);
  43. });
  44. });
  45. }
  46. // 拿到变化的文件
  47. function hotDownloadManifest() {
  48. return new Promise(function (resolve) {
  49. let request = new XMLHttpRequest();
  50. let requestPath = "/" + hotCurrentHash + ".hot-update.json";
  51. request.open("GET", requestPath, true);
  52. request.onreadystatechange = function () {
  53. if (request.readyState === 4) {
  54. let update = JSON.parse(request.responseText);
  55. resolve(update);
  56. }
  57. };
  58. request.send();
  59. });
  60. }
  61. function hotDownloadUpdateChunk(chunkId) {
  62. var script = document.createElement("script");
  63. script.charset = "utf-8";
  64. // /main.xxx.hot-update.js
  65. script.src = "/" + chunkId + "." + hotCurrentHash + ".hot-update.js";
  66. document.head.appendChild(script);
  67. }
  68. // ok 的时候 重新刷新App
  69. function reloadApp(hot) {
  70. if (hot) {
  71. // 如果为true,热更新
  72. hotEmitter.emit("webpackHotUpdate");
  73. } else {
  74. // 不支持热更新 直接加载
  75. window.location.reload();
  76. }
  77. }
  78. window.hotCreateModule = () => {
  79. var hot = {
  80. _acceptedDependencies: {}, //接收的依赖
  81. accept: function (dep, callback) {
  82. for (var i = 0; i < dep.length; i++) {
  83. hot._acceptedDependencies[dep[i]] = callback;
  84. }
  85. },
  86. };
  87. return hot;
  88. };
  89. //11. 补丁JS取回来后会调用`webpackHotUpdate`方法
  90. window.webpackHotUpdate = (chunkId, moreModules) => {
  91. for (let moduleId in moreModules) {
  92. let oldModule = __webpack_require__.c[moduleId]; //获取老模块
  93. let { parents, children } = oldModule; //父亲们 儿子们
  94. var module = (__webpack_require__.c[moduleId] = {
  95. i: moduleId,
  96. l: false,
  97. exports: {},
  98. parents,
  99. children,
  100. hot: window.hotCreateModule(),
  101. });
  102. // 更新为最新
  103. moreModules[moduleId].call(
  104. module.exports,
  105. module,
  106. module.exports,
  107. __webpack_require__
  108. );
  109. module.l = true;
  110. parents.forEach((parent) => {
  111. let parentModule = __webpack_require__.c[parent];
  112. parentModule.hot &&
  113. parentModule.hot._acceptedDependencies[moduleId] &&
  114. parentModule.hot._acceptedDependencies[moduleId]();
  115. });
  116. hotCurrentHash = currentHash;
  117. }
  118. };

5、总结

调试模式
node debugger.js

热更新 配置

  • devServer 开启 hot:true
  • new webpack.HotModuleReplacementPlugin()

热更新原理
webpack:处理配置
http: 相应请求
socket服务器:
客户端请求index.html 生成index.html main.js
服务器请求生成的资源
websocket客户端通过长链接链接到websocket服务器
每当源文件发生改变后,会通知webpack重新打包,websocket服务器会向客户端发消息,重新拉新的代码,更新

服务端devServer操作

首先通过devServer,开启一个本地服务,启用webpack的watch监听,是每次编译拿到最新结果得重点。 初始化webpack,初始化compiler对象, 注册webpack的done事件回调,每次拿到结果后,通过中间件设置展示访问的index页面,注册socket 在每次编译完后,emit最新的hash和ok通知客户端做相应操作

步骤

  1. 启动webpack-dev-server服务器
  2. 创建webpack实例
  3. 创建Server服务器
  4. 添加webpack的done事件回调,编译完成后向客户端发消息
  5. 创建express应用app
  6. 添加webpack-dev-middleware中间件,中间件负责返回生成的文件
  7. 设置文件系统为内存文件系统
  8. 创建http服务器并启动服务
  9. 使用sockjs在浏览器和服务端之间建立一个websock长链接,创建socket服务器

客户端

客户端的操作也需要依赖webpack.HotModuleReplacementPlugin()这个插件,会将每次编译差异部分的代码,写入到不同文件中,客户端要做的就是拿到这些文件,下载通过script引入,执行全局的webpackHotUpdate方法,将需要更新的模块中的方法进行执行

步骤

  1. webpack-dev-server/client-src/default/index.js 监听hash消息,保存hash值
  2. 客户端收到ok消息,会调用reloadApp方法进行更新
  3. reloadApp方法中会判断是否支持热更新,不支持之间刷新页面,支持会发射webpackHotUpdate事件
  4. 当监听到webpackHotUpdate事件后,后进行check方法检查
  5. check方法后调用module.hot.check方法
  6. 通过调用JsonpMainTemplate.runtime.js 的webpackUpdateChunk方法通过jsonP获取最新的代码块
  7. 布丁JS取回来后,会继续调用HotModuleReplace.runtime 的hotAddUpdateChunk方法动态更新模块代码
  8. 调用hotApply方法进行热更新