webpack HMR使用与简单实现

475 阅读13分钟

webpack HMR

1. 什么是HMR?

模块热替换(HMR-hot module replacement) 是指当我们对代码修改并保存后,webpack将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换老的模块,以实现在不刷新浏览器的前提下更新页面。

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

2. 使用HMR

首先创建一个项目

mkdir webpack-hmr && cd webpack-hmr && yarn init -y

安装必要的依赖

yarn add webpack webpack-cli webpack-dev-server html-webpack-plugin -D

创建以下文件

image-20220619114324929.png

src/index.js

function render() {
  const title = require('./src/title.js');
  document.getElementById('root').innerHTML = title;
}
render();

src/title.js

module.exports =  'title';

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HMR</title>
  </head>
  <body>
    <input />
    <div id="root"></div>
  </body>
</html>

webpack.config.js

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
​
module.exports = {
  mode: 'development',
  devtool: false,
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
  },
  devServer: {
    hot: true, // 表示开启HMR
    contentBase: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html',
    }),
    // 当hot为true时,webpack会自动给我们添加这个插件,如果不为true,需要我们手动添加
    // new webpack.HotModuleReplacementPlugin()
  ],
};

在package.json中添加scripts

"scripts": {
  "build": "webpack",
  "dev": "webpack serve"
}

执行 yarn dev,可以看到在8080端口已经运行起来我们的项目了

image-20220619115402743.png

动修改src/title.js中的代码并保存

module.exports = 'title1'

可以看到页面title确实变成title1了。但是如果在input中输入一些字符串,然后再次修改src/title.js,会发现input会重置,这相当于刷新了整个页面,这不是HMR呀,我想要的是只变更新的模块呀😢

其实还需要在src/index.js中新增一些代码

function render() {
  const title = require('./src/title.js');
  document.getElementById('root').innerHTML = title;
}
render();
​
// 表示如果监听到title.js变化时重新执行render函数
// module.hot 是开启了HMR后webpack-dev-server在运行时中注入的
if (module.hot) {
  module.hot.accept(['./title.js'], render);
}

这样当变更title.js时,就不会重置input了,只更新title。

当第一次webpack打包完成之后,会生成一个hash发送到客户端。 当模块发生变化的时候。webpack再次生成一个hash值,发送到客户端。客户端在发生热模块替换的时候,会拿到之前旧的hash值去请求manifest文件(HotModuleReplacementPlugin生成的),找到要发生变化的chunk,然后通过再去请求最新的模块代码(HotModuleReplacementPlugin生成的),从而完成更新

image-20220619191319381.png

image-20220619191422402.png

3. 基础知识

3.1 module和chunk

  • 在 webpack里有各种各样的模块
  • 一般一个入口会依赖多个模块
  • 一个入口一般会对应一个chunk,这个chunk里包含这个入口依赖的所有的模块

3.2 HotModuleReplacementPlugin

  • webpack/lib/HotModuleReplacementPlugin.js

  • 它会生成两个补丁文件

    • 上一次编译生成的hash.hot-update.json,说明从上次编译到现在哪些代码块发生成改变
    • chunk名字.上一次编译生成的hash.hot-update.js,存放着此代码块最新的模块定义,里面会调用webpackHotUpdate方法
  • 向代码块中注入HMR runtime代码,热更新的主要逻辑,比如拉取代码、执行代码、执行accept回调都是它注入的到chunk中的

  • hotCreateRequire会帮我们给模块 module的parentschildren赋值

3.3 webpack的监控模式

  • 如果使用监控模式编译webpack的话,如果文件系统中有文件发生了改变,webpack会监听到并重新打包
  • 每次编译会产生一个新的hash值

4. HMR工作流程

4.1. 服务器部分

  1. 启动webpack-dev-server服务器
  2. 创建webpack实例
  3. 创建Server服务器
  4. 添加webpack的done事件回调,在编译完成后会向浏览器发送消息
  5. 创建express应用app
  6. 使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中
  7. 设置文件系统为内存文件系统
  8. 添加webpack-dev-middleware中间件
  9. 创建http服务器并启动服务
  10. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些socket消息进行不同的操作。当然服务端传递的最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换
