Webpack module hot replacement (HMR) principle analysis

Hot Module Replacement (HMR) is one of the most exciting features webPack has introduced so far. When you make changes to your code and save it, WebPack repackages it and sends the new Module to the browser. Browsers replace old modules with new ones so that applications can be updated without refreshing the browser. For example, in the process of developing a Web page, when you click on a button and a popover appears, you find that the popover title is not aligned. At this time, you modify the CSS style and save it, and the title style changes without the browser refreshing. It feels like modifying element styles directly in Chrome’s developer tools.

This article does not tell you how to use HMR. If you are still unfamiliar with HMR, you are advised to read the HMR guide on the website, which has the simplest use cases for HMR. I will be waiting for you to come back.

Why HMR

Before Webpack HMR, there were plenty of live Reload tools or libraries, such as Live-Server, that monitored file changes and then told the browser to refresh the page, so why do we need HMR? Some of the answers have actually been mentioned above.

  • The Live Reload tool cannot save the states of the application. When the page is refreshed, the state before the application is lost. As in the previous example, click the button and a pop-up window appears. However, Webapck HMR does not refresh the browser, but performs hot replacement of modules at runtime, which ensures that application state will not be lost and improves development efficiency.
  • In ancient development process, we may need to manually run the command code for packaging, and packaging and then manually refresh the browser page, and this a series of repetitive work can be done through the HMR workflow automation, let more energy into the business, rather than wasting time on repetitive tasks.
  • HMR is compatible with most front-end frameworks or libraries on the market, such as React Hot Loader and VUE-Loader. HMR monitors changes of React or Vue components and updates the latest components to the browser in real time. Elm Hot Loader supports translation and packaging of Elm language code via Webpack, as well as HMR functionality.

Diagram of how HMR works

When I first met HMR, I felt it was very magical and had some questions lingering in my mind.

  1. Webpack can pack different modules into bundles or chunks, but when I was developing webPack HMR, I didn’t find webPack-packed files in my Dist directory. Where did they go?
  2. The package.json file for Webpack-dev-server depends on the Webpack-dev-middleware library. What role does Webpack-dev-middleware play in HMR?
  3. While using HMR, I learned from Chrome Developer tools that the browser communicates with webSocket and webpack-dev-server, but I didn’t find any new module code in the WebSocket message. How does the packaged new module get sent to the browser? Why isn’t the new module sent to the browser with the message via Websocket?
  4. When the browser gets the latest module code, how does HMR replace the old module with the new one, and how does the dependency between modules be handled during the replacement process?
  5. Is there a fallback mechanism if the module fails to be replaced during hot replacement?

With these questions in mind, I decided to dig deep into the Webpack source code to find the secrets behind HMR.

Figure 1: DIAGRAM of HMR workflow

The figure above is a module hot update flow chart of Webpack with Webpack-dev-server for application development.

  • The red box at the bottom of the image is the server side, and the orange box above is the browser side.
  • The green boxes are the areas controlled by the WebPack code. The blue boxes are the areas controlled by the WebPack-Dev-Server code, the magenta boxes are the file system, where the changes to the modified files occur, and the cyan boxes are the application itself.

