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
创建以下文件
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端口已经运行起来我们的项目了
动修改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生成的),从而完成更新
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的parents
、children
赋值
3.3 webpack的监控模式
- 如果使用监控模式编译webpack的话,如果文件系统中有文件发生了改变,webpack会监听到并重新打包
- 每次编译会产生一个新的hash值
4. HMR工作流程
4.1. 服务器部分
- 启动webpack-dev-server服务器
- 创建webpack实例
- 创建Server服务器
- 添加webpack的
done
事件回调,在编译完成后会向浏览器发送消息 - 创建express应用app
- 使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中
- 设置文件系统为内存文件系统
- 添加webpack-dev-middleware中间件
- 创建http服务器并启动服务
- 使用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.js | addEntries.js#L22 |
entry添加webpack/hot/dev-server.js | addEntries.js#L30 |
5. setupHooks | Server.js#L122 |
6. 添加webpack的done 事件回调 | Server.js#L183 |
编译完成向websocket客户端推送消息,最主要信息还是新模块的hash值,后面的步骤根据这一hash值来进行模块热替换 | Server.js#L178 |
7.创建express应用app | Server.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. 客户端部分
webpack-dev-server/client-src/default/index.js
端会监听到此hash
消息,会保存此hash值- 客户端收到
ok
的消息后会执行reloadApp
方法进行更新 - 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射
webpackHotUpdate
事件,如果不支持则直接刷新浏览器 - 在
webpack/hot/dev-server.js
会监听webpackHotUpdate
事件,然后执行check()
方法进行检查 - 在check方法里会调用
module.hot.check
方法 - 它通过调用
JsonpMainTemplate.runtime
的hotDownloadManifest
方法,向 server 端发送 Ajax 请求,服务端返回一个Manifest
文件,该Manifest
包含了所有要更新的模块chunk名 - 调用
JsonpMainTemplate.runtime
的hotDownloadUpdateChunk
方法通过JSONP请求获取到最新的模块代码 - 补丁取回来后会调用
JsonpMainTemplate.runtime.js
的webpackHotUpdate
方法,里面会调用hotAddUpdateChunk
方法,用新的模块替换掉旧的模块 - 然后会调用
HotModuleReplacement.runtime.js
的hotAddUpdateChunk
方法动态更新模块代 码 - 然后调用
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.runtime 的hotDownloadUpdateChunk 方法通过JSONP请求获取到最新的模块代码 | JsonpMainTemplate.runtime.js#L14 |
9. 补丁JS取回来后会调用JsonpMainTemplate.runtime.js 的webpackHotUpdate 方法 | JsonpMainTemplate.runtime.js#L8 |
10. 然后会调用HotModuleReplacement.runtime.js 的hotAddUpdateChunk 方法动态更新模块代码 | 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 |
一张图概括
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'),
}