步骤代码位置
1.启动webpack-dev-server服务器webpack-dev-server.js#L159
2.创建webpack实例webpack-dev-server.js#L89
3.创建Server服务器webpack-dev-server.js#L100
4.更改config的entry属性webpack-dev-server.js#L157
entry添加dev-server/client/index.jsaddEntries.js#L22
entry添加webpack/hot/dev-server.jsaddEntries.js#L30
5. setupHooksServer.js#L122
6. 添加webpack的done事件回调Server.js#L183
编译完成向websocket客户端推送消息,最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换Server.js#L178
7.创建express应用appServer.js#L169
8. 添加webpack-dev-middleware中间件Server.js#L208
以watch模式启动webpack编译,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包index.js#L41
设置文件系统为内存文件系统index.js#L65
返回一个中间件,负责返回生成的文件middleware.js#L20
app中使用webpack-dev-middlerware返回的中间件Server.js#L128
9. 创建http服务器并启动服务Server.js#L135
10. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接Server.js#L745
创建socket服务器并监听connection事件SockJSServer.js#L33

4.2. 客户端部分

  1. webpack-dev-server/client-src/default/index.js端会监听到此hash消息,会保存此hash值
  2. 客户端收到ok的消息后会执行reloadApp方法进行更新
  3. 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器
  4. webpack/hot/dev-server.js会监听webpackHotUpdate事件,然后执行check()方法进行检查
  5. 在check方法里会调用module.hot.check方法
  6. 它通过调用 JsonpMainTemplate.runtimehotDownloadManifest方法,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件,该 Manifest 包含了所有要更新的模块chunk名
  7. 调用JsonpMainTemplate.runtimehotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码
  8. 补丁取回来后会调用JsonpMainTemplate.runtime.jswebpackHotUpdate方法,里面会调用hotAddUpdateChunk方法,用新的模块替换掉旧的模块
  9. 然后会调用HotModuleReplacement.runtime.jshotAddUpdateChunk方法动态更新模块代 码
  10. 然后调用hotApply方法进行热更新
步骤代码
1.连接websocket服务器socket.js#L25
2.websocket客户端监听事件socket.js#L53
监听hash事件,保存此hash值index.js#L55
3.监听ok事件,执行reloadApp方法进行更新index.js#L93
4. 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器reloadApp.js#L7
5. 在webpack/hot/dev-server.js会监听webpackHotUpdate事件dev-server.js#L55
6. 在check方法里会调用module.hot.check方法dev-server.js#L13
7. 调用hotDownloadManifest,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lastHash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名HotModuleReplacement.runtime.js#L180
8. 调用JsonpMainTemplate.runtimehotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码JsonpMainTemplate.runtime.js#L14
9. 补丁JS取回来后会调用JsonpMainTemplate.runtime.jswebpackHotUpdate方法JsonpMainTemplate.runtime.js#L8
10. 然后会调用HotModuleReplacement.runtime.jshotAddUpdateChunk方法动态更新模块代码HotModuleReplacement.runtime.js#L222
11.然后调用hotApply方法进行热更新HotModuleReplacement.runtime.js#L257 HotModuleReplacement.runtime.js#L278
12.从缓存中删除旧模块HotModuleReplacement.runtime.js#L510
13.执行accept的回调HotModuleReplacement.runtime.js#L569

一张图概括

image-20220619121817642.png

5. 实现简易的HMR服务器(服务器部分)

5.1 启动开发服务器

新建一个webpack-dev-server文件夹,并创建一个index.js文件

webpack-dev-server/index.js

const webpack = require('webpack');
const Server = require('./lib/Server');
const config = require('../webpack.config');
​
function startServer(compiler, options) {
  const devOptions = options.devServer || {};
  const { host = 'localhost', port = 8080 } = devOptions;
  const server = new Server(compiler, devOptions);
  server.listen(port, host, err => {
    if (err) {
      console.log(err);
      process.exit(1);
    }
    console.log(`Project is running at: http://${host}:${port}/`);
  });
}
​
const compiler = webpack(config);
startServer(compiler, config);

webpack-dev-server/lib/Server.js

const path = require('path');
const http = require('http');
const express = require('express');
​
class Server {
  constructor(compiler, options) {
    this.compiler = compiler;
    this.options = options;
    this.setupApp();
    this.createServer();
  }
  // 生成express实例
  setupApp() {
    this.app = express();
  }
  // 创建http服务
  createServer() {
    this.server = http.createServer(this.app);
  }
  // 开启服务
  listen(port, host = 'localhost', cb = () => {}) {
    this.server.listen(port, host, cb);
  }
}
​
module.exports = Server;

