What is the HMR

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.

configuration

  1. Webpack – dev – server installation
    npm install --save-dev webpack-dev-server
Copy the code
  1. Configuration devServer
    devServer: {
        contentBase: './dist'
    }
Copy the code
  1. Configuring the Opening Entrance
"start": "webpack-dev-server --open -hot"
Copy the code

Some doubt

  1. Where does webpack pack files go
  2. What role does Webpack-dev-Middleware play in HMR
  3. In the process of HMR, we can know that the browser communicates through Websocket through Chrome development tool, but there is no code of the new module found in the Message of Websocket. How does the packaged new module get sent to the browser?
  4. When the browser gets the new module code, how does HMR replace the old module with the new one?
  5. Is there a fallback mechanism for hot replacement of a module if the replacement fails?

With these questions in mind, we continue to explore the mysteries of HMR

The build process for webPack

After the project is started and the build is packaged, the console prints out the build process and we can see that a Hash value has been generated:a93fd735d02d98633356 Then, after each of our code modifications save, the console appears with Compiling… The command, which triggers a new compilation, can be observed in the console:

  • The new Hash value is a61bDD6e82294ED06FA3
  • New JSON file: a93fd735d02d98633356.hot-update.json
  • New js file: index. A93fd735d02d98633356. Hot – update. Js

First, we know that the Hash value represents the identity of each compilation. Second, according to the newly generated file name, the Hash value output last time is used as the identification of the newly generated file. By analogy, the Hash value is used as the identifier of the next hot update.

And let’s see, what’s the newly generated file? Each time you change the code, a recompilation is triggered, and the browser makes two requests. The request is the newly generated 2 files. As follows:

First look at the JSON file. In the returned result, H represents the Hash value generated this time and is used as the prefix for the next hot update request of the file. C indicates that the file to be hot updated corresponds to the index module.

Take a look at the generated JS file, that is the modified code, recompiled and packaged.

Implementation principle of hot update

Webpack-dev-server starts the local service

We can find the entry file bin/webpack-dev-server.js according to the bin command in package.json of webpack-dev-server.

// node_modules/webpack-dev-server/bin/webpack-dev-server.js

// Generate webpack compiler for main engine
let compiler = webpack(config);

// Start the local service
let server = new Server(compiler, options, log);
server.listen(options.port, options.host, (err) = > {
    if (err) {throw err};
});
Copy the code

Local service code:

// node_modules/webpack-dev-server/lib/Server.js
class Server {
    constructor() {
        this.setupApp();
        this.createServer();
    }
    
    setupApp() {
        // Depends on express
    	this.app = new express();
    }
    
    createServer() {
        this.listeningApp = http.createServer(this.app);
    }
    
    listen(port, hostname, fn) {
        return this.listeningApp.listen(port, hostname, (err) = > {
            // After the Express service is started, the WebSocket service is started
            this.createSocketServer(); }}}Copy the code

This section of code does three things:

