Review of recent original articles 😊 :

  • 1.2W word | Beginner intermediate Front-end JavaScript Self-test List-1
  • The Great Webpack HMR Study Guide (with source Code Analysis)
  • The Great Webpack Build Process Study Guide
  • WeakMap you Don’t Know
  • The Blob You Don’t Know
  • The Great Tsconfig. json Guide
  • “200 lines of JS code, take you to implement code compiler”

Study chapters:Webpack HMR Principle Analysis

I. Introduction to HMR

Hot Module Replacement (HMR Module Hot Replacement) is a very useful feature provided by Webpack that allows various modules to be updated while JavaScript is running without a complete refresh.

Hot Module Replacement (or HMR) is one of the most useful features offered by webpack. It allows all kinds of modules to Be updated at Runtime without the need for a full refresh.

When we modify the code and save it, Webpack repackages the code, and HMR replaces, adds, or removes modules as the application runs without reloading the entire page. HMR significantly speeds up development in the following ways:

  • Preserve application state that is lost when the page is fully reloaded;
  • Only update changes to save valuable development time;
  • Styling is faster – almost as fast as changing styles in the browser debugger.

Note: HMR is not intended for production environments, which means it should only be used in development environments.

Ii. Use of HMR

Enabling HMR functionality in Webpack is simple:

1. Method 1: Use devServer

1.1 Setting devServer

Simply add the devServer option to webpack.config.js and set the hot value to true, And use HotModuleReplacementPlugin and NamedModulesPlugin two Plugins (optional) :

// webpack.config.js

