Webpack-based live Reload and Hot update HMR – How do I get the browser to update the code when the file is modified

Whether react or Vue, the official scaffolding is provided for developers to get started quickly. When we modify a JS or CSS file during development, WebPack will automatically compile our file, and we can see the compiled file after refreshing the browser. For this reason, we think it would be nice if the file were compiled, the browser refreshed automatically, or the browser partially refreshed (not the whole browser) after we made the changes and saved. Of course, libraries based on the WebPack packaging tool are already implemented. The following is a simple analysis of this part of the process

  • Live reload: when a file is modified, the webpack compiles and the browser refreshes -> equivalent to the page window.location.reload()
  • Hot update HMR: Live Reload does not save the states of the application. When the page is refreshed, the state of the application is lost. For example, when you click a button on a page, the popup window appears. When the browser refreshes, the popup window disappears. To return to the previous state, you need to click the button again. However, WebAPCK hot update HMR does not refresh the browser, but performs hot replacement on the module at runtime, which ensures that the application state will not be lost and improves the development efficiency

Relevant version selection:

  1. Git Checkout v2.7.0
  2. Git Checkout is available in version 1.12.2
  3. Git Checkout v2.9.7

Note: The webpack version is V2, because the previous debug webpack packaging process is V2, so you may ask why webpack-dev-server version is V2. To explain, the version can be selected through the field peerDependencies in package.json, for which I selected the latest version, v2.9.7. The same goes for webpack-dev-Middleware versions, depending on dependencies. Attached is the package.json file description of the webpack-dev-server library

"name": "webpack-dev-server"."version": "2.9.7"."peerDependencies": {
    "webpack": "^ 2.2.0 | | ^ 3.0.0"  // Here is the required version number
 }
Copy the code

To enter the topic demo, see examples/ API /simple under the webpack-dev-server directory. Just paste the key code. Clone code is recommended for comparison

Server.js entry file

'use strict';

const Webpack = require('webpack');
const WebpackDevServer = require('.. /.. /.. /lib/Server');
const webpackConfig = require('./webpack.config');

const compiler = Webpack(webpackConfig);
const devServerOptions = Object.assign({}, webpackConfig.devServer, {
  stats: {
    colors: true}});const server = new WebpackDevServer(compiler, devServerOptions);

server.listen(8080.'127.0.0.1', () = > {console.log('Starting server on http://localhost:8080');
});
Copy the code

const webpackConfig = require(‘./webpack.config’); The following file

'use strict';
var path = require("path");
// our setup function adds behind-the-scenes bits to the config that all of our
// examples need
const { setup } = require('.. /.. /util');

module.exports = setup({
  context: __dirname,
  entry: [
   './app.js'.'.. /.. /.. /client/index.js? http://localhost:8080/'.'webpack/hot/dev-server'].devServer: {  // When the file is modified and saved during development, the update mode is hot update HMR
    hot: true}});Copy the code

Entry entry contains ‘.. /.. /.. /client/index.js? http://localhost:8080/’ and ‘webpack/hot/dev-server’ do: The former is the client browser code of WebpackDevServer, which is connected to the Server through sockjS-client for communication, such as saving the code after modification during development. WebpackDevServer gets webPack compiled results via WebPack-dev-Middleware and sends message types via WebSockets to the client browser. The webpack hot update HMR client browser code, packaging will insert into, is received when the browser web sockets, after sending a message. If webpackConfig configuration webpack HotModuleReplacementPlugin plugin. Will go hot update HMR mode

. /.. /.. The /client/index.js file is as follows

'use strict';

const socket = require('./socket');

let urlParts;
let hotReload = true;

// __resourceQuery is the same as.. /.. /.. The argument http://localhost:8080/ after /client/index.js is replaced when webpack is packaged
if (typeof __resourceQuery === 'string' && __resourceQuery) {
  // If this bundle is inlined, use the resource query to get the correct url.
  urlParts = url.parse(__resourceQuery.substr(1));
} else {
  // ...
}

let hot = false;
let currentHash = ' ';

const onSocketMsg = {
  hot: function msgHot() {
    hot = true;
  },
  hash: function msgHash(hash) {
    currentHash = hash;
  },
  ok: function msgOk() { reloadApp(); }};// Set up webSockets link
socket(socketUrl, onSocketMsg);