package.json

   "scripts": {
     "build": "webpack",
     "dev": "webpack serve",
+    "start": "node ./webpack-dev-server"
   },

现在运行yarn start就可以启动一个运行在8080端口的服务器,但是现在什么都不会返回,下面一步步来完善它

5.2 给entry添加客户端

webpack-dev-server/lib/Server.js

const path = require('path');
const http = require('http');
const express = require('express');
+ const updateCompiler = require('./utils/updateCompiler');
​
class Server {
  constructor(compiler, options) {
    this.compiler = compiler;
    this.options = options;
+    updateCompiler(compiler);
    this.setupApp();
    this.createServer();
  }
  // 生成express实例
  setupApp() {
    this.app = express();
  }
  // 创建http服务
  createServer() {
    this.server = http.createServer(this.app);
  }
  // 开启服务
  listen(port, host = 'localhost', cb = () => {}) {
    this.server.listen(port, host, cb);
  }
}
module.exports = Server;

webpack-dev-server/lib/utils/updateCompiler.js

const path = require('path');
​
// 为了实现客户端与服务端通信,需要向入口文件中多注入两个文件
function updateCompiler(compiler) {
  const config = compiler.options;
  // webpack-dev-server/client/index.js 在浏览器启动websocket客户端
  config.entry.main.import.unshift(require.resolve('../../client/index.js'));
  // webpack/hot/dev-server.js 在浏览器监听websocket发射出来的webpackHotUpdate事件
  config.entry.main.import.unshift(require.resolve('../../../webpack/hot/dev-server.js'));
  compiler.hooks.entryOption.call(config.context, config.entry);
}
module.exports = updateCompiler;

webpack-dev-server/client/index.js

console.log('this is webpack-dev-server/client/index.js');

webpack/hot/dev-server.js

console.log('this is webpack-dev-server/webpack/hot/dev-server.js');

5.3 添加webpack编译成功后done事件的回调

webpack-dev-server/lib/Server.js

const path = require('path');
const http = require('http');
const express = require('express');
const updateCompiler = require('./utils/updateCompiler');
​
class Server {
  constructor(compiler, options) {
    this.compiler = compiler;
    this.options = options;
    updateCompiler(compiler);
+    this.clientSocketList = []; //存放着所有的通过websocket连接到服务器的客户端
+    this.setupHooks();
    this.setupApp();
    this.createServer();
  }
+  setupHooks() {
+    // 监听编译完成事件,当webpack编译完成之后会调用此钩子函数  tapable
+    this.compiler.hooks.done.tap('webpack-dev-server', stats => {
+      // stats是一个描述对象,里面放着打包后的结果hash、chunkHash、contentHash、产生了哪些代码块、产出哪些模块等信息
+      console.log('complicate done. current hash is ', stats.hash);
+      // 向所有的客户端进行广播,通知各个客户端我已经编译成功了,新的模块代码已经生成,快来拉我的新代码
+      this.clientSocketList.forEach(socket => {
+        socket.emit('hash', stats.hash);
+        socket.emit('ok');
+      });
+      // 记录最新的描述对象
+      this._stats = stats;
+    });
+  }
  // 生成express实例
  setupApp() {
    this.app = express();
  }
  // 创建http服务
  createServer() {
    this.server = http.createServer(this.app);
  }
  // 开启服务
  listen(port, host = 'localhost', cb = () => {}) {
    this.server.listen(port, host, cb);
  }
}
module.exports = Server;

5.4 webpack-dev-middleware中间件

webpack-dev-middleware 实现webpack编译和文件相关操作

webpack-dev-server/lib/Server.js