The figure above shows the cycle we took to modify the code until the module’s hot update was complete, and the entire HMR process is marked by dark green Arabic numerals.

  1. First, in Webpack’s Watch mode, a file in the file system changes, WebPack listens to the file changes, recompiles and packages the module according to the configuration file, and saves the packaged code in memory through a simple JavaScript object.
  2. The second step is the interface interaction between Webpack-dev-server and Webpack. In this step, it’s mainly the interaction between Webpack-Dev-middleware of Dev-server and Webpack. Webpack-dev-middleware calls the API exposed by Webpack to monitor code changes and tells Webpack to pack the code into memory.
  3. The third step is a monitoring of file changes by Webpack-dev-server, which is different from the first step in that it does not monitor code changes for repackaging. . When we are in the configuration file is configured with devServer watchContentBase to true, the Server will listen to these configuration static files in the folder change, change will notify the browser after the live for the application to reload. Notice that this is browser refresh, and HMR are two different things.
  4. The fourth step is also the work of the webpack-dev-server code. This step mainly uses sockJS (webpack-dev-server dependency) to establish a websocket long connection between the browser and the server. This tells the browser about the status of the various stages of the Webpack compilation and packaging, as well as the information that the Server listens for static file changes in step 3. The browser performs different operations based on these socket messages. Of course, the most important information passed by the server is the hash value of the new module, and the subsequent steps use this hash value to perform module hot replacement.
  5. The webpack-dev-server/client cannot request updated code and does not perform hot module operations, handing the work back to Webpack. The job of webpack/hot/dev-server is to decide whether to refresh the browser or hot update the module based on the information passed to it by webpack-dev-server/client and the configuration of dev-server. Of course, if you just refresh the browser, there are no further steps.
  6. HotModuleReplacement runtime is the centre of the client HMR, step on it receives hash value passed to his new module, it through JsonpMainTemplate. Runtime sends an Ajax request to the server end, The server returns a JSON that contains the hash values of all the modules to be updated. Once the updated list is retrieved, the module requests the latest module code again via JSONP. These are steps 7, 8 and 9 in the figure above.
  7. In step 10, the HotModulePlugin will compare the old and new modules and decide whether to update the module. After the decision is made, the dependency relationship between the modules will be checked and the dependency references between the modules will be updated.
  8. As a final step, when HMR fails, we fall back to live Reload, which is a browser refresh to get the latest packaging code.

A simple example of using HMR

In the last part, a flow chart of HMR was used to briefly explain the process of module thermal update by HMR. Of course, you may still feel confused and unfamiliar with some of the English terms above (which stand for code repository or file modules in the repository), but in this section, I’m going to use the simplest and purest example, Through the analysis of WEPACK and Webpack-dev-server source code, the specific responsibilities of each library in the HMR process are explained in detail.

To begin the example, a brief description of the repository file, which contains the following files:

--hello.js
--index.js
--index.html
--package.json
--webpack.config.js
Copy the code

The project contains two js files, the entry file is the index.js file, and the hello.js file is a dependency of the index.js file. The js code is as you can see (click the link above to view the source code). A div element containing “Hello World” will be added to the body element.

The configuration of webpack.config.js is as follows:

const path = require('path')
const webpack = require('webpack')
module.exports = {
    entry: './index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, '/')
    },
    devServer: {
        hot: true
    }
}
Copy the code

It is worth mentioning that in the above configuration is not configured HotModuleReplacementPlugin, the reason is that when we set devServer. Hot to true, and in the package. The json file to add the following script script:

“start”: “webpack-dev-server –hot –open”

After adding – hot configuration items, devServer will tell webpack HotModuleReplacementPlugin plug-in, introduced automatic, rather than us and manual are introduced.

Go to the repository directory, NPM install install dependencies, run NPM start to start devServer service, visit http://127.0.0.1:8080 to see our page.

The next step will be the key step. In a simple example, I will modify the code in the hello.js file to analyze the HMR operation process at the source level. Of course, I will still follow the diagram above to analyze. Modify the code as follows :(the first line of all the following code blocks is the path to the file)

// hello.js -const hello = () => 'hello world' // Change hello world string to hello eleme + const hello = () => 'hello eleme'Copy the code

The Hello World text becomes Hello Eleme.

Step 1: Webpack watches the file system into memory

Webpack-dev-middleware calls webpack’s API on the watch file system, and when the hello.js file changes, Webpack recompiles it and saves it to memory.

// webpack-dev-middleware/lib/Shared.js if(! options.lazy) { var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback); context.watching = watching; }Copy the code

You may be wondering why WebPack doesn’t package files directly into output.path. Where did the files go? It turns out that Webpack packs bundle.js files into memory. The reason it doesn’t generate files is that accessing code in memory is faster than accessing files in the file system, and it also reduces the overhead of writing code to files, all thanks to memory-FS. Memory-fs is a dependent library for Webpack-dev-middleware, which replaces webpack’s outputFileSystem with a MemoryFileSystem instance. The code will then be printed out to memory. The source code for webpack-dev-Middleware is as follows:

