What is a hot update

Hot update generally refers to the app has a small update, in the form of direct patch update app, in contrast, another is to download the entire app update package, hot update way found in mobile game, because the mobile game is bigger, now generally are hundreds of megabytes to several G, if there is a small update let users to download the complete package, I don’t think anyone will be using this app in a few days

Web hot update & hot load implementation premise

If you follow the above statement, there may be no hot update on the Web, mainly because the Web is B/S (browser + server), the browser mainly communicates with the server through THE HTTP protocol, the server responds to the HTTP request, the HTTP connection is finished, unlike the APP is C /S (client + server). The server cannot push patches to the browser, but why is it that the Web can now implement hot-loader or module replacement HMR mainly due to the two new communication methods proposed in HTML5

  • WebSocket: a communication mechanism that allows a long TCP link between a browser and a server

  • EventSource (server-sent-Events) : indicates a network event interface pushed by the Server. An EventSource instance opens a persistent connection to the HTTP service, sending events in text/event-stream format, and remains open until it is asked to close. (specific can look at this article www.ruanyifeng.com/blog/2017/0…).

The difference between:

  • Unlike WebSocket, the EventSource server push is unidirectional. Data information is distributed one-way from server to client. This makes EventSources a great choice when there is no need to send data from the client to the server as a message.

  • SSE supports disconnected reconnection, WebSocket needs to implement itself

  • SSE uses HTTP protocol, which is supported by existing server software. WebSocket is a stand-alone protocol.

  • SSE is generally only used to transmit text, binary data needs to be encoded after transmission, WebSocket default support to transmit binary data

  • SSE supports user-defined message types to be sent.

How WebPack hot updates work

Hot load: Simply refresh the browser without retaining the page state, like window.location.reload() hot update: The other is module hot replacement based on WDS (Webpack-dev-server), which only needs to partially refresh the changed modules on the page, while retaining the current page state (Webpack-dev-server cannot do this by itself, it needs to be used with React-hot-Loader). For example, the status of check boxes, input boxes, and so on

Webpack build

When you run NPM run start, the following information is displayed on the console

The hash: 8420886c15F084075F1D field represents the identifier of a compilation. When the changes are saved, WebPack recompiles the code and the console displays the following

Find hash has become 94992 a48d7e645922c7b because it represents a new compilation contrast, found that there will be a new file js: main. 8420886 c15f084075f1d. Hot – update. Js new json: 8420886c15f084075f1d.hot-update.json: 8420886c15f084075F1d is the prefix for this generation of JS and JSON. Let’s take a look at the browser side. After the code is modified and saved, the browser will issue two requests after the compilation of Webpack

You can see that the requested files are the JS and JSON files generated for this compilation, and then take a look at the contents of each file. Let’s first take a look at the json file

The c field represents the currently changed Module, and the H field represents the current compiled hash, which is used as the prefix for the next request file. Take a look at the contents of the JS file, XXX is the moduleId, and we will discuss webpackHotUpdate later

webpackHotUpdate("main", {'xxx': (function(module, __webpack_exports__, __webpack_require__) {}})))Copy the code

What does the server do during the hot update

What does the server do

Before we start, run the following steps to debug the React project you created with the creation-react-app

  • NPM run eject to generate the configuration

  • Modify webpack.config.js in the config directory

const shouldUseReactRefresh = env.raw.FAST_REFRESH || false;
/ / to
const shouldUseReactRefresh = false;
Copy the code

This is the official implementation of the Fast Refresh function, which is faster and more stable, similar to the React-hot-loader. For details, see blog.logrocket.com/whats-new-i…

  • Modify theconfigIn the directorywebpackDevServer.config.js
injectClient: false
/ / to
injectClient: true
Copy the code

The configuration for this is described below

  • Remove webpackDevClientEntry from entry in webpack.config.js

  • Add the following code to index.js in the SRC directory

if (module.hot) {
  module.hot.accept(() = > {
    ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>.document.getElementById('root')); })}Copy the code

Running NPM run start calls start.js start.js in the scripts

const devServer = new WebpackDevServer(compiler, serverConfig);
Copy the code

Webpack-dev-server package server.js, server.js constructor

class Server {
    constructor(compiler, options = {}, _log) {
        / / webpack compiler
        this.compiler = compiler;
        this.options = options;
        updateCompiler(this.compiler, this.options);
        this.setupHooks();
        // Initialize the app
        this.setupApp();
        this.setupDevMiddleware();
        this.setupFeatures();
        / / create a server
        this.createServer(); }}Copy the code

First look at the updateCompiler () function that captures the code of the main webpack – dev – server/lib/utils/updateCompiler js