const path = require('path');
const http = require('http');
const express = require('express');
const updateCompiler = require('./utils/updateCompiler');
+ const webpackDevMiddleware = require('./webpack-dev-middleware');
​
class Server {
  constructor(compiler, options) {
    this.compiler = compiler;
    this.options = options;
    updateCompiler(compiler);
    this.clientSocketList = []; //存放着所有的通过websocket连接到服务器的客户端
    this.setupHooks();
    this.setupApp();
+    this.setupDevMiddleware();
    this.createServer();
  }
+  setupDevMiddleware() {
+    if (this.options.static) {
+      // 如果devServer配置了static,则将其作为服务器静态资源目录
+      this.app.use(express.static(this.options.static));
+    }
+    this.middleware = webpackDevMiddleware(this.compiler);
+    this.app.use(this.middleware);
+  }
  setupHooks() {
    // 监听编译完成事件,当webpack编译完成之后会调用此钩子函数
    this.compiler.hooks.done.tap('webpack-dev-server', stats => {
      // stats是一个描述对象,里面放着打包后的结果hash、chunkHash、contentHash、产生了哪些代码块、产出哪些模块等信息
      console.log('complicate done. current hash is:', stats.hash);
      // 向所有的客户端进行广播,通知各个客户端我已经编译成功了,新的模块代码已经生成,快来拉我的新代码
      this.clientSocketList.forEach(socket => {
        socket.emit('hash', stats.hash);
        socket.emit('ok');
      });
      // 记录最新的描述对象
      this._stats = stats;
    });
  }
  // 生成express实例
  setupApp() {
    this.app = express();
  }
  // 创建http服务
  createServer() {
    this.server = http.createServer(this.app);
  }
  // 开启服务
  listen(port, host = 'localhost', cb = () => {}) {
    this.server.listen(port, host, cb);
  }
}
module.exports = Server;
​

webpack-dev-server/lib/webpack-dev-middleware/index.js

const MemoryFileSystem = require('memory-fs');
const middleware = require('./middleware');
​
const memoryFileSystem = new MemoryFileSystem();
function webpackDevMiddleware(compiler) {
  // 以监听模式启动编译,如果以后文件发生变更了,webpack会重新编译
  compiler.watch({}, () => {
    console.log('start watching!');
  });
  // 使用memory-fs代替node fs, 这样可以在内存中模拟文件系统,而不用在实际的文件系统中生成文件
  // 因为每次改修都会产生两个json文件,如果用node fs开发一次本地文件系统会生成大量文件
  const fs = (compiler.outputFileSystem = memoryFileSystem);
  return middleware({
    fs,
    outputPath: compiler.options.output.path,
  });
}
​
module.exports = webpackDevMiddleware;

webpack-hmr/webpack-dev-server/lib/webpack-dev-middleware/middleware.js

const mime = require('mime');
const path = require('path');
module.exports = function wrapper(context) {
  // 真正的 express 中间件
  return function middleware(req, res, next) {
    let url = req.url;
    if (url === '/') {
      url = '/index.html';
    }
    // mock掉这个烦人的favicon
    if (url === '/favicon.ico') {
      return res.sendStatus(404);
    }
    let filename = path.join(context.outputPath, url);
    try {
      let stat = context.fs.statSync(filename);
      if (stat.isFile()) {
        let content = context.fs.readFileSync(filename);
        res.setHeader('Content-Type', mime.getType(filename));
        res.send(content);
      } else {
        res.sendStatus(404);
      }
    } catch (e) {
      res.sendStatus(404);
    }
  };
};

5.5 创建消息服务器

webpack-dev-server/lib/Server.js