// webpack-dev-middleware/lib/Shared.js var isMemoryFs = ! compiler.compilers && compiler.outputFileSystem instanceof MemoryFileSystem; if(isMemoryFs) { fs = compiler.outputFileSystem; } else { fs = compiler.outputFileSystem = new MemoryFileSystem(); }Copy the code

First determine whether the current fileSystem is already an instance of the MemoryFileSystem. If not, replace the outputFileSystem with the MemoryFileSystem. The bundle.js file code is stored in memory as a simple javascript object. When the browser requests the bundle.js file, devServer directly finds the javascript object stored in memory and returns it to the browser.

Step 2: devServer notifies the browser that the file has changed

At this stage, SockJS acts as a bridge between the server and the browser. When devServer is started, SockJS establishes a webSocket long connection between the server and the browser, so as to inform the browser of the various stages of webPack compilation and packaging. The most important step is that webpack-dev-server calls webpack API to listen for the done event of compile. When compile is complete, Webpack-dev-server sends the hash value of the compiled and packaged new module to the browser using the _sendStatus method.

// webpack-dev-server/lib/Server.js compiler.plugin('done', (stats) => {// stats.hash is the hash value of the latest package this._sendStats(this.sockets, stats.tojson (clientStats)); this._stats = stats; }); . Server.prototype._sendStats = function (sockets, stats, force) { if (! force && stats && (! stats.errors || stats.errors.length === 0) && stats.assets && stats.assets.every(asset => ! asset.emitted) ) { return this.sockWrite(sockets, 'still-ok'); } // Call the sockWrite method to send the hash value over the websocket to the browser this.sockWrite(sockets, 'hash', stats.hash); if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }};Copy the code

Step 3: webpack-dev-server/client responds to the server message

In case you are wondering, I did not add the code to receive webSocket messages in the business code, nor did I add a new entry file to the entry property in webpack.config.js, So where does the code in bundle.js that receives webSocket messages come from? Add webpack-dev-client code to the entry property of webpack configuration. The final bundle.js file will have code to receive webSocket messages.

Webpack-dev-server /client will temporarily store the hash value when receiving the hash message of Type OK. After receiving the message of type OK, the application will perform reload operation, as shown in the following figure. The hash message is before the OK message.

\

\

Figure 2: Websocket receives the list of messages that Dev-Server sends to the browser via SockJS

In the reload operation, webpack-dev-server/client decides whether to refresh the browser or do a hot update (HMR) to the code based on the hot configuration. The code is as follows:

// webpack-dev-server/client/index.js
hash: function msgHash(hash) {
    currentHash = hash;
},
ok: function msgOk() {
    // ...
    reloadApp();
},
// ...
function reloadApp() {
  // ...
  if (hot) {
    log.info('[WDS] App hot update...');
    const hotEmitter = require('webpack/hot/emitter');
    hotEmitter.emit('webpackHotUpdate', currentHash);
    // ...
  } else {
    log.info('[WDS] App updated. Reloading...');
    self.location.reload();
  }
}
Copy the code

As shown in the code above, the hash value is first temporarily stored in the currentHash variable, and when an OK message is received, the App is reload. If module hot updates are configured, call Webpack/Hot/Emitter to send the latest hash value to Webpack, and then give control to the WebPack client code. If module hot updates are not configured, the location.reload method is called directly to refresh the page.

Step 4: Webpack receives the latest hash value validation and requests the module code