function reloadApp() {
  if(isUnloading || ! hotReload) {return;
  }
  // If devserver. hot is set to true in webpackConfig, the HMR mode will be hot updated
  if (hot) {
    const hotEmitter = require('webpack/hot/emitter');
    hotEmitter.emit('webpackHotUpdate', currentHash);
  } else { // If not, reload the browser with live reload
    applyReload(rootWindow, intervalId);
  }
  function applyReload(rootWindow, intervalId) {
    clearInterval(intervalId);
    log.info('[WDS] App updated. Reloading... '); rootWindow.location.reload(); }}Copy the code

const socket = require(‘./socket’); The following file

'use strict';

const SockJS = require('sockjs-client');
let sock = null;

function socket(url, handlers) {
  sock = new SockJS(url);
  
  sock.onclose = function onclose() {
    // here is the logical omission of the reconnection...
  };

  sock.onmessage = function onmessage(e) { // Execute message type logic when receiving webSockets message from Server
    // This assumes that all data sent via the websocket is JSON.
    const msg = JSON.parse(e.data);
    if(handlers[msg.type]) { handlers[msg.type](msg.data); }}; }module.exports = socket;

Copy the code

The ‘webpack/hot/dev-server’ file is as follows

// => module.hot is replaced with true: the code location is identified during the early AST syntax tree analysis, and then replaced during the Webpack Assets phase
// => module.hot is replaced with true: the code location is identified during the early AST syntax tree analysis, and then replaced during the Webpack Assets phase
if(module.hot) {
var lastHash;
var upToDate = function upToDate() {
  return lastHash.indexOf(__webpack_hash__) >= 0;
};
var check = function check() {
  module.hot.check(true).then(function(updatedModules) {
    if(! updatedModules) {console.warn("[HMR] Cannot find update. Need to do a full reload!");
      console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
      window.location.reload();
      return;
    }

    if(! upToDate()) { check(); }require("./log-apply-result")(updatedModules, updatedModules);

    if(upToDate()) {
      console.log("[HMR] App is up to date.");
    }

  }).catch(function(err) {
    var status = module.hot.status();
    if(["abort"."fail"].indexOf(status) >= 0) {
      console.warn("[HMR] Cannot apply update. Need to do a full reload!");
      console.warn("[HMR] " + err.stack || err.message);
      // window.location.reload();
    } else {
      console.warn("[HMR] Update failed: "+ err.stack || err.message); }}); };var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate".function(currentHash) {
  lastHash = currentHash;
  if(! upToDate() &&module.hot.status() === "idle") {
    console.log("[HMR] Checking for updates on the server..."); check(); }});console.log("[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}

Copy the code

Conclusion: This code is inserted into the client browser to start the WebPack hot UPDATE HMR, and when the hot update HMR mode fails, the browser is refreshed directly

const { setup } = require(‘.. /.. /util’); The following file

module.exports = {
  setup(config) {
    const defaults = { plugins: [].devServer: {}};const result = Object.assign(defaults, config);
    result.plugins.push(new webpack.HotModuleReplacementPlugin());
    result.plugins.push(new HtmlWebpackPlugin({
      filename: 'index.html'.template: path.join(__dirname, '.assets/layout.html'),
      title: exampleTitle
    }));
    returnresult; }};Copy the code

The role of webpack. HotModuleReplacementPlugin plug-in is: Add functional code to the code generated by the WebPack package. When we’re developing, after we modify a file and save it, the browser gets the modified module code, and then executes and updates the dependencies. Of course, how the browser gets the code and performs the updates is covered below

Webpack Entry entry file app.js

'use strict';

require('./example');

if (module.hot) {
  module.hot.accept((err) = > {
    if (err) {
      console.error('Cannot apply HMR update.', err); }}); }Copy the code

Webpack Entry entry file example.js

'use strict';

const target = document.querySelector('#target');

target.innerHTML = 'Modify to update this element without reloading the page.';

Copy the code

HTML Template Template file


      
<html>
  <head>
    <title>WDS ▻ API: Simple Server</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="shortcut icon" href="/.assets/favicon.ico"/>
    <link rel="stylesheet" href="Https://fonts.googleapis.com/css?family=Source+Code+Pro:400, 600 | Source Sans + + Pro: 400400 I, 500600"/>
    <link rel="stylesheet" href="/.assets/style.css"/>
  </head>
	<body>
    <main>
      <header>
        <h1>
          <img src="/.assets/icon-square.svg" style="width: 35px; height: 35px;"/>
          webpack-dev-server
        </h1>
      </header>
      <section>
        <h2>API: Simple Server</h2>
        <div id="target"></div>
      </section>
       <section>
        <div id="targetmodule"></div>
      </section>
    </main>
	<script type="text/javascript" src="main.js"></script></body>
</html>

Copy the code

These are some of the documents involved…

To see what happens, run the node –inspect-brk server.js file and visit http://localhost:8080

This is webSockets Server code from Webpack-dev-server. Webpack-dev-middleware registers a Webapck packaging lifecycle event callback to synchronize key packaging lifecycle points to the client browser (right). According to the console, the message types are hot, hash, and OK. Where the hot type is to tell the client browser to update the code by hot-updating HMR instead of directly refreshing the browser by hot-reloading live reload. Hash is the hash value after the webpack. Ok indicates that the webpack life cycle has been completed. You are ready to update the client browser code, which is the process of webPack hot updating the HMR.

Now when you modify the example.js file that’s how the browser updates the code flow the moment of truth comes, right

//target.innerHTML = 'Modify to update this element without reloading the page.';
target.innerHTML = 'Hot update HMR mode';

Copy the code

File changes, webpack HotModuleReplacementPlugin plug-in key webpack Compilation object event callback functions are as follows

compilation.plugin("record".function(compilation, records) {
  // The generated records module is used to find the changed words when the file changes
  debugger
  if(records.hash === this.hash) return;
  records.hash = compilation.hash;
  records.moduleHashs = {};
  // Loop through each module, a file in webpack is a module, and hash to see if the file has changed
  this.modules.forEach(function(module) {
    var identifier = module.identifier();
    var hash = require("crypto").createHash("md5");
    module.updateHash(hash);
    records.moduleHashs[identifier] = hash.digest("hex");
  });
  records.chunkHashs = {};
  // This Webpack compilation object
  this.chunks.forEach(function(chunk) {
    records.chunkHashs[chunk.id] = chunk.hash;
  });
  records.chunkModuleIds = {};
  this.chunks.forEach(function(chunk) {
    records.chunkModuleIds[chunk.id] = chunk.modules.map(function(m) {
      return m.id;
    });
  });
});
var initialPass = false;
var recompilation = false;
compilation.plugin("after-hash".function() {
  // Records The corresponding hash determines the identity after the module changes
  debugger
  var records = this.records;
  if(! records) { initialPass =true;
    return;
  }
  if(! records.hash) initialPass =true;
  var preHash = records.preHash || "x";
  var prepreHash = records.prepreHash || "x";
  if(preHash === this.hash) {
    recompilation = true;
    this.modifyHash(prepreHash);
    return;
  }
  records.prepreHash = records.hash || "x";
  records.preHash = this.hash;
  // The hash value of the Complain object
  this.modifyHash(records.prepreHash);
});
compilation.plugin("additional-chunk-assets".function() {
  // Here when modul changes, find the changed module and generate JSON and the corresponding Module Template information
  debugger
  var records = this.records;
  if(records.hash === this.hash) return;
  if(! records.moduleHashs || ! records.chunkHashs || ! records.chunkModuleIds)return;
  // Loop through the module to see if the module has changed with the hash value
  this.modules.forEach(function(module) {
    var identifier = module.identifier();
    var hash = require("crypto").createHash("md5");
    module.updateHash(hash);
    hash = hash.digest("hex");
    module.hotUpdate = records.moduleHashs[identifier] ! == hash; });// this.hash Hash of the Webpack Compilation object
  var hotUpdateMainContent = {
    h: this.hash,
    c: {}};// records.chunkHashs contains hash information for all chunks
  Object.keys(records.chunkHashs).forEach(function(chunkId) {
    chunkId = isNaN(+chunkId) ? chunkId : +chunkId;
    // If the Module changes due to file modification => find the chunk
    var currentChunk = this.chunks.find(chunk= > chunk.id === chunkId);
    if(currentChunk) {
      // Use chunk to determine which module has changed
      var newModules = currentChunk.modules.filter(function(module) {
        return module.hotUpdate;
      });
      var allModules = {};
      currentChunk.modules.forEach(function(module) {
        allModules[module.id] = true;
      });
      // If there is a module in the project that is not referenced, the changed module will be found
      var removedModules = records.chunkModuleIds[chunkId].filter(function(id) {
        return! allModules[id]; });// If the module changes
      if(newModules.length > 0 || removedModules.length > 0) {
        // Get the module string template based on the changed module
        var source = hotUpdateChunkTemplate.render(chunkId, newModules, removedModules, this.hash, this.moduleTemplate, this.dependencyTemplates);
        var filename = this.getPath(hotUpdateChunkFilename, {
          hash: records.hash,
          chunk: currentChunk
        });
        this.additionalChunkAssets.push(filename);
        / / filename is: ` ${currentChunk}. ${records. The hash}. Hot - update. Js} ` = > 0.9236 d98784cee1af7a96. Hot - update. Js file
        this.assets[filename] = source;
        // Identifies module changes
        hotUpdateMainContent.c[chunkId] = true;
        currentChunk.files.push(filename);
        this.applyPlugins("chunk-asset", currentChunk, filename); }}else {
      hotUpdateMainContent.c[chunkId] = false; }},this);
  Json '=> 9236d98784cee1af7a96.hot-update.json file
  var source = new RawSource(JSON.stringify(hotUpdateMainContent));
  var filename = this.getPath(hotUpdateMainFilename, {
    hash: records.hash
  });
  this.assets[filename] = source;

  // Note: The content added to this.assets above generates the contents of the file in the Compiler.emitAssets stage
});

Copy the code

Conclusion: When the file changes, Webpack compiles and generates hot-update.json and the corresponding file module hot-update.js information for generating JS files in the Compiler.emitassets phase

How do I notify the browser when WebPack is finished? The following file is webpack-dev-server server.js

function Server(compiler, options) {
  // debugger
  // Default options
  if(! options) options = {};// Attributes in the WebPack configuration, determined by hot update
  this.hot = options.hot || options.hotOnly;

  compiler.plugin('done', (stats) => {
    // Register events for the Webpack Compiler object and notify the client browser with WebSockets
    debugger
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
  });

  // Init express server
  const app = this.app = new express(); // eslint-disable-line

  app.all(The '*', (req, res, next) => { // eslint-disable-line
    if (this.checkHost(req.headers)) { return next(); }
    res.send('Invalid Host header');
  });

  // webpackDevMiddleware listens for changes to files watch -> build
  // middleware for serving webpack bundle
  this.middleware = webpackDevMiddleware(compiler, options);
  // ...
  this.listeningApp = http.createServer(app);
  // ...
}

// delegate listen call and init sockjs
Server.prototype.listen = function (port, hostname, fn) {
  this.listenHostname = hostname;
  // eslint-disable-next-line

  const returnValue = this.listeningApp.listen(port, hostname, (err) => {
    const sockServer = sockjs.createServer({
      // Use provided up-to-date sockjs-client
      sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js'.// Limit useless logs
      log(severity, line) {
        if (severity === 'error') { log(line); }}}); sockServer.on('connection', (conn) => {
      if(! conn)return;
      if (!this.checkHost(conn.headers)) {
        this.sockWrite([conn], 'error'.'Invalid Host header');
        conn.close();
        return;
      }
      this.sockets.push(conn);

      conn.on('close', () = > {const connIndex = this.sockets.indexOf(conn);
        if (connIndex >= 0) {
          this.sockets.splice(connIndex, 1); }});Devserver. hot= true tells the client how to browse the updated code according to the configuration in webpackConfig
      if (this.hot) this.sockWrite([conn], 'hot');

      if (!this._stats) return;
      this._sendStats([conn], this._stats.toJson(clientStats), true);
    });

    if (fn) {
      fn.call(this.listeningApp, err); }});return returnValue;
};

Server.prototype.sockWrite = function (sockets, type, data) {
  sockets.forEach((sock) = > {
    sock.write(JSON.stringify({
      type,
      data
    }));
  });
};

// send stats to a socket or multiple sockets
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'); }
  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'); }};module.exports = Server;
Copy the code

When a type: OK message occurs after the client browser receives the message, the process is as follows: Part of the code after webpack is packaged

//webpack/hot/dev-server.js is the file added in the webpack entry
module.hot.check(true).then(function(updatedModules) {}).catch(function(updatedModules) {})

/ / to enter
function hotCheck(apply) {
  if(hotStatus ! = ="idle") throw new Error("check() is only allowed in idle status");
  hotApplyOnUpdate = apply;
  hotSetStatus("check");
  return hotDownloadManifest().then(function(update) {
  
    // update.c Identifies whether the chunk has changed
    hotAvailableFilesMap = update.c;
    hotUpdateNewHash = update.h;

    hotSetStatus("prepare");
    var promise = new Promise(function(resolve, reject) {});// Start requesting hot-update.json file
     hotEnsureUpdateChunk(chunkId);
    return promise;
  });
}

// Request the hot-update.json file generated by webpack earlier
function hotDownloadManifest() { // eslint-disable-line no-unused-vars
  return new Promise(function(resolve, reject) {
    if(typeof XMLHttpRequest === "undefined")
      return reject(new Error("No browser support"));
    try {
      var request = new XMLHttpRequest();
      var requestPath = __webpack_require__.p + "" + hotCurrentHash + ".hot-update.json";
      request.open("GET", requestPath, true);
      request.timeout = 10000;
      request.send(null);
    } catch(err) {
      return reject(err);
    }
    request.onreadystatechange = function() {
      if(request.readyState ! = =4) return;
        // ...resolve(update); }}; }); }// Request the hot-update.js file generated by webpack earlier
function hotDownloadUpdateChunk(chunkId) { // eslint-disable-line no-unused-vars
  var head = document.getElementsByTagName("head") [0];
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.charset = "utf-8";
  script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
  head.appendChild(script);
}

// The requested js file executes the following code
function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars
  hotAddUpdateChunk(chunkId, moreModules);
  if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules); };// The following part of logic...
while(queue.length > 0) {
  moduleId = queue.pop();
  module = installedModules[moduleId];
  if(!module) continue;

  var data = {};

  // Call dispose handlers
  var disposeHandlers = module.hot._disposeHandlers;
  for(j = 0; j < disposeHandlers.length; j++) {
    cb = disposeHandlers[j];
    cb(data);
  }
  hotCurrentModuleData[moduleId] = data;

  // disable module (this disables requires from this module)
  module.hot.active = false;

  // Delete the cache
  // remove module from cache
  delete installedModules[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 the changing module
// insert new code
for(moduleId in appliedUpdate) {
  if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) { modules[moduleId] = appliedUpdate[moduleId]; }}// After inserting the module, re-execute the js file. This process is not refreshed by the browser, as can be seen from the browser Network
// Load self accepted modules
for(i = 0; i < outdatedSelfAcceptedModules.length; i++) {
  var item = outdatedSelfAcceptedModules[i];
  moduleId = item.module;
  hotCurrentParents = [moduleId];
  try {
    __webpack_require__(moduleId);
  } catch(err) {}
}

// __webpack_require__(moduleId); Enter app.js file again to execute =>

/ * 37 * /
/ * * * / (function(module, exports, __webpack_require__) {

"use strict";

__webpack_require__(71);

if (true) {
  module.hot.accept((err) = > {
    if (err) {
      console.error('Cannot apply HMR update.', err); }}); }Copy the code

Finally, let’s summarize the hot update HMR process:

When we modify a file and save it, webpack-dev-server gets access to webpack lifecycle points through webpack-dev-middleware, Webpack packaging process through HotModuleReplacementPlugin plug-in generated hot – update. Js and hot – update. Json file, the former is the module strings of change information, The latter is the chunk information corresponding to the module after packaging and the hash value after packaging, which determines whether the client browser is updated. Webpack-dev-server then sends the message over WebSockets to the client browser. The browser receives the message, requests both files, and then removes the installedModules global cache object, reassigns the value, and executes the file again. In this way, we can update the changed module without refreshing the browser. The code for updating the module in Webpack is quite complex, and some details are not debugged, so the process from Server to Client and from Client to Server is also clear

The last

The content is a little too much, please forgive the mistake! There are some technical points that are not mentioned, such as webpack packaging process, webpack module to detect file changes, Webpack-dev-middleware, webpack-dev-server module, request forwarding, etc., which are also not discussed. Clone the code yourself if you are interested, if you debug the Webpack process, it will be much better to learn about these things.

There may be some students who will say: look at the effect of these, of course, for me at that time is curiosity, through understanding daniu code implementation, can learn about the relevant excellent lib library, enhance their code reading ability. Again is to understand some of the bottom of the use of its, but also with ease.

Reference: 1, 2, fed.taobao.org/blog/taofed zhuanlan.zhihu.com/p/30669007… 3, github.com/webpack/tap… 4. Astexplorer.net is useful to see how WebPack performs AST analysis on code