const path = require('path');
const http = require('http');
const express = require('express');
+ const WebsocketServer = require('socket.io');
const updateCompiler = require('./utils/updateCompiler');
const webpackDevMiddleware = require('./webpack-dev-middleware');
​
class Server {
  constructor(compiler, options) {
    this.compiler = compiler;
    this.options = options;
    updateCompiler(compiler);
    this.clientSocketList = []; //存放着所有的通过websocket连接到服务器的客户端
    this.setupHooks();
    this.setupApp();
    this.setupDevMiddleware();
    this.createServer();
    this.createSocketServer();
  }
  setupDevMiddleware() {
    if (this.options.static) {
      // 如果devServer配置了static,则将其作为服务器静态资源目录
      this.app.use(express.static(this.options.static));
    }
    this.middleware = webpackDevMiddleware(this.compiler);
    this.app.use(this.middleware);
  }
  setupHooks() {
    // 监听编译完成事件,当webpack编译完成之后会调用此钩子函数
    this.compiler.hooks.done.tap('webpack-dev-server', stats => {
      // stats是一个描述对象,里面放着打包后的结果hash、chunkHash、contentHash、产生了哪些代码块、产出哪些模块等信息
      console.log('complicate done. current hash is:', stats.hash);
      // 向所有的客户端进行广播,通知各个客户端我已经编译成功了,新的模块代码已经生成,快来拉我的新代码
      this.clientSocketList.forEach(socket => {
        socket.emit('hash', stats.hash);
        socket.emit('ok');
      });
      // 记录最新的描述对象
      this._stats = stats;
    });
  }
  // 生成express实例
  setupApp() {
    this.app = express();
  }
  // 创建http服务
  createServer() {
    this.server = http.createServer(this.app);
  }
+  createSocketServer() {
+    // ws还是需要借助http server发送消息
+    const io = WebsocketServer(this.server);
+    io.on('connection', socket => {
+      console.log('client connected');
+      this.clientSocketList.push(socket);
+      socket.on('disconnect', () => {
+        let index = this.clientSocketList.indexOf(socket);
+        this.clientSocketList = this.clientSocketList.splice(index, 1);
+      });
+      if (this._stats) {
+        socket.emit('hash', this._stats.hash);
+       socket.emit('ok');
+     }
+    });
+  }
  // 开启服务
  listen(port, host = 'localhost', cb = () => {}) {
    this.server.listen(port, host, cb);
  }
}
module.exports = Server;
​

5.6 客户端连接消息服务器

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HMR</title>
  </head>
  <body>
    <input />
    <div id="root"></div>
+    <script src="/socket.io/socket.io.js"></script>
  </body>
</html>

webpack/hot/emitter.js

// 自己实现的一个发布订阅器,当然了你也可以用一些第三方提供的
class EventEmitter {
  constructor() {
    this.events = {};
  }
  on(eventName, fn) {
    this.events[eventName] = fn;
  }
  emit(eventName, ...args) {
    this.events[eventName](...args);
  }
}
module.exports = new EventEmitter();
​

webpack-dev-server/client/index.js

const hotEmitter = require('../../webpack/hot/emitter');
const socket = io();
let currentHash = '';
let initial = true;
socket.on('hash', hash => {
  currentHash = hash;
});
socket.on('ok', () => {
  console.log('ok');
  if (initial) {
    return (initial = false);
  }
  reloadApp();
});
function reloadApp() {
  hotEmitter.emit('webpackHotUpdate', currentHash);
}
​

webpack/hot/dev-server.js

const hotEmitter = require('../../webpack/hot/emitter');
hotEmitter.on('webpackHotUpdate', currentHash => {
  if(!lastHash){
    lastHash = currentHash;
    return;
  }
  
  module.hot.check
});

6. 打包后的代码分析(客户端部分)

要想实现真正的热模块更新,就必须维护模块之间的父子关系。当子模块发生变化时,就通知父模块的accept方法,重新加载更新子模块,所以接下来我们会对上面webpack打包出来的文件进行分析,并进行相应修改,让模块之间生成相应的父子关系,并且实现模块中的check和accept方法。

static/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HMR</title>
  </head>
  <body>
    <input />
    <div id="root"></div>
    <script src="/socket.io/socket.io.js"></script>
    <script src="hmr.js"></script>
  </body>
</html>

static/hmr.js