In this step, it is actually the result of the coordination between the three modules in webpack (three files, after the English name corresponds to the file path), First, the webpack/hot/dev-server (dev-server) listens for the webpackHotUpdate message sent by the third step, webpack-dev-server/client. Call webpack/lib/HotModuleReplacement runtime (hereinafter referred to as HMR runtime) the check method of testing whether there are new update, In the process of the check will use webpack/lib/JsonpMainTemplate runtime (hereinafter referred to as the json runtime) hotDownloadUpdateChunk and two of the method HotDownloadManifest, the second method is to call AJAX to the server to request whether there is an updated file, if there is an updated file list back to the browser, and the first method is to request the latest module code through JSONP, and then return the code to the HMR Runtime, The HMR Runtime does further processing based on the new module code returned, either by refreshing the page or by hot updating the module.

\

\

Figure 3: The hotDownloadManifest method gets the list of updated files

\

\

Figure 4: hotDownloadUpdateChunk gets the updated new module code

As shown in the above two figures, it is worth noting that both requests are request file names that are concatenated using the previous hash value, while the hotDownloadManifest method returns the latest hash value. The hotDownloadUpdateChunk method returns the code block for the latest hash value. The new code block is then returned to HMR Runtime for module hot update.

Remember question 3 from the HMR working diagram? Why is the code to update the module not sent directly to the browser via Websocket in step 3, but retrieved via JSONP? Dev-server /client is only responsible for message passing and not for new module fetching. This should be done by HMR Runtime. HMR Runtime should be the place to fetch new code. Another interesting thing about using Webpack-hot-middleware is that it’s possible to use webpack-hot-middleware in conjunction with Webpack without using webpack-dev-server. Instead of using a Websocket, it uses an EventSource. In summary, new module code should not be placed in webSocket messages in the HMR workflow.

Step 5: HotModuleReplacement. The runtime for hot update module

This step is a key step in the entire module hot update (HMR), and module hot updates occur in the hotApply method in THE HMR Runtime. I am not going to post the entire hotApply method source code here, because it contains over 300 lines of code. I will only take the key snippet.

// webpack/lib/HotModuleReplacement.runtime function hotApply() { // ... var idx; var queue = outdatedModules.slice(); while(queue.length > 0) { moduleId = queue.pop(); module = installedModules[moduleId]; / /... // remove module from cache delete installedModules[moduleId]; // when disposing there is no need to call dispose handler delete outdatedDependencies[moduleId]; // remove "parents" references from all children for(j = 0; j < module.children.length; j++) { var child = installedModules[module.children[j]]; if(! child) continue; idx = child.parents.indexOf(moduleId); if(idx >= 0) { child.parents.splice(idx, 1); }}} //... // insert new code for(moduleId in appliedUpdate) { if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) { modules[moduleId] = appliedUpdate[moduleId]; }} / /... }Copy the code

The first stage is to find outdatedModules and outdatedDependencies. If you are interested in hotApply, you can read the source code. The second phase removes expired modules and dependencies from the cache as follows:

delete installedModules[moduleId];

delete outdatedDependencies[moduleId];

The third stage is to add the new module to modules, and the next time you call webpack_require (webpack’s rewritten require method), you get the new module code.

If an error occurs during the hot update process, the hot update will revert to the refresh browser. This part of the code is in the dev-server code, the brief code is as follows:

module.hot.check(true).then(function(updatedModules) {
    if(!updatedModules) {
        return window.location.reload();
    }
    // ...
}).catch(function(err) {
    var status = module.hot.status();
    if(["abort", "fail"].indexOf(status) >= 0) {
        window.location.reload();
    }
});
Copy the code

Dev-server first verifies if there are any updates, and then reloads the browser if there are no code updates. If abort or fail errors occur during hotApply, reload the browser as well.

Step 6: What does the business code need to do?

After replacing the old module with the new module code, our business code cannot know that the code has changed. That is to say, when the hello.js file is modified, we need to call the HMR Accept method in the index.js file to add the processing function after the module update. Insert the return value of the Hello method into the page in time. The code is as follows:

// index.js
if(module.hot) {
    module.hot.accept('./hello.js', function() {
        div.innerHTML = hello()
    })
}
Copy the code

This is the whole HMR workflow.

Reference data: zhuanlan.zhihu.com/p/30669007