什么是HMR
Hot Module Replacement
是指当我们对代码修改并保存后,webpack将会对代码进行重新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面
使用HMR
安装
yarn add webpack webpack-cli webpack-dev-server html-webpack-plugin socket.io socket.io-client events mime fs-extra --dev
使用
webpack.config.js
webpack.config.js
let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
let HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {mode: "development",entry:"./src/index.js",output: {filename: "[name].js",path: path.resolve(__dirname, "dist")},devServer:{hot:true,port:8000,contentBase:path.join(__dirname,'static')},plugins: [new HtmlWebpackPlugin({template:'./src/index.html'}),new HotModuleReplacementPlugin()]
}
src\index.html
src\index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>hmr</title>
</head>
<body><input/><div id="root"></div>
</body>
</html>
src\index.js
src\index.js
let render = () => {let title = require("./title.js");root.innerText = title;
}
render();if (module.hot) {module.hot.accept(["./title.js"], render);
}
title.js
src\title.js
module.exports = "title";
package.json
"scripts": {"build": "webpack","dev": "webpack serve"
}
debugger
"scripts": {"build": "webpack","dev": "webpack serve",
+ "debug": "webpack serve"},
基础知识
module和chunk
- 在 webpack里有各种各样的模块
- 一般一个入口会依赖多个模块
- 一个入口一般会对应一个chunk,这个chunk里包含这个入口依赖的所有的模块
HotModuleReplacementPlugin
- webpack\lib\HotModuleReplacementPlugin.js
- 它会生成两个补丁文件
- 上一次编译生成的hash.hot-update.json,说明从上次编译到现在哪些代码块发生成改变
- chunk名字.上一次编译生成的hash.hot-update.js,存放着此代码块最新的模块定义,里面会调用
webpackHotUpdate
方法
- 向代码块中注入HMR runtime代码,热更新的主要逻辑,比如拉取代码、执行代码、执行accept回调都是它注入的到chunk中的
hotCreateRequire
会帮我们给模块 module的parents
、children
赋值
webpack的监控模式
- 如果使用监控模式编译webpack的话,如果文件系统中有文件发生了改变,webpack会监听到并重新打包
- 每次编译会产生一个新的hash值
工作流程
服务器部分
- 启动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 |
客户端部分
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
包含了所有要更新的模块的hash
值和chunk名 - 调用
JsonpMainTemplate.runtime
的hotDownloadUpdateChunk
方法通过JSONP请求获取到最新的模块代码 - 补丁JS取回来后会调用
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 |
相关代码
- webpack-dev-server.js
- Server.js
- webpack-dev-middleware/index.js
- SockJSServer.js
启动开发服务器
startDevServer.js
startDevServer.js
const webpack = require("webpack")
const Server = require('./webpack-dev-server/lib/Server');
const config = require("./webpack.config")
function startDevServer(compiler,options) {const devServerOptions = options.devServer||{};const server = new Server(compiler, devServerOptions);const {host='localhost',port=8080}=devServerOptions;server.listen(port, host, (err) => {console.log(`Project is running at http://${host}:${port}`);});
}
const compiler = webpack(config);
startDevServer(compiler,config);
Server.js
webpack-dev-server\lib\Server.js
const express = require("express");
const http = require("http");
class Server {constructor(compiler,devServerOptions) {this.compiler = compiler;this.devServerOptions = devServerOptions;this.setupApp();this.createServer();}setupApp() {this.app = new express();}createServer() {this.server = http.createServer(this.app);}listen(port, host = "localhost", callback = ()=>{}) {this.server.listen(port, host, callback);}
}
module.exports = Server;
package.json
package.json
"scripts": {"build": "webpack","dev": "webpack-dev-server",
+ "start":"node ./startDevServer.js"},
给entry添加客户端
Server.js
webpack-dev-server\lib\server\Server.js
const express = require("express");
+const updateCompiler = require('./utils/updateCompiler');
const http = require("http");
class Server {constructor(compiler,devServerOptions) {this.compiler = compiler;this.devServerOptions = devServerOptions;
+ updateCompiler(compiler);this.setupApp();this.createServer();}setupApp() {this.app = new express();}createServer() {this.server = http.createServer(this.app);}listen(port, host = "localhost", callback = ()=>{}) {this.server.listen(port, host, callback);}
}
module.exports = Server;
updateCompiler.js
webpack-dev-server\lib\utils\updateCompiler.js
const path = require("path");
let updateCompiler = (compiler) => {const config = compiler.options;//来自webpack-dev-server/client/index.js 在浏览器启动WS客户端config.entry.main.import.unshift(require.resolve("../../client/index.js"),);//webpack/hot/dev-server.js 在浏览器监听WS发射出来的webpackHotUpdate事件config.entry.main.import.unshift(require.resolve("../../../webpack/hot/dev-server.js"));console.log(config.entry);compiler.hooks.entryOption.call(config.context, config.entry);
}
module.exports = updateCompiler;
client\index.js
webpack-dev-server\lib\client\index.js
console.log('webpack-dev-server\client\index.js');
dev-server.js
webpack-dev-server\lib\client\hot\dev-server.js
console.log('webpack-dev-server\lib\client\hot\dev-server.js');
添加webpack的done事件回调
Server.js
webpack-dev-server\lib\server\Server.js
const express = require("express");
const updateCompiler = require('./utils/updateCompiler');
const http = require("http");
class Server {constructor(compiler,devServerOptions) {this.compiler = compiler;this.devServerOptions=devServerOptions;updateCompiler(compiler);
+ this.sockets = [];
+ this.setupHooks();this.setupApp();this.createServer();}
+ setupHooks() {
+ this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {
+ console.log("stats.hash", stats.hash);
+ this.sockets.forEach((socket) => {
+ socket.emit("hash", stats.hash);
+ socket.emit("ok");
+ });
+ this._stats = stats;
+ });
+ }setupApp() {this.app = new express();}createServer() {this.server = http.createServer(this.app);}listen(port, host = "localhost", callback = ()=>{}) {this.server.listen(port, host, callback);}
}
module.exports = Server;
webpack-dev-middleware中间件
webpack-dev-middleware
实现webpack编译和文件相关操作
Server.js
webpack-dev-server\lib\Server.js
const express = require("express");
const updateCompiler = require('./utils/updateCompiler');
+const webpackDevMiddleware = require('../../webpack-dev-middleware');
const http = require("http");
class Server {constructor(compiler,devServerOptions) {this.compiler = compiler;this.devServerOptions = devServerOptions;updateCompiler(compiler);this.sockets = [];this.setupHooks();this.setupApp();
+ this.setupDevMiddleware();this.createServer();}
+ setupDevMiddleware() {
+ if(this.devServerOptions.contentBase)
+ this.app.use(express.static(this.devServerOptions.contentBase));
+ this.middleware = webpackDevMiddleware(this.compiler);
+ this.app.use(this.middleware);
+ }setupHooks() {this.compiler.hooks.done.tap('webpack-dev-server', (stats) => {console.log("stats.hash", stats.hash);this.sockets.forEach((socket) => {socket.emit("hash", stats.hash);socket.emit("ok");});this._stats = stats;});}setupApp() {this.app = new express();}createServer() {this.server = http.createServer(this.app);}listen(port, host = "localhost", callback = ()=>{}) {this.server.listen(port, host, callback);}
}
module.exports = Server;
webpack-dev-middleware\index.js
webpack-dev-middleware\index.js
const middleware = require("./middleware");
const MemoryFileSystem = require("memory-fs");
let memoryFileSystem = new MemoryFileSystem();
function webpackDevMiddleware(compiler) {compiler.watch({}, () => {console.log("start watching!");});let fs = compiler.outputFileSystem = memoryFileSystem;return middleware({fs,outputPath:compiler.options.output.path});
}module.exports = webpackDevMiddleware;
middleware.js
webpack-dev-middleware\middleware.js
const mime = require('mime');
const path = require("path");
module.exports = function wrapper(context) {return function middleware(req, res, next) {let url = req.url;if (url === "/") { url = "/index.html"; }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 (error) {res.sendStatus(404);}};
};