'use strict';

/* eslint-disable no-shadow, no-undefined */
const webpack = require('webpack');
const addEntries = require('./addEntries');
const getSocketClientPath = require('./getSocketClientPath');

function updateCompiler(compiler, options) {
  if(options.inline ! = =false) {
    // Find the HMR plugin
    const findHMRPlugin = (config) = > {
      if(! config.plugins) {return undefined;
      }
      return config.plugins.find(
        (plugin) = > plugin.constructor === webpack.HotModuleReplacementPlugin
      );
    };

    const compilers = [];
    const compilersWithoutHMR = [];
    let webpackConfig;
    webpackConfig = compiler.options;
    compilers.push(compiler);
    // Not found
    if(! findHMRPlugin(compiler.options)) { compilersWithoutHMR.push(compiler); }// Add something to the entry file
    addEntries(webpackConfig, options);
    
    // If the plugin is found, run the plugin (that is, call the apply method of the plugin)
    if (options.hot || options.hotOnly) {
      compilersWithoutHMR.forEach((compiler) = > {
        const plugin = findHMRPlugin(compiler.options);
        if(plugin) { plugin.apply(compiler); }}); }}}Copy the code

See addEntries () method webpack – dev – server/lib/utils/addEntries js

'use strict';

const webpack = require('webpack');
const createDomain = require('./createDomain');

/**
 * A Entry, it can be of type string or string[] or Object<string | string[],string>
 * @typedef {(string[] | string | Object<string | string[],string>)} Entry* /

/**
 * Add entries Method
 * @param {? Object} config - Webpack config
 * @param {? Object} options - Dev-Server options
 * @param {? Object} server
 * @returns {void}* /
function addEntries(config, options, server) {
  if(options.inline ! = =false) {
    const app = server || {
      address() {
        return { port: options.port };
      },
    // clientEntry
    const domain = createDomain(options, app);
    const sockHost = options.sockHost ? `&sockHost=${options.sockHost}` : ' ';
    const sockPath = options.sockPath ? `&sockPath=${options.sockPath}` : ' ';
    const sockPort = options.sockPort ? `&sockPort=${options.sockPort}` : ' ';
    const clientEntry = `The ${require.resolve(
      '.. /.. /client/'
    )}?${domain}${sockHost}${sockPath}${sockPort}`;
    // hotEntry
    if (options.hotOnly) {
      hotEntry = require.resolve('webpack/hot/only-dev-server');
    } else if (options.hot) {
      hotEntry = require.resolve('webpack/hot/dev-server');
    }

    const checkInject = (option, _config, defaultValue) = > {
      if (typeof option === 'boolean') return option;
      if (typeof option === 'function') return option(_config);
      return defaultValue;
    };

      const additionalEntries = checkInject(
        options.injectClient,
        config,
        webTarget
      )
        ? [clientEntry]
        : [];

      if (hotEntry && checkInject(options.injectHot, config, true)) {
        additionalEntries.push(hotEntry);
      }
      // 
      config.entry.unshift(additionalEntries)
      // Add the HMR plug-in if it is not available
      config.plugins.push(newwebpack.HotModuleReplacementPlugin()); }); }}module.exports = addEntries;
Copy the code

The whole updateCompiler() function basically does

  • Add the following two files to the entryxxx/node_modules/webpack-dev-server/client/index.jsThe file is mainly responsible for communicating with the serverxxx/node_modules/webpack/hot/dev-server.jsMainly responsible for the logic of hot update
[
    'xxx/node_modules/webpack-dev-server/client/index.js? [http://localhost:3000](http://localhost:3000/)'
    'xxx/node_modules/webpack/hot/dev-server.js'
    '.. /src/index.js'
]
Copy the code
  • joinHotModuleReplacementPluginPlug-in, and call the execution plug-in (call the apply method)

Let’s take a look at what setupHooks does, and capture the main code, server.js

setupHooks () {
    const addHooks = (compiler) = > {
      const { done } = compiler.hooks;
      done.tap('webpack-dev-server'.(stats) = > {
        this._sendStats(this.sockets, this.getStats(stats));
        this._stats = stats;
      });
    };
    addHooks(this.compiler);
}

// send stats to a socket or multiple sockets
_sendStats(sockets, stats, force) {
  this.sockWrite(sockets, 'hash', stats.hash);
  this.sockWrite(sockets, 'ok');
}
Copy the code

SetupHooks mainly did

  • Listen for WebPack compilation to complete and passwebsocketSend a message to the front-end

Next look down at setupApp server.js

    setupApp() {
        // Init express server
        // eslint-disable-next-line new-cap
        this.app = new express();
     }
Copy the code

Nothing to talk about, setupDevMiddleware

  setupDevMiddleware() {
    // middleware for serving webpack bundle
    this.middleware = webpackDevMiddleware(
      this.compiler,
      Object.assign({}, this.options, { logLevel: this.log.options.level })
    );
  }
Copy the code

Look at webpackDevMiddleware, the file is in the index.js index.js file under the Webpack-dev-Middleware package

module.exports = function wdm(compiler, opts) {

  const context = createContext(compiler, options);
    // Listen for code changes and recompile
    context.watching = compiler.watch(options.watchOptions, (err) = > {
      if (err) {
        context.log.error(err.stack || err);
        if(err.details) { context.log.error(err.details); }}}); setFs(context, compiler);// For the moment, ignore the return
};
  // Omit some useless code
  setFs(context, compiler) {

    let fileSystem;

    // store our files in memory
    const isConfiguredFs = context.options.fs;
    constisMemoryFs = ! isConfiguredFs && ! compiler.compilers && compiler.outputFileSysteminstanceof MemoryFileSystem;

    if (isConfiguredFs) {
      / / to omit
    } else if (isMemoryFs) {
      fileSystem = compiler.outputFileSystem;
    } else {
      fileSystem = new MemoryFileSystem();
      compiler.outputFileSystem = fileSystem;
    }
    context.fs = fileSystem;
  },
Copy the code

From the above code you can see that so far, mainly done

  • Webpack is started in Watch mode and will be recompiled and repackaged as soon as the monitored files change.

  • The storage mode of Webpack files is changed to memory, which improves the storage and reading efficiency of files.

Go ahead and look at the webpack-dev-middleware/index.js that comes back from webpack-dev-middleware

  function wdm (compiler, opts) {
      return Object.assign(middleware(context), {
          / / to omit
      });
  }
  // middleware
  module.exports = function wrapper(context) {
  return function middleware(req, res, next) {
    let filename = getFilenameFromUrl(
      context.options.publicPath,
      context.compiler,
      req.url
    );
    return new Promise((resolve) = > {
      // If the file name in memory satisfies the hash and is a file rather than a directory, processRequest is executed
      handleRequest(context, filename, processRequest, req);
      // eslint-disable-next-line consistent-return
      function processRequest() {
        try {
         // Omit the code
        // Read the contents of the file
        let content = context.fs.readFileSync(filename);
        // Omit the code
        if (res.send) {
          res.send(content);
        } else{ res.end(content); } resolve(); }}); }; };Copy the code

The Middleware method is a middleware wrapper function that returns the middleware. The middleware is meant to intercept the update file requested by the browser, read the content from memory and return it. Go back to the setupDevMiddleware method webpack-dev-server /lib/server.js

  setupDevMiddleware() {
    // middleware for serving webpack bundle
    this.middleware = webpackDevMiddleware(
      this.compiler,
      Object.assign({}, this.options, { logLevel: this.log.options.level })
    );
  }
Copy the code

SetupDevMiddleware is the middleware that comes back

  • Webpack is started in Watch mode and will be recompiled and repackaged as soon as the monitored files change.

  • The storage mode of Webpack files is changed to memory, which improves the storage and reading efficiency of files.

  • Initialize this.middleware. The middleware is designed to intercept a browser request to update a file, read the contents of the file from memory and return it

Next look down at the setupFeature() method webpack-dev-server /lib/server.js

  setupFeatures() {
    const features = {
      middleware: () = > {
        // include our middleware to ensure
        // it is able to handle '/index.html' request after redirect
        this.setupMiddleware(); }}; features['middleware']()
  }
  // this.middleware is for the middleware above. Load the middleware
  setupMiddleware() {
    this.app.use(this.middleware);
  }
Copy the code

The setUpFeature() mainly does

  • Loading middleware

Moving on to the createServer() function

this.listeningApp = http.createServer(this.app);
Copy the code

Mainly for the

  • Creating a server

Go back to scripts/start.js

devServer.listen(port, HOST, err= > {
  // Omit the code
  openBrowser(urls.localUrlForBrowser);
});
Copy the code

Call the listen() method webpack-dev-server /lib/server.js in server.js

listen(port, hostname, fn) {
    this.hostname = hostname;

    return this.listeningApp.listen(port, hostname, (err) = > {
      // 
      this.createSocketServer();
    });
  }
Copy the code

The listen() method mainly does

  • Create a websocket

Use a flow chart to represent the entire process

The complete process for hot updates is as follows

Images from (segmentfault.com/a/119000002…).

Reference documentation

Easily understand webpack hot update principle [www.ruanyifeng.com/blog/2017/0…