  • Start the Webpack and generate the Compiler instance. Compiler has many methods, such as starting all webpack compilation and listening for changes to local files.
  • Start the local Server using the Express framework so that the browser can request local static resources.
  • After the local server is started, start the WebSocket service. Through Websocket, you can establish bidirectional communication between local service and browser. This way, when the local file changes, the browser can be notified immediately to hot update the code!

The above code does three things, but the source code does a lot more before starting the service. What else does webpack-dev-server /lib/server.js do?

Modify the entry configuration of webpack.config.js

The updateCompiler(this.piler) method is called before starting the local service. There are two key pieces of code in this method. One is to get the WebSocket client code path, and the other is to get the WebPack hot update code path based on the configuration.

// Get the webSocket client code
const clientEntry = `The ${require.resolve(
    '.. /.. /client/'
)}?${domain}${sockHost}${sockPath}${sockPort}`;

// Get hot update code based on configuration
let hotEntry;
if (options.hot) {
    hotEntry = require.resolve('webpack/hot/dev-server');
}
Copy the code

The modified WebPack entry configuration is as follows:

// The modified entry entry
{ entry:
    { index: 
        [
            // clientEntry obtained above
            'xxx/node_modules/webpack-dev-server/client/index.js? http://localhost:8080'.// hotEntry obtained above
            'xxx/node_modules/webpack/hot/dev-server.js'.// Development configuration entry
            './src/index.js',}}Copy the code

Adding two files to the entry means that they are packaged together in the bundle, so why add two files?

  • webpack-dev-server/client/index.js

This file is used for the Websocket, since websoket is two-way communication, the local server webSocket is started during step 1 webpack-dev-server initialization. What about the client, which is our browser, which doesn’t have the code to communicate with the server? So we need to sneak the Websocket client communication code into our code.

  • webpack/hot/dev-server.js

This file is mainly used to check the update logic, as discussed later.

Listen for webpack compilation to finish

After modifying the entry configuration, the setupHooks method is called. This method is used to register listening events for each webpack compilation.

// node_modules/webpack-dev-server/lib/Server.js
// Bind listener events
setupHooks() {
    const {done} = compiler.hooks;
    // Listen for webPack's done hook, tapable's listener method
    done.tap('webpack-dev-server'.(stats) = > {
        this._sendStats(this.sockets, this.getStats(stats));
        this._stats = stats;
    });
};
Copy the code

When a webpack compilation is complete, the _sendStats method is called to send notifications, OK, and hash events to the browser through the websocket so that the browser can get the latest hash value to check and update the logic.

// Send a message to the client through the websoket
_sendStats() {
    this.sockWrite(sockets, 'hash', stats.hash);
    this.sockWrite(sockets, 'ok');
}
Copy the code

Webpack listens for file changes

Every time you change the code, a compilation is triggered. We also need to listen for changes to our native code, mainly through the setupDevMiddleware method.

This method basically implements the Webpack-dev-Middleware library. Many people can’t tell the difference between Webpack-dev-middleware and webpack-dev-server. Since webpack-dev-server is only responsible for service startup and setup, all file-related operations have been removed to the Webpack-dev-Middleware library, mainly for compiling and exporting local files and listening.

Let’s take a look at the webpack-dev-Middleware source code and see what it does:

// node_modules/webpack-dev-middleware/index.js
compiler.watch(options.watchOptions, (err) = > {
    if (err) { /* Error handling */}});// Write the packed file to memory through the "memory-fs" library
setFs(context, compiler); 
Copy the code

1. The compiler.watch method is called, which mainly does two things:

  • The first is a compilation package for the native file code, which is a series of compilation processes for Webpack.
  • Second, after the compilation is complete, enable the monitoring of local files. When the file changes, recompile the file, and continue to listen after the compilation is complete.

2. Execute the setFs method, whose main purpose is to package the compiled file into memory.

That’s why during development, you’ll find no packaged code in the dist directory, because it’s all in memory. The reason 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.

The browser receives a notification of hot updates

We can already listen for changes to the file and trigger a recompile when the file changes.

It also listens for events at the end of each compilation. When listening for a Webpack compile, the _sendStats method sends a notification to the browser via websoket to check if a hot update is needed.

The following focuses on what the OK and hash events in the _sendStats method do.

How does the browser receive webSocket messages? Recall the entry file added in step 2, which is the WebSocket client code.

'xxx/node_modules/webpack-dev-server/client/index.js? http://localhost:8080'
Copy the code

The code for this file is packaged into bundle.js and runs in the browser. Take a look at the core code of this file:

// webpack-dev-server/client/index.js
var socket = require('./socket');
var onSocketMessage = {
    hash: function hash(_hash) {
        // Update currentHash
        status.currentHash = _hash;
    },
    ok: function ok() {
        sendMessage('Ok');
        // Perform operations such as update checkreloadApp(options, status); }};SocketUrl,? http://localhost:8080, local service address
socket(socketUrl, onSocketMessage);

function reloadApp() {
	if (hot) {
        log.info('[WDS] App hot update... ');
        
        // hotEmitter is an instance of EventEmitter
        var hotEmitter = require('webpack/hot/emitter');
        hotEmitter.emit('webpackHotUpdate', currentHash); }}Copy the code

The socket method establishes a connection between the Websocket and the server and registers two listening events.

  • Hash event, which updates the hash value after the last packing.
  • Ok event for hot update check.

The hot update check event is a call to the reloadApp method. This method also uses EventEmitter of Node.js to emit webpackHotUpdate messages. Why not just check for updates? Personal understanding is for better maintenance of the code, and a clearer division of responsibilities. Websocket is only used by the client (browser) to communicate with the server. The real work of doing things is handed back to Webpack.

So what does Webpack do? Let’s recall step 2. There is still one file not mentioned in the entry file, which is:

'xxx/node_modules/webpack/hot/dev-server.js'
Copy the code

The code for this file will also be packaged into bundle.js and run in the browser. Let’s see what this file does:

// node_modules/webpack/hot/dev-server.js
var check = function check() {
    module.hot.check(true)
        .then(function(updatedModules) {
            // Refresh the page directly
            if(! updatedModules) {window.location.reload();
                return;
            }
            
            // The hot update is finished, and the information is printed
            if (upToDate()) {
                log("info"."[HMR] App is up to date.");
            }
    })
        .catch(function(err) {
            window.location.reload();
        });
};

var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate".function(currentHash) {
    lastHash = currentHash;
    check();
});
Copy the code

Here WebPack listens for the webpackHotUpdate event, retrieves the latest hash value, and finally checks for updates.

To check for updates, the module.hot.check method is called. Module.hot. check is a hot hot check. The answer is HotModuleReplacementPlugin, leave a question here, then we continue to look down.

HotModuleReplacementPlugin

Seems to have been in front of the webpack dev – server to do the HotModuleReplacementPlugin during hot update and do great things?

First of all, you can compare bundle.js with hot updates configured and bundle.js not configured.

  • (1) Not configured.

  • (2) configure the HotModuleReplacementPlugin or – hot.

We noticed that moudle added a hot attribute. Look at the hotCreateModule method. That’s where module.hot.check came from.

Comparing the packaged files, the moudle in webpack_require and the number of lines of code. We can find HotModuleReplacementPlugin originally is also quietly filled a lot of code to bundle. The js. This is very similar to step 2. Why fortress the code? Because checking for updates is done in the browser, the code must be in the client’s environment.

Moudle.hot. check starts hot updates

From the previous step, we saw where the moudle.hot.check method came from. What did that do?

  • Using the hash value saved last time, call hotDownloadManifest to send XXX /hash.hot-update.json ajax request;

Result Obtain the hot update module and the Hash id of the next hot update, and enter the hot update preparation phase.

hotAvailableFilesMap = update.c; // The file needs to be updated
hotUpdateNewHash = update.h; // Updates the hash value for the next hot update
hotSetStatus("prepare"); // The system enters the hot update state
Copy the code
  • Call hotDownloadUpdateChunk and send XXX /hash.hot-update.js in JSONP mode.
function hotDownloadUpdateChunk(chunkId) {
    var script = document.createElement("script");
    script.charset = "utf-8";
    script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
    if (null) script.crossOrigin = null;
    document.head.appendChild(script);
 }
Copy the code

You can see that the newly compiled code is inside a webpackHotUpdate function body.

Now look at the Webpack Host pDate method.

window["webpackHotUpdate"] = function (chunkId, moreModules) { hotAddUpdateChunk(chunkId, moreModules); };Copy the code

This method does two things:

  • The hotAddUpdateChunk method assigns updated moreModules values to the global full hotUpdate.
  • The hotUpdateDownloaded method calls hotApply to replace the code.
function hotAddUpdateChunk(chunkId, moreModules) {
    // Updated moreModules assign values to the global full hotUpdate
    for (var moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { hotUpdate[moduleId] = moreModules[moduleId]; }}// Call hotApply to replace the module
    hotUpdateDownloaded();
}
Copy the code

HotApply hot update module replacement

The core logic of hot updates is in the hotApply method

1. Delete expired modules, that is, modules that need to be replaced: You can use hotUpdate to find the old modules

var queue = outdatedModules.slice();
while (queue.length > 0) {
    moduleId = queue.pop();
    // Remove expired modules from the cache
    module = installedModules[moduleId];
    // Delete expired dependencies
    delete outdatedDependencies[moduleId];
    
    // Store the deleted module ID to update the code
    outdatedSelfAcceptedModules.push({
        module: moduleId
    });
}
Copy the code

2. Add the new module to modules

appliedUpdate[moduleId] = hotUpdate[moduleId];
for (moduleId in appliedUpdate) {
    if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) { modules[moduleId] = appliedUpdate[moduleId]; }}Copy the code

3. Execute the related module code with __webpack_require__

for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
    var item = outdatedSelfAcceptedModules[i];
    moduleId = item.module;
    try {
        // Execute the latest code
        __webpack_require__(moduleId);
    } catch (err) {
        / /... Fault-tolerant processing}}Copy the code

conclusion