(() => {
  var cache = {};
  var currentHash;
  var lastHash;
​
  var modules = {
    './src/index.js': (module, exports, require) => {
      function render() {
        const title = require('./src/title.js');
        document.getElementById('root').innerHTML = title;
      }
      render();
      if (module.hot) {
        module.hot.accept(['./src/title.js'], render);
      }
    },
    './src/title.js': module => {
      module.exports = 'title';
    },
    './webpack/hot/emitter.js': module => {
      class EventEmitter {
        constructor() {
          this.events = {};
        }
        on(eventName, fn) {
          this.events[eventName] = fn;
        }
        emit(eventName, ...args) {
          this.events[eventName](...args);
        }
      }
      module.exports = new EventEmitter();
    },
  };
​
  let hotCheck = () => {
    // 6.它通过调用 JsonpMainTemplate.runtime的hotDownloadManifest方法,向 server 端发送 Ajax 请求
    // 服务端返回一个 Manifest文件,该 Manifest 包含了所有要更新的模块的 hash 值和chunk名
    hotDownloadManifest()
      .then(update => {
        update.c.forEach(chunkID => {
          // 7.调用hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码
          hotDownloadUpdateChunk(chunkID);
        });
        lastHash = currentHash;
      })
      .catch(err => {
        window.location.reload();
      });
  };
​
  let hotDownloadManifest = () => {
    let hotUpdatePath = `main.${lastHash}.hot-update.json`;
    return fetch(hotUpdatePath).then(r => r.json());
  };
​
  let hotDownloadUpdateChunk = chunkID => {
    let script = document.createElement('script');
    script.src = `${chunkID}.${lastHash}.hot-update.js`;
    document.head.appendChild(script);
  };
​
  // 8.补丁JS取回来后会调用webpackHotUpdate方法(HotModuleReplacementPlugin插件去调用的)
  self['webpackHotUpdatewebpack_hmr'] = (chunkId, moreModules) => {
    // 9.调用hotAddUpdateChunk方法动态更新模块代码
    hotAddUpdateChunk(chunkId, moreModules);
  };
​
  let hotUpdate = {};
  function hotAddUpdateChunk(chunkId, moreModules) {
    for (var moduleId in moreModules) {
      hotUpdate[moduleId] = modules[moduleId] = moreModules[moduleId];
    }
    // 10.然后调用hotApply方法进行热更新
    hotApply();
  }
​
  function hotApply() {
    for (let moduleId in hotUpdate) {
      let oldModule = cache[moduleId];
      // 删除之前的模块缓存
      delete cache[moduleId];
      // 让父模块重新执行之前通过module.hot.accept方法存入的回调函数
      oldModule.parents &&
        oldModule.parents.forEach(parentModule => {
          parentModule.hot._acceptedDependencies[moduleId] && parentModule.hot._acceptedDependencies[moduleId]();
        });
    }
  }
​
  function hotCreateModule() {
    var hot = {
      _acceptedDependencies: {},
      accept: function (deps, callback) {
        for (var i = 0; i < deps.length; i++) {
          hot._acceptedDependencies[deps[i]] = callback;
        }
      },
      check: hotCheck,
    };
    return hot;
  }
​
  // 替换原本的require方法,用来存储模块父子关系
  function hotCreateRequire(parentModuleId) {
    var parentModule = cache[parentModuleId];
    if (!parentModule) return require;
    var fn = function (childModuleId) {
      parentModule.children.push(childModuleId);
      require(childModuleId);
      let childModule = cache[childModuleId];
      childModule.parents.push(parentModule);
      return childModule.exports;
    };
    return fn;
  }
​
  function require(moduleId) {
    var cachedModule = cache[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    var module = (cache[moduleId] = {
      exports: {},
      hot: hotCreateModule(moduleId),
      parents: [],
      children: [],
    });
    modules[moduleId](module, module.exports, hotCreateRequire(moduleId));
    return module.exports;
  }
​
  (() => {
    var hotEmitter = require('./webpack/hot/emitter.js');
    var socket = io();
    var currentHash = '';
    var initial = true;
    // 1.客户端会监听到此`hash`消息,会保存此hash值
    socket.on('hash', hash => {
      if(initial) {
        lastHash = hash
      }
      currentHash = hash;
    });
    socket.on('ok', () => {
      console.log('ok');
      if (initial) {
        return (initial = false);
      }
      //2. 客户端收到ok的消息后会执行reloadApp方法进行更新
      reloadApp();
    });
    function reloadApp() {
      // 3.在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器
      hotEmitter.emit('webpackHotUpdate', currentHash);
    }
  })();
​
  (() => {
    var hotEmitter = require('./webpack/hot/emitter.js');
    // 4.监听webpackHotUpdate事件,然后执行hotCheck()方法进行检查
    hotEmitter.on('webpackHotUpdate', currentHash => {
      if (!lastHash) {
        lastHash = currentHash;
        console.log('lastHash=', lastHash, 'currentHash=', currentHash);
        return;
      }
      console.log('lastHash=', lastHash, 'currentHash=', currentHash);
      console.log('webpackHotUpdate hotCheck');
      // 在hotCheck方法里会调用module.hot.check方法
      // module.hot.check();
      hotCheck();
    });
  })();
​
  return hotCreateRequire('./src/index.js')('./src/index.js');
})();
​

webpack.config.js

devServer: {
  hot: true,
+  static: path.resolve(__dirname, 'static'),
}