const path = require('path')
const webpack = require('webpack')
module.exports = {
 entry: './index.js',  output: {  filename: 'bundle.js',  path: path.join(__dirname, '/')  }, + devServer: { + hot: true, // Start module hot update HMR + open: true, // enable automatic browser page opening +},  plugins: [ + new webpack.NamedModulesPlugin(), + new webpack.HotModuleReplacementPlugin()  ] } Copy the code

1.2 add scripts

Then run the command scripts in package.json:

// package.json

{
  // ...
  "scripts": {
+ "start": "webpack-dev-server"  },  // ... } Copy the code

2. Use cli parameters

The other is by adding the –hot parameter. After adding – hot parameters, devServer will tell Webpack introducing HotModuleReplacementPlugin automatically, without the need for manual we introduced. It is often paired with –open to automatically open the browser to the page. Here we remove the two Plugins we added earlier:

// webpack.config.js

const path = require('path')
const webpack = require('webpack')
module.exports = {
 // ... - plugins: [ - new webpack.NamedModulesPlugin(), - new webpack.HotModuleReplacementPlugin() -] } Copy the code

Then modify the scripts configuration in package.json:

// package.json

{
  // ...
  "scripts": {
- "start": "webpack-dev-server" + "start": "webpack-dev-server --hot --open"  },  // ... } Copy the code

3. Simple example

Based on the above configuration, we simply implement a scenario where the hello.js module is imported into the index.js file, and when the Hello.js module changes, the index.js module will be updated. The module code is as follows:

// hello.js
export default() = >'hi leo! ';

// index.js
import hello from './hello.js'
const div = document.createElement('div'); div.innerHTML = hello();  document.body.appendChild(div); Copy the code

Then import the packaged JS file in index.html and execute NPM start to run the project:


      
<html lang="en">
<head>
 <meta charset="UTF-8">
</head>
<body>  <div>Awesome Webpack HMR study guide</div>  <script src="bundle.js"></script> </body> </html> Copy the code

4. Implement monitoring updates

When we enabled HMR HotModuleReplacementPlugin plug-in, the interface will be exposed to the global module. The hot properties below. In general, you can check that the interface is accessible before you start using it. For example, you can accept an updated module like this:

if (module.hot) {
  module.hot.accept('./library.js'.function() {
    // Use the updated Library module to perform some operations...
  })
}
Copy the code

For more information about the Module. hot API, see the official document Hot Module Replacement API. Returning to the above example, we tested the functionality of the update module. At this point we modify the index.js code to listen for updates in the hello.js module:

import hello from './hello.js';
const div = document.createElement('div');
div.innerHTML = hello();
document.body.appendChild(div);

+ if (module.hot) { + module.hot.accept('./hello.js', function() { + console.log(' Now updating the Hello module ~'); + div.innerHTML = hello(); +}) +} Copy the code

Then modify the hello.js file to test the effect:

- export default () => 'hi leo! ';
+ export default () => 'hi leo! hello world';
Copy the code

When we save the code, the console prints"Update hello module now ~", and the page"hi leo!"Also updated to"hi leo! hello world"To prove that we are listening for file updates.





This is how to use simple Webpack HMR. For more information, please read the official documentationHot Module Replacement.

5. Common Configuration and techniques of devServer

5.1 Common Configuration

Depending on the directory structure, the contentBase and openPage parameters should be set to appropriate values, otherwise the runtime should not immediately access your home page. Also pay attention to your publicPath. The path generated after static resources are packaged is a point of consideration, depending on your directory structure.

devServer: {
  contentBase: path.join(__dirname, 'static'),    // Tell the server where to supply content from (default current working directory)
  openPage: 'views/index.html'.// Specify the page to open when the browser starts by default
  index: 'views/index.html'.// specify the home page location
  watchContentBase: true.// File changes under contentBase will reload the page (default false)
 host: 'localhost'.// Default localhost, '0.0.0.0' for external access  port: 8080./ / the default 8080  inline: true.// Can monitor js changes  hot: true./ / hot start  open: true.// Automatically open the browser on startup (specify open Chrome, open: 'Google Chrome')  compress: true.// All services are gzip compressed  disableHostCheck: true.// true: the host check is not performed  quiet: false. https: false. clientLogLevel: 'none'. stats: { // Set the console prompt  chunks: false. children: false. modules: false. entrypoints: false.// Whether to output the entry information  warnings: false. performance: false.// Whether to output webpack suggestions (such as file size)  },  historyApiFallback: {  disableDotRule: true. },  watchOptions: {  ignored: /node_modules/.// Skip the node_modules directory  },  proxy: { // Interface proxy (this configuration is more recommended: write to package.json and import here)  "/api-dev": {  "target": "http://api.test.xxx.com". "secure": false. "changeOrigin": true. "pathRewrite": { // Rewrite a section of the URL (for example, void api-dev)  "^/api-dev": ""  }  }  },  before(app) { }, } Copy the code

5.2 Tip 1: Output the dev-server code as a file

Dev-server outputs code that is usually in memory, but can also be written to disk to produce a physical file:

devServer:{
  writeToDisk: true.}
Copy the code

Usually used for proxy mapping file debugging, many JS files will be compiled with hash, and files without hash will also be compiled in real time.

5.3 Tip 2: Start the service using a local IP address by default

Sometimes you want to start a service using a local IP address by default:

devServer:{
  disableHostCheck: true.// true: the host check is not performed
  // useLocalIp: true, // Not recommended
  // host: '0.0.0.0', // this configuration is not recommended
}
Copy the code

You also need to set host to 0.0.0.0. It is recommended to append this configuration to scripts rather than write it dead in the configuration. Otherwise, you don’t want to change this in the future.

"dev-ip": "yarn run dev --host 0.0.0.0 --useLocalIp"
Copy the code

5.4 Tip 3: Specify a debug domain name

Sometimes the debugging domain name is specified, for example, local.test.baidu.com.

devServer:{
  open: true.  public: 'local.test.baidu.com:8080'.// A port is required
  port: 8080.}
Copy the code

In addition, you need to change 127.0.0.1 to the specified host. You can use tools such as iHost to change 127.0.0.1. Each tool is similar to the following format:

127.0. 01. local.test.baidu.com
Copy the code

Service starts automatically open local.test.baidu.com: 8080

5.5 Tip 4: Enable Gzip Compression

devServer:{
  compress: true.}
Copy the code

Iii. Introduction to the basic principle of HMR

From the previous introduction, we know that the main function of HMR is to replace, add, or remove modules while the application is running without having to reload the entire page. So, how do Webpack’s compiled source file changes relate to the replacement module implementation at run time? With these two questions in mind, let’s take a brief look at the HMR core workflow (simplified version) :

PNG workflow flowchart of HMR

Next, start the HMR workflow analysis:

  1. When Webpack (Watchman) detects changes in file/module code in the project, it notifies the Packager (HMR Plugin) in Webpack of the changes.
  2. It is then processed by the HMR Plugin and sent to the Application’s Runtime framework (HMR Runtime).
  3. Finally, HMR Runtime will update (add/delete/replace) these changed files/modules into the module system.

The HMR Runtime is injected by the build tool at compile time. It maps the compile-time file to the Runtime Module with a unified Module ID, and provides a series of apis externally for application layer frameworks (such as React) to call.

💖 Note 💖 : It is recommended to understand the general process of the above picture first and then read it later. Rest assured, I wait for you ~😃

Four, HMR complete principle and source code analysis

From the previous section, we have a general idea of the HMR simple workflow, so you may now be wondering: what notifying the HMR Plugin about file updates? How does HMR Plugin send updates to HMR Runtime? And so on.

Then we start to analyze the whole HMR module hot update process in detail with the source code. First of all, we still look at the flow chart, and we can not understand the method name in the figure (red font yellow background color part) :

Webpack HMR.png

The figure above shows a complete HMR workflow from the time we modify the code to the time the module is hot updated. The workflow has been identified with red Arabic numerals.

To understand how this works, let’s first understand these concepts:

  • Webpack-dev-server: a server plug-in, equivalent to an Express server, that launches a Web service, suitable only for development environments;
  • Webpack-dev-middleware: a webpack-dev-server middleware that monitors changes in resources through watch mode and automatically packages them.
  • 3. Webpack-hot-middleware: Middleware used in combination with webpack-dev-middleware to enable non-refresh browser updates, also known as HMR;

Let’s learn how HMR works:

1. Monitor code changes and recompile packages

Using NPM start will start webpack-dev-server to start the local server and enter Webpack’s Watch mode according to the devServer configuration, and then initialize Webpack-dev-Middleware. Watch the file system in Webpack-dev-Middleware by calling the startWatch() method:

// webpack-dev-server\bin\webpack-dev-server.js
1. Start local server Line 386
server = new Server(compiler, options);

// webpack-dev-server\lib\Server.js
Initialize webpack-dev-middleware Line 109 this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));  // webpack-dev-middleware\lib\Shared.js // 3. Start watch file system Line 171 startWatch: function() {  / /...  // start watching  if(! options.lazy) { var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);  context.watching = watching;  }  / /... } share.startWatch(); // ... Copy the code

When the startWatch() method is executed, it enters Watch mode, and if any code changes are found in the file, the module is recompiled and packaged according to the configuration file.

2. Save the compilation result

Webpack interacts with Webpack-dev-middleware, which calls Webpack’s API to monitor code changes, And tells Webpack to store the recompiled code in memory via JavaScript objects.

We can see that the dist directory specified in output.path does not store the compiled file. Why is this?

In fact, Webpack keeps the compiled results in memory, because accessing code in memory is faster than accessing files in the file system, which reduces the overhead of writing code to files.

Webpack saves code to memory thanks to webpack-dev-Middleware’s memory-FS dependent library, which replaces an outputFileSystem with a MemoryFileSystem instance, Implements code output to memory. Part of the source code is as follows:

// webpack-dev-middleware\lib\Shared.js Line 108

// store our files in memory
var fs;
varisMemoryFs = ! compiler.compilers && compiler.outputFileSystem instanceof MemoryFileSystem; if(isMemoryFs) {  fs = compiler.outputFileSystem; } else {  fs = compiler.outputFileSystem = new MemoryFileSystem(); } context.fs = fs; Copy the code

The code checks whether fileSystem is an instance of a MemoryFileSystem, and if not, replaces the outputFileSystem with the MemoryFileSystem instance. 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.

3. Monitor file changes and refresh the browser

Webpack-dev-server starts to monitor file changes, and unlike step 1, it does not monitor code changes to recompile and pack. . When we are in the configuration file is configured with devServer watchContentBase to true, Webpack dev – server can monitor configuration static files in the folder change, change, notify the browser browser refresh for the application, it is different from HMR.

// webpack-dev-server\lib\Server.js
// 1. 读取参数 Line 385
if (options.watchContentBase) { defaultFeatures.push('watchContentBase'); }

// 2. Define _watch method Line 697
Server.prototype._watch = function (watchPath) {  // ...  const watcher = chokidar.watch(watchPath, options).on('change', () = > { this.sockWrite(this.sockets, 'content-changed');  });   this.contentBaseWatchers.push(watcher); };  // 3. Run _watch() to listen for file changes on Line 339 watchContentBase: (a)= > {  if (/^(https? :)? \ \ / / /.test(contentBase) || typeof contentBase === 'number') {  throw new Error('Watching remote files is not supported.');  } else if (Array.isArray(contentBase)) {  contentBase.forEach((item) = > {  this._watch(item);  });  } else {  this._watch(contentBase);  } } Copy the code

4. Establish WS and synchronize the compile status

This step is handled in webpack-dev-server, mainly through sockjs (webpack-dev-server dependency), Establish a WebSocket long connection between webpack-dev-server’s Client and webpack-dev-middleware.

The state information for each phase of the Webpack compilation package is then synchronized to the browser side. There are two important steps:

  • Delivery status

Webpack-dev-server listens for the done event of compile through Webpack API. When compile is complete, Webpack-dev-server uses the _sendStats method to send the hash value of the compiled new module to the browser using the socket.

  • Save the state

The browser side will_sendStatsSent overhashSave it,It will use the post-module hot update.

// webpack-dev-server\lib\Server.js

// 1. Define the _sendStats method Line 685
// send stats to a socket or multiple sockets
Server.prototype._sendStats = function (sockets, stats, force) {
 / /...  this.sockWrite(sockets, 'hash', stats.hash); };  // 2. Listen to the done event Line 86 compiler.plugin('done', (stats) => {  // Pass _sendStats() as the hash value of the latest packaged file (stats.hash)  this._sendStats(this.sockets, stats.toJson(clientStats));  this._stats = stats; });  // webpack-dev-server\client\index.js Save the hash value Line 74 var onSocketMsg = {  // ...  hash: function hash(_hash) {  currentHash = _hash;  },  // ... } socket(socketUrl, onSocketMsg); Copy the code

5. The browser advertises messages

After the hash message is sent, the socket also sends an OK message to webpack-dev-server. Since the Client does not request the hot update code or perform the hot update module operation, So send the work back to Webpack by emitting a “webpackHotUpdate” message.

// webpack-dev-server\client\index.js
// 1. Process ok message Line 135
var onSocketMsg = {
  // ...
  ok: function ok() {
 sendMsg('Ok');  if (useWarningOverlay || useErrorOverlay) overlay.clear();  if (initial) return initial = false; // eslint-disable-line no-return-assign  reloadApp();  },  // ... }  // 2. Refresh APP Line 218 function reloadApp() {  // ...  if (_hot) {  // Load emitter dynamically  var hotEmitter = require('webpack/hot/emitter');  hotEmitter.emit('webpackHotUpdate', currentHash);  if (typeofself ! = ='undefined' && self.window) {  // broadcast update to window  self.postMessage('webpackHotUpdate' + currentHash, The '*');  }  }  // ... } Copy the code

6. Pass hash to HMR

Webpack/hot/dev – server to monitor the browser webpackHotUpdate news, the new modules hash value to the client HMR core central HotModuleReplacement. Runtime. And call the check method to detect the update, determine whether the browser refresh or module hot update. If the browser is refreshed, there is no further step

// webpack\hot\dev-server.js
// 1. Listen to webpackHotUpdate Line 42
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate".function(currentHash) {
    lastHash = currentHash;
 if(! upToDate() &&module.hot.status() === "idle") {  log("info"."[HMR] Checking for updates on the server...");  check();  } });  var check = function check() {  module.hot.check(true).then(function(updatedModules) {  if(! updatedModules) { // ...  window.location.reload();// Refresh the browser  return;  }  if(! upToDate()) { check();  }  }).catch(function(err) { / *... * /}); };  // webpack\lib\HotModuleReplacement.runtime.js / / 3. Call HotModuleReplacement. Runtime defines the check method of Line 167 function hotCheck(apply) {  if(hotStatus ! = ="idle") throw new Error("check() is only allowed in idle status");  hotApplyOnUpdate = apply;  hotSetStatus("check");  return hotDownloadManifest(hotRequestTimeout).then(function(update) {  / /...  }); } Copy the code

7. Check whether updates exist

When HotModuleReplacement. Runtime calls check method, Invokes the JsonpMainTemplate. The runtime hotDownloadUpdateChunk (for the latest module code) and hotDownloadManifest if there is a update file (for) two methods, the source code of these two methods, in the next step.

// webpack\lib\HotModuleReplacement.runtime.js
/ / 1. Call HotModuleReplacement. Runtime definition hotDownloadUpdateChunk method Line 171
function hotCheck(apply) {
    if(hotStatus ! = ="idle") throw new Error("check() is only allowed in idle status");
    hotApplyOnUpdate = apply;
 hotSetStatus("check");  return hotDownloadManifest(hotRequestTimeout).then(function(update) {  / /...  {  HotDownloadUpdateChunk is called in the hotEnsureUpdateChunk method  hotEnsureUpdateChunk(chunkId);  }  }); } Copy the code

The hotEnsureUpdateChunk method calls hotDownloadUpdateChunk:

// webpack\lib\HotModuleReplacement.runtime.js Line 215
 function hotEnsureUpdateChunk(chunkId) {
  if(! hotAvailableFilesMap[chunkId]) {   hotWaitingFilesMap[chunkId] = true;
  } else {
 hotRequestedFilesMap[chunkId] = true;  hotWaitingFiles++;  hotDownloadUpdateChunk(chunkId);  }  } Copy the code

8. Request to update the latest file list

In the callcheckMethod, will first call JsonpMainTemplate. In the runtimehotDownloadManifestMethod to the serverMake an AJAX request to see if there is an update file, if anymainfestReturn to the browser side.There are some primitives involved hereXMLHttpRequest, I will not post all the ~

// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest defines Line 22
function hotDownloadManifest(requestTimeout) {
    return new Promise(function(resolve, reject) {
        try {
 var request = new XMLHttpRequest();  var requestPath = $require$.p + $hotMainFilename$;  request.open("GET", requestPath, true);  request.timeout = requestTimeout;  request.send(null);  } catch(err) {  return reject(err);  }  request.onreadystatechange = function() {  // ...  };  }); } Copy the code

9. Request to update the latest module code

inhotDownloadManifestMethod is also executedhotDownloadUpdateChunkMethod,Request the latest module code through JSONPAnd returns the code to the HMR Runtime.

The HMR Runtime then processes the new code further to determine whether it is a browser refresh or a module hot update.

// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest defines Line 12
function hotDownloadUpdateChunk(chunkId) {
  // Create a script tag to initiate a JSONP request
    var head = document.getElementsByTagName("head") [0];
 var script = document.createElement("script");  script.type = "text/javascript";  script.charset = "utf-8";  script.src = $require$.p + $hotChunkFilename$;  $crossOriginLoading$;  head.appendChild(script); } Copy the code

Update module and dependency references

This step is the core of the entire module hot update (HMR). The HMR Runtime hotApply method removes expired modules and code and adds new modules and code for hot update.

According to the hotApply method, module hot replacement can be divided into three stages:

  1. Find expired modulesoutdatedModulesAnd expired dependenciesoutdatedDependencies
// webpack\lib\HotModuleReplacement.runtime.js
// Find outdatedModules and outdatedDependencies Line 342
function hotApply() { 
  // ...
  var outdatedDependencies = {};
 var outdatedModules = [];  function getAffectedStuff(updateModuleId) {  var outdatedModules = [updateModuleId];  var outdatedDependencies = {};  // ...  return {  type: "accepted". moduleId: updateModuleId,  outdatedModules: outdatedModules,  outdatedDependencies: outdatedDependencies  };  };  function addAllToSet(a, b) {  for (var i = 0; i < b.length; i++) {  var item = b[i];  if (a.indexOf(item) < 0)  a.push(item);  }  }  for(var id in hotUpdate) {  if(Object.prototype.hasOwnProperty.call(hotUpdate, id)) {  / /... Omit redundant code  if(hotUpdate[id]) {  result = getAffectedStuff(moduleId);  }  if(doApply) {  for(moduleId in result.outdatedDependencies) {  // Add to outdatedDependencies  addAllToSet(outdatedDependencies[moduleId], result.outdatedDependencies[moduleId]);  }  }  if(doDispose) {  // Add to outdatedModules  addAllToSet(outdatedModules, [result.moduleId]);  appliedUpdate[moduleId] = warnUnexpectedRequire;  }  }  } } Copy the code
  1. Remove references to expired modules, dependencies, and all child elements from the cache;
// webpack\lib\HotModuleReplacement.runtime.js
// Remove references to Line 442 for expired modules, dependencies, and all child elements from the cache
function hotApply() {
   // ...
    var idx;
 var queue = outdatedModules.slice();  while(queue.length > 0) {  moduleId = queue.pop();  module = installedModules[moduleId];  // ...  // Remove modules from the cache  delete installedModules[moduleId];  // Remove unwanted handlers from expired dependencies  delete outdatedDependencies[moduleId];  // Remove references to all child elements  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);  }  }  }  // Remove obsolete dependencies from module subcomponents  var dependency;  var moduleOutdatedDependencies;  for(moduleId in outdatedDependencies) {  if(Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {  module = installedModules[moduleId];  if(module) {  moduleOutdatedDependencies = outdatedDependencies[moduleId];  for(j = 0; j < moduleOutdatedDependencies.length; j++) {  dependency = moduleOutdatedDependencies[j];  idx = module.children.indexOf(dependency);  if(idx >= 0) module.children.splice(idx, 1);  }  }  }  } } Copy the code
  1. Add the new module code to modules, next call__webpack_require__(webpack rewriterequireMethod, is to get the new module code.
// webpack\lib\HotModuleReplacement.runtime.js
// Add the new module code to modules Line 501
function hotApply() {
   // ...
    for(moduleId in appliedUpdate) {
 if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {  modules[moduleId] = appliedUpdate[moduleId];  }  } } Copy the code

After the hotApply method is executed, the new code has replaced the old code, but our business code is not aware of these changes, so we need to notify the application layer to use the new module for a “partial refresh” through the Accept event. We use this in our business:


if (module.hot) {
  module.hot.accept('./library.js'.function() {
    // Use the updated Library module to perform some operations...
  })
} Copy the code

11. Hot update error handling

In the hot update process, abort or FAIL errors may occur in the hotApply process, and the hot update is returned to the Browser Reload, and the hot update of the whole module is completed.

// webpack\hot\dev-server.js Line 13
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

Five, the summary

In this paper, we mainly share Webpack HMR use and implementation principle and source code analysis, in the source code analysis, through a “Webpack HMR working principle analysis” figure to let you understand the whole WORKFLOW of HMR, HMR itself source content is more, many details of this article is not complete write, You need to slowly read and understand the source code.

Refer to the article

1. Official document “Hot Module Replacement” 2. “Principle Analysis of Webpack HMR” 3.