What is module hot update?
Hot Module replacement (or HMR) is one of the most useful features webPack provides. It allows all types of modules to be updated at run time without a complete refresh.
Let’s run an example to get a better sense of what module hot updates are.
In the video, I changed the font color and the page immediately updated, but the content in the input field remained. HMR helps us achieve this effect, otherwise we would have to manually refresh the page every time we change the code, and the content of the page would not remain. The benefits of module hot updates are obvious, as they can save development time and improve the development experience.
Careful students may notice that WebPack automatically recompiles and generates two more files.
- How does HMR automatically compile?
- How do browsers perceive changes to the contents of modules?
- And what about the two new files?
- How is local updating done?
With these questions in mind, let’s explore the principle of module hot update.
Configuration of module hot update
Before learning the principle, we need to have a clear understanding of the configuration of module hot update. Because we rarely need to configure ourselves manually in our daily work, we will ignore some details. Now let’s review the configuration process to help you understand the source code.
Step 1: Install webpack-dev-server
npm install --save-dev. webpack-dev-server
Copy the code
Step 2: Register the module.hot.accept event in the parent module
//src/index.js
let div = document.createElement('div');
document.body.appendChild(div);
let input = document.createElement('input');
document.body.appendChild(input);
let render = () = > {
let title = require('./title.js')
div.innerHTML = title;
}
render()
// Add the following content
+ if (module.hot) {
+ module.hot.accept(['./title.js'], render)
+ }
Copy the code
// submodule SRC /title.js
module.exports = 'Hello webpack'
Copy the code
Step 3: Configure hot:true in webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development'.devtool: 'source-map'.entry: './src/index.js'.output: {
filename: 'main.js'.path: path.resolve(__dirname, 'dist')
},
+ devServer: {
+ hot: true+},plugins: [
new HtmlWebpackPlugin(),
],
}
Copy the code
Now you may be wondering why hot updates can be implemented without listening to module.hot.accept when modifying code. That’s because the Loader we’re using is already doing it for us behind the scenes.
Webpack-dev-server provides real-time reloading, but not partial refreshing. Must match after two steps of configuration to achieve partial refresh, these two steps behind is actually using the HotModuleReplacementPlugin.
Is to say the HMR webpack dev – server and HotModuleReplacementPlugin common credit.
Principle of thermal renewal
Now let’s get into our topic for today. Let’s introduce the first protagonist: Webpack-dev-server.
Webpack-dev-server
The package.json file in node_modules/webpack-dev-server can be used to find the file where the command is actually run based on the value of bin. ./node_modules/webpack-dev-server/bin/webpack-dev-server.js
Let’s follow the entry file to see what webpack-dev-server does. In order to reduce the length, improve the quality of reading, the following examples are simple version of the implementation, interested can refer to the source code together.
1. Enable the local service
An instance of compiler was first created through Webpack, and then a local service was turned on by creating a custom Server instance.
// node_modules/webpack-dev-server/bin/webpack-dev-server.js
const webpack = require('webpack');
const config = require('.. /.. /webpack.config');
const Server = require('.. /lib/Server')
const compiler = webpack(config);
const server = new Server(compiler);
server.listen(8080.'localhost'.() = > {})
Copy the code
This custom Server not only creates an HTTP service, but also creates a Websocket service based on the HTTP service. Meanwhile, it monitors the access of the browser and sends hash value to the browser when the browser successfully accesses, so as to realize the two-way communication between the Server and the browser.
// node_modules/webpack-dev-server/lib/Server.js
class Server {
constructor() {
this.setupApp();
this.createServer();
}
// Create an HTTP application
setupApp() {
this.app = express();
}
// Create an HTTP service
createServer() {
this.server = http.createServer(this.app);
}
// Listen on the port number
listen(port, host, callback) {
this.server.listen(port, host, callback)
this.createSocketServer();
}
// Create a Websocket service based on the HTTP service and register a listening event connection
createSocketServer() {
const io = socketIO(this.server);
io.on('connection'.(socket) = > {
this.clientSocketList.push(socket);
socket.emit('hash'.this.currentHash);
socket.emit('ok');
socket.on('disconnect'.() = > {
let index = this.clientSocketList.indexOf(socket);
this.clientSocketList.splice(index, 1)})})}}module.exports = Server;
Copy the code
2. Monitor compilation completed
It’s not enough for the server to notify the browser of the hash and pull code when the WebSocket connection is established. We also want the browser to be notified when the code changes. Therefore, you also need to listen for compile completion events before starting the service.
// When the compilation is complete, the websocket sends a broadcast to the browser
setupHooks() {
let { compiler } = this;
compiler.hooks.done.tap('webpack-dev-server'.(stats) = > {
this.currentHash = stats.hash;
this.clientSocketList.forEach((socket) = > {
socket.emit('hash'.this.currentHash);
socket.emit('ok'); })})}Copy the code
3. Listen for file modification
To trigger recompilation when code changes, you need to listen for code changes. The source code for this step is the webpackDevMiddleware library. In the library, Compiler. watch is used to monitor the modification of files, and memory-FS is used to store the compiled products in memory, which is why we can’t see the changes in the dist directory. The benefit of putting them in memory is to improve the development efficiency by faster reading and writing.
// node_modules/webpack-dev-middleware/index.js
const MemoryFs = require('memory-fs')
compiler.watch({}, () = > {})
let fs = new MemoryFs();
this.fs = compiler.outputFileSystem = fs;
Copy the code
4. Insert client code into the browser
As mentioned above, in order to realize the communication between the browser and the local service, it is necessary for the browser to access the websocket service opened locally. However, the browser itself does not have such ability, which requires us to provide such client code to run it in the browser. So before the custom Server starts the HTTP service, it calls the updateCompiler() method, which modifies entry in the WebPack configuration so that the code for the two inserted files can be packaged together in main.js to run in the browser.
//node_modules/webpack-dev-server/lib/utils/updateCompiler.js
const path = require('path');
function updateCompiler(compiler) {
compiler.options.entry = {
main: [
path.resolve(__dirname, '.. /.. /client/index.js'),
path.resolve(__dirname, '.. /.. /.. /webpack/hot/dev-server.js'),
config.entry,
]
}
}
module.exports = updateCompiler
Copy the code
node_modules /webpack-dev-server/client/index.js
This code is placed in the browser as client code and is used to establish a Websocket connection, save the hash when the server sends a hash broadcast, and call reloadApp() when the server sends an OK broadcast.
let currentHash;
let hotEmitter = new EventEmitter();
const socket = window.io('/');
socket.on('hash'.(hash) = > {
currentHash = hash;
})
socket.on('ok'.() = > {
reloadApp();
})
function reloadApp() {
hotEmitter.emit('webpackHotUpdate', currentHash)
}
Copy the code
webpack/hot/dev-server.js
ReloadApp () continues to call module.hot.check(), which of course is not called when the page is first loaded. As for why it is divided into two files, I understand that each module is responsible for different division of labor in order to understand lotus root.
let lastHash;
hotEmitter.on('webpackHotUpdate'.(currentHash) = > {
if(! lastHash) { lastHash = currentHash;return;
}
module.hot.check();
})
Copy the code
Where does module.hot.check() come from? The answer is HotModuleReplacementPlugin. We can see, under the sources of the browser. The main js is inserted into a lot of code, the code is being HotModuleReplacementPlugin inserted.
Not only does it insert the code in main.js, it also generates the two patches generated after compilation mentioned earlier.
HotModuleReplacementPlugin
Now, let’s take a look at today’s second leading HotModuleReplacementPlugin in the main, js quietly inserted which code, so as to realize the hot update.
1. Add the hot attribute to the module
As mentioned earlier, when code changes, the server sends an OK message to the browser, which executes module.hot.check for module heat. This is where the check method comes from.
function hotCreateModule() {
let hot = {
_acceptedDependencies: {},
accept(deps, callback) {
deps.forEach(dep= > hot._acceptedDependencies[dep] = callback);
},
check: hotCheck
}
return hot
}
Copy the code
2. Request the patch file
Module.hot.check () calls hotCheck, and the browser gets two patch files from the server.
function hotCheck() {
hotDownloadManifest().then(update= > {
//{"h":"eb861ba9f6408c42f1fd","c":{"main":true}}
let chunkIds = Object.keys(update.c) //['main']
chunkIds.forEach(chunkId= > {
hotDownloadUpdateChunk(chunkId)
})
lastHash = currentHash;
}).catch(() = > {
window.location.reload(); })}Copy the code
What do these two files look like
- d04feccfa446b174bc10.hot-update.json
Tell the browser the new hash value and which chunk changed
- main.d04feccfa446b174bc10.hot-update.js
Tell the browser that the/SRC /title.js module in the main block has changed
The first is to request the hot-update.json file using the hash value saved last time using XMLHttpRequest. The purpose of this description file is to provide the chunkId of the modified file.
function hotDownloadManifest() {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
let url = `${lastHash}.hot-update.json`
xhr.open('get', url);
xhr.responseType = 'json'
xhr.onload = function () {
resolve(xhr.response)
}
xhr.send()
})
}
Copy the code
JSONP then uses the chunkId returned by hot-update.json and the hash file name saved last time to get the file content.
function hotDownloadUpdateChunk(chunkId) {
let script = document.createElement('script');
script.src = `${chunkId}.${lastHash}.hot-update.js`;
document.head.appendChild(script);
}
window.webpackHotUpdate = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
}
Copy the code
3. Replacement of module content
When the hot-update.js file is loaded, window.webpackHotUpdate is executed and hotApply is invoked. HotApply finds the old module by its ID and removes it, then performs the Accept callback registered in the parent module to implement a local update of the module’s contents.
window.webpackHotUpdate = function (chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
}
let hotUpdate = {}
function hotAddUpdateChunk(chunkId, moreModules) {
for (let moduleId in moreModules) {
modules[moduleId] = hotUpdate[moduleId] = moreModules[moduleId];
}
hotApply();
}
function hotApply() {
for (let moduleId in hotUpdate) {
let oldModule = installedModules[moduleId]
delete installedModules[moduleId]
oldModule.parents.forEach((parentModule) = > {
let cb = parentModule.hot._acceptedDependencies[moduleId]
cb && cb()
})
}
}
Copy the code
conclusion
Summary of module hot update principle:
After NPM run dev is executed, the compiler entry is first modified by the updateCompiler method, and the code of the two files is packaged into main.js. One of the two files is used to communicate with the server. One is used to call module.hot.check. Tap to listen for compilation completion, watch to listen for code changes, and use createSocketServer() to enable the HTTP service and websocekt service.
When the user accesses http://localhost:8080, the browser establishes a Websocket connection with the server. The server then sends the browser hash and OK, which inform the browser of the hash value of the latest compiled version and tell the browser to pull the code. At the same time, the server will return the file in memory according to the route. Then, the browser saves the hash and the page content is displayed.
Recompilation is triggered when native code is modified, and webpackDevMiddleWare stores the compiled artifacts in memory, thanks to the built-in module memory-fs. HotModuleReplacementPlugin will generate two patches at the same time, the two patches a which is used to tell the browser the chunk changed, one is used to tell the browser module and content change. When the recompilation is complete, the browser will save the current hash, then concatenate the description file path with the hash value of the previous one, and concatenate the patch file for another request based on what the description file returns. WebpckHotUdate will be executed after the request is successful, and hotApply will be called again. In fact, the callback event in the second step of configuring the module hot update will be executed, thus achieving a partial refresh of the page content.
Reference Documents:
Module hot replacement | webpack Chinese documents
Easy to understand webpack hot update principle – nuggets