First, concept introduction

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.

Significantly speed up development in the following ways:

  • Preserve application state that is lost during a full page reload.
  • Update only the changes to save valuable development time.
  • When CSS/JS changes are made in the source code, they are immediately updated in the browser, which is almost equivalent to changing styles directly in the browser DevTools.

Note: HMR can only be used in development environments.

This article sample code running environment: Node :12.20.1, webpack:5.37.1, webpack-CLI :4.7.0, webpack-dev-server:3.11.2.

Two, use mode

  1. Update webpack.config.js configuration.
+ devServer: {
  + port: 9000,
  + host: '127.0.0.1',
  + hot: true, +},plugins: [
  / / defines devServer. Hot: true, and you don't write can omit the detailed reasons can see four, source, this paper analyses the 1.1 - > webpack - dev - server/lib/utils/addEntries js code
  // new webpack.HotModuleReplacementPlugin()
] 
Copy the code
  1. Package. json adds scripts.

Webpack-cli provides three commands to start webpack-dev-server: webpack serve, webpack s, and webpack Server. Webpack-cli /lib/webpack-cli.js code

"scripts": {+"dev": "webpack serve",}Copy the code
  1. Add module.hot? To index.js Accept method.
function render() {
  root.innerHTML = require('./print.js')
}
render()

+ module.hot? .accept(['./print.js'], render)
Copy the code
  1. Modify the print. Js.
 function printMe() {-console.log('Updating print.js');
  + console.log('Updating print.js 1');
 }

 module.exports = printMe

Copy the code

Third, source debugging

  1. Add scripts to package.json.
"scripts": {+"debug": "node ./node_modules/.bin/webpack serve"
}
Copy the code
  1. Open the debug panel in vscode, create a new launch.json configuration, and start debug.
{
  "version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "debug"."runtimeExecutable": "npm"."runtimeArgs": [
        "run-script"."debug"]."skipFiles": [
        "<node_internals>/**"].// CWD is set to the root of the project to be debugged
      "cwd": "${workspaceFolder}/webpack/hmr/test"}}]Copy the code

4. Source code analysis

The following is a description of what each module (file) does after executing the Webpack serve command.

Webpack serve ->./node_modules/./bin/webpack runCli (line 48)

const runCli = cli= > {
  const pkg = require('webpack-cli/package.json');
	require('webpack-cli/bin/cli.js');
};
Copy the code

-> webpack-cli/bin/cli.js (line 25)

  runCLI(process.argv);
Copy the code

-> webpack-cli/lib/bootstrap.js (line 4)

  const cli = new WebpackCLI();
  await cli.run(args);
Copy the code

-> webpack-cli/lib/webpack-cli.js (line 783)

  const externalBuiltInCommandsInfo = [
    {
      name: 'serve [entries...] '.alias: ['server'.'s'].pkg: '@webpack-cli/serve',}]const loadCommandByName = async (commandName) => {
    require(pkg);
  }
  this.webpack = require('webpack');
  // line 1834
  createCompiler(options, callback) {
    let compiler = this.webpack(options}
    return compiler;
  }
Copy the code

-> @webpack-cli/serve/index.js (line 6)

  const startDevServer_1 = __importDefault(require("./startDevServer"));
  // line 81
  const compiler = await cli.createCompiler();
  await startDevServer_1.default(compiler);
Copy the code

-> @webpack-cli/serve/startDevServer.js (line 92)

  Server = require('webpack-dev-server/lib/Server');
  const server = new Server(compiler, options);
  server.listen(options.port, options.host, (error) = > {
      if (error) {
          throwerror; }});Copy the code

-> webpack-dev-server/lib/Server.js (line 92)

  // Modify entry and plugins line 72
  updateCompiler(this.compiler, this.options);
  // Listen for Compiler.hooks. Done line 182
  const { done } = compiler.hooks;
  done.tap('webpack-dev-server'.(stats) = > {
    this._sendStats(this.sockets, this.getStats(stats));
    this._stats = stats;
  });
  // Instantiate Express Line 168
  this.app = new express();
  // Call webpack-dev-middleware line 207
  webpackDevMiddleware(
    this.compiler,
    Object.assign({}, this.options, { logLevel: this.log.options.level })
  );
  // Create HTTP server line 688
  this.listeningApp = http.createServer(this.app);
  // Set HTTP Listen line 774
  listen(port, hostname, fn) {
    this.hostname = hostname;
    return this.listeningApp.listen(port, hostname, (err) = > {
      // Create socket server
      this.createSocketServer();
    });
  }
  // Instantiate socket Server line 696
  createSocketServer() {
    const SocketServerImplementation = this.socketServerImplementation;
    this.socketServer = new SocketServerImplementation(this);
    this.socketServer.onConnection((connection, headers) = > {
      if(! connection) {return;
      }
      this.sockets.push(connection);
      if (this.hot) {
        // Tell the client that the hot update service is started
        this.sockWrite([connection], 'hot'); }}); }Copy the code

1 -> webpack-dev-server/lib/utils/updateCompliler.js (line 48)

  addEntries(webpackConfig, options);
  compilers.forEach((compiler) = > {
    const config = compiler.options;
    // Validate the entry modification
    compiler.hooks.entryOption.call(config.context, config.entry);
    // Make plugins changes take effect
    providePlugin.apply(compiler);
  });
Copy the code

1.1 – > webpack – dev – server/lib/utils/addEntries js (line 142)

  // config.entry Adds webpack-dev-server/client/index.js line 39
  const clientEntry = `The ${require.resolve(
      '.. /.. /client/'
    )}?${domain}${sockHost}${sockPath}${sockPort}`;
  // config.entry adds webpack/hot/dev-server.js line 49
  hotEntry = require.resolve('webpack/hot/dev-server');
  config.entry = prependEntry(config.entry || './src', additionalEntries);
  / / devServer. Hot: true automatically add HotModuleReplacementPlugin plug-in line 153
  config.plugins.push(new webpack.HotModuleReplacementPlugin());
Copy the code

1.1.1 -> webpack-dev-server/client/index.js (line 176)

The code will be packaged into app.bundle.js to instantiate the WebSocket on the client side, which will be used to establish a connection with the WS service on the server side to receive the latest changes from each compilation of webPack.

  var onSocketMessage = {
    The HMR mode is enabled on line 46
    hot: function hot() {
      options.hot = true;
      log.info('[WDS] Hot Module Replacement enabled.');
    },
    // Receives the latest Hash line 63 each time webPack compiles
    hash: function hash(_hash) {
      status.currentHash = _hash;
    },
    // Line 107 has been compiled
    ok: function ok() {
      // reloadApp is not used for the first time
      if (options.initial) {
        return options.initial = false; } reloadApp(options, status); }},// The client establishes the webSocket connection. There are three types of sockjs, webSocket, and PATH: line 176
  socket(socketUrl, onSocketMessage);
Copy the code

1.1.2 – > webpack – dev – server/client/utils/reloadApp js (line 23)

  // Trigger the webpackHotUpdate event line 23
  var hotEmitter = require('webpack/hot/emitter');
  hotEmitter.emit('webpackHotUpdate', currentHash);
Copy the code

1.2.1 -> webpack/hot/dev-server.js (line 23)

The code is packaged into app.bundle.js, and sets up webpackHotUpdate event listeners, and performs hot update check when the webpackHotUpdate event is triggered.

  // Trigger the webpackHotUpdate event line 23
  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(); }});// trigger module.hot.check() line 12
  var check = function check() {
		module.hot
			.check(true)
			.then(function (updatedModules) {
				if(! updatedModules) {window.location.reload();
					return; }}); };Copy the code

2 -> webpack-dev-server/lib/utils/getSocketServerImplementation.js (line 3)

  // Provides three types of socket services: SOckJS, WS and PATH
  if (options.transportMode.server === 'sockjs') {
    ServerImplementation = require('.. /servers/SockJSServer');
  } else if (options.transportMode.server === 'ws') {
    ServerImplementation = require('.. /servers/WebsocketServer');
  } else {
    try {
      // eslint-disable-next-line import/no-dynamic-require
      ServerImplementation = require(options.transportMode.server);
    } catch (e) {
      serverImplFound = false; }}Copy the code

2.1 – > webpack – dev – server/lib/servers/WebsocketServer js (line 12)

  // Instantiate the WS service
  this.wsServer = new ws.Server({
    noServer: true.path: this.server.sockPath,
  });
  // After the HTTP server is accessed by the client, the WS Server triggers the connection event line 17
  this.server.listeningApp.on('upgrade'.(req, sock, head) = > {
    
    this.wsServer.handleUpgrade(req, sock, head, (connection) = > {
      this.wsServer.emit('connection', connection, req);
    });
  });
Copy the code

3 -> webpack-dev-middleware/index.js (line 67)

  // Enable watching mode compile line 41
  context.watching = compiler.watch(options.watchOptions, (err) = > {
    if (err) {
      context.log.error(err.stack || err);
      if(err.details) { context.log.error(err.details); }}});// Set file system line 65
  setFs(context, compiler);
  // Provide static resource service line 67
  middleware(context)
Copy the code

3.1 -> webpack-dev /middleware.js (line 96)

  // server content line 96
  let content = context.fs.readFileSync(filename);
  res.setHeader('Content-Type', contentType);
  res.statusCode = res.statusCode || 200;
  if (res.send) {
    res.send(content);
  } else {
    res.end(content);
  }
Copy the code

3.2 -> Compiler. watch generates dist/app.bundle.js for the first time

  // Intercept the __webpack_require__ request, override the require request, On the module of new hot, parents, children attribute webpack/lib/HMR/HotModuleReplacement runtime. Js line 39
  __webpack_require__.i.push(function (options) {
    var module = options.module;
    var require = createRequire(options.require, options.id);
    module.hot = createModuleHotObject(options.id, module);
    module.parents = currentParents;
    module.children = [];
    currentParents = [];
    options.require = require;
  });
  / / collect the dependencies between modules, in the module of parents and children attribute webpack/lib/HMR/HotModuleReplacement runtime. Js line 52
  function createRequire(require, moduleId) {
    var me = installedModules[moduleId];
    if(! me)return require;
    var fn = function (request) {
      if (me.hot.active) {
        if (installedModules[request]) {
          var parents = installedModules[request].parents;
          if (parents.indexOf(moduleId) === -1) { parents.push(moduleId); }}else {
          currentParents = [moduleId];
          currentChildModule = request;
        }
        if (me.children.indexOf(request) === -1) { me.children.push(request); }}else {
        currentParents = [];
      }
      return require(request);
    };
    return fn;
  }
  / / create the module. The hot object webpack/lib/HMR/HotModuleReplacement runtime. Js line 103
  function createModuleHotObject(moduleId, me) {
    var hot = {
      _acceptedDependencies: {}, // Collect module.hot.accept dependencies
      active: true.accept: function (dep, callback, errorHandler) {
        if (dep === undefined) hot._selfAccepted = true;
        else if (typeof dep === "function") hot._selfAccepted = dep;
        else if (typeof dep === "object"&& dep ! = =null) {
          for (var i = 0; i < dep.length; i++) {
            hot._acceptedDependencies[dep[i]] = callback || function () {}; }}else {
          hot._acceptedDependencies[dep] = callback || function () {}; }},check: hotCheck,
      apply: hotApply,
    };
    return hot;
  }
  / / open hot update check webpack/lib/HMR/HotModuleReplacement runtime. Js line 242
  function hotCheck(applyOnUpdate) {
    // __webpack_require__.hmrM = hmrDownloadManifest, fetch request app.[hash].hot-update.json file
    return __webpack_require__.hmrM().then(function (update) {
      var updatedModules = [];
      currentUpdateApplyHandlers = [];
      return Promise.all(
        Object.keys(__webpack_require__.hmrC).reduce(function (promises, key) {
          / / __webpack_require__. HmrC = hmrDownloadUpdateHandlers, create scripts, use the json way to load the app. [hash]. Hot - update. Js file
          __webpack_require__.hmrC[key](
            update.c,
            update.r,
            update.m,
            promises,
            ApplyHandlers / / collection
            currentUpdateApplyHandlers,
            updatedModules
          );
          return promises;
        },
          [])
      ).then(function () {
        return waitForBlockingPromises(function () {
          if (applyOnUpdate) {
            / / triggers applyHandlers
            returninternalApply(applyOnUpdate); }}); }); }); }// Fetch requests app.[hash].hot-update.json file
  __webpack_require__.hmrM = () = > {
    if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");
    return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) = > {
      if (response.status === 404) return;
      if(! response.ok)throw new Error("Failed to fetch update manifest " + response.statusText);
      return response.json();
    });
  };
  __webpack_require__.hmrC.jsonp = function (chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList) {
    // Collect processing methods after loading app.[hash].hot-update.js file
    applyHandlers.push(applyHandler);
    chunkIds.forEach(function (chunkId) {
      if( __webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId] ! = =undefined
      ) {
        // Create a JSONP taskpromises.push(loadUpdateChunk(chunkId, updatedModulesList)); }}); };/ / trigger module. Hot. _acceptedDependencies webpack/lib/HMR/JavascriptHotModuleReplacement runtime. Js line 24
  function applyHandler(options) {
    var moduleOutdatedDependencies;
    return {
      apply: function (reportError) {
        for (var outdatedModuleId in outdatedDependencies) {
          if (__webpack_require__.o(outdatedDependencies, outdatedModuleId)) {
            var module = __webpack_require__.c[outdatedModuleId];
            if (module) {
              moduleOutdatedDependencies =
                outdatedDependencies[outdatedModuleId];
              for (var j = 0; j < moduleOutdatedDependencies.length; j++) {
                var dependency = moduleOutdatedDependencies[j];
                var acceptCallback =
                  // Clear the methods in module.hot._acceptedDependencies
                  module.hot._acceptedDependencies[dependency]; }}}}returnoutdatedModules; }}; }// Concatenate the jsonp request path app.[hash].hot-update.js
  function loadUpdateChunk(chunkId) {
    return new Promise((resolve, reject) = > {
      var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
      __webpack_require__.l(url, loadingEnded);
    });
  }
  // Create a script tag to load the js file
  __webpack_require__.l = (url, done, key, chunkId) = > {
    var script, needAttach;
    if(! script) { needAttach =true;
      script = document.createElement('script');
      script.charset = 'utf-8';
      script.timeout = 120;
      script.src = url;
    }
    needAttach && document.head.appendChild(script);
  };
  / / define the self "webpackHotUpdate" method, to collect with currentUpdate need hot update module module webpack/lib/web/JsonpChunkLoadingRuntimeModule js line 303
  self["webpackHotUpdate"] = (chunkId, moreModules, runtime) = > {
    for (var moduleId in moreModules) {
      if (__webpack_require__.o(moreModules, moduleId)) {
        currentUpdate[moduleId] = moreModules[moduleId];
        if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId); }}};/ / use need hot update module webpack/lib/HMR/HotModuleReplacement runtime. Js line 298
  function internalApply(options) {
    var results = currentUpdateApplyHandlers.map(function (handler) {
      return handler(options);
    });
    results.forEach(function (result) {
      if (result.apply) {
        / / call applyHandler. The apply ()
        varmodules = result.apply(reportError); }}); }Copy the code

3.3 – > compiler. Watch a recompile, by webpack. HotModuleReplacementPlugin generated dist/app. [hash]. Hot – update. Json

  // c is the chunkIds to be replaced
  {"c": ["app"]."r": []."m": []}Copy the code

3.4 – > compiler. Watch a recompile, by webpack. HotModuleReplacementPlugin generated dist/app. [hash]. Hot – update. Js

  Call self["webpackHotUpdate"](chunkId, moreUpdates)
  self["webpackHotUpdate"] ("app", {
    "./src/print.js":
      ((module) = > {
        function printMe() {
          console.log('Updating print.js.3');
        }
        module.exports = printMe
      })
  },
    function (__webpack_require__) {
      "use strict";
      (() = > {
        // Compile the latest hash for each time
        __webpack_require__.h = () = > ("4bbb5237e2809cf139b2")}) (); });Copy the code

Five, the working principle

HMR workflow analysis

Webpack listens to the project file or module of code is modified, by webpack. HotModuleReplacementPlugin plug-in generated hot – update. Json and hot – update. Js patch file, Then the client gets the patch file through socket connection, and finally HMR Runtime applies the patch file content to the module system.

HMR working principle diagram

The figure above shows a complete HMR workflow from the time we modify the code to the time the module is hot updated, with arrows marking the workflow.

  1. Webpack-dev-server provides HTTP server and Socket Server services, and enables Webpack’s Watch mode in webpack-dev-Middleware middleware.
  2. When a file in the file system changes, WebPack listens for the file change, recompiles and packages the module according to the configuration file, and saves the packaged code in the file system.
  3. Webpack-dev-server /client/index.js Sets up a websocket long connection between the browser and the server to inform the browser of the status information of webpack compilation and packaging.
  4. The browser side receives the latest build information via webpack/hot/dev-server.js, which triggers the module.hot.check method to start the hot update check.
  5. Implement webpack/lib/HMR/HotModuleReplacement runtime. Methods of hotCheck of js, request by hmrDownloadManifest to fetch hot – update. Json file, Then execute loadScript to load the hot-update.js file as jSONp.
  6. Execute self[‘webpackHotUpdate’] in hot-update.js, In webpack/lib/web/JsonpChunkLoadingRuntimeModule. Js file of self [‘ webpackHotUpdate] method in the definition to the need to update the collection module dependencies.
  7. Call internalApply, replace the old module with a new one, and then execute the callback function in Module.hot. __acceptedDependencies to implement the module replacement effect.

Six, source code implementation

The code address

Site address: http://127.0.0.1:9001/hmr.html

List of modules that have implemented corresponding functions:

  • @webpack-cli/serve/lib/index.js Start the service
  • Webpack-dev-server /lib/server.js sets HTTP and socket service listening
  • Webpack-dev-server /client/index.js provides websocket connections
  • Webpack/hot/dev – server. Js trigger module. Hot. Check
  • Webpack-dev-middleware /index.js enables compiler.watch() and provides static resource services
  • Static /hmr.js Collects module dependencies, downloads hot-update.json, hot-update, js files, and performs module hot replacement

Seven,

In this article, we mainly share the HMR source code analysis and working principle of Webpack. In the working principle analysis, through a “Working principle diagram of Webpack HMR” diagram, we can understand the whole working process of HMR. HMR itself has more source content, and many details are not written in this article completely. You need to slowly read and understand the source code.

Refer to the article

1. Official documents