In the modern front-end development experience, the browser automatically completes the code update while maintaining the current page state after the code change, which has already become the standard configuration in many development tool chains, today we discuss the topic is the use and implementation principle of Webpack DevServer & HMR.

The basic use

Take a look at the following example:

// src/index.css
#app > div {
  color:red;
  font-size: 20px;
}

// src/app.js
export function setup(initValue = null) {
  let appElement = document.getElementById('app');
  let nameInputElement = document.createElement('input');
  nameInputElement.type = 'text';
  nameInputElement.placeholder = 'Please enter your name';
  appElement.appendChild(nameInputElement);

  let nameDisplayElement = document.createElement('div');
  nameDisplayElement.innerHTML = 'Name:';
  appElement.appendChild(nameDisplayElement);

  nameInputElement.addEventListener('keyup'.(event) = > {
    nameDisplayElement.innerHTML = ` name:${event.target.value}`;
  });

  if (initValue) {
    nameInputElement.value = initValue;
    nameDisplayElement.innerHTML = ` name:${initValue}`; }}// src/index.js
import './index.css';
import { setup } from './app';

setup();
Copy the code

In the code snippet above, we create a text input field and a div that displays the contents of the input field in real time, as shown below:

Let’s take a quick look at two ways Webpack can enable DevServer & HMR.

Direct configuration

Since webpack-cli comes with webpack-dev-server built-in, we can enable devServer & HMR directly by setting the devServer property for webpack configuration:

// webpack.config.js
module.exports = {
  // Other configuration information......
  entry: './src/index.js'.devServer: {
    static: './dist'.port: 3000.hot: true,}};Copy the code

In the code snippet above, we set devServer to static (static resource root path), port (service port number), hot (HMR enabled), then run NPX webpack serve –open, wait for the browser to open, Try updating SRC /index.css or SRC /app.js and save. You will find that the browser automatically updates the code as follows:

Middleware

We can easily enable DevServer & HMR through direct configuration. Since webpack-CLI uses webpack-dev-server, Webpack-dev-middleware and Webpack-hot-middleware are used by webpack-dev-middleware, so in this section we use them directly to enable DevServer & HMR:

First run the following command to install the dependencies:

yarn add webpack-dev-middleware webpack-hot-middleware express --dev 
# or npm install --save-dev webpack-dev-middleware webpack-hot-middleware express
Copy the code

Create./server.js and enter the following:

const express = require('express');

const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler));
app.use(webpackHotMiddleware(compiler));

app.listen(config.devServer.port, function () {
  console.log(`Project is running at: http://localhost:${config.devServer.port}\n`);
});
Copy the code

In the code snippet above, we use Express to implement the local server, first instantiating Express and then loading the webpack.config.js configuration to complete the compiler instantiation. We then inject Webpack-dev-middleware and Webpack-hot-middleware into Express Middleware, and finally listen to the development service port in the WebPack configuration to start the service.

Then modify webpack.config.js:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  // Other configuration information......
  entry: [
    'webpack-hot-middleware/client? path=/__webpack_hmr&timeout=20000'.'./src/index.js',].devServer: {
    static: './dist'.port: 3000,},plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
};
Copy the code

Comparing the above code snippet with the webpack.config.js content from the previous section, you can see that there are several additional configurations:

  • inentryaddwebpack-hot-middleware/client? path=/__webpack_hmr&timeout=20000;
  • deletedevServerIn thehotAttribute (this attribute is redundant, so it is deleted);
  • inpluginsaddwebpack.HotModuleReplacementPlugin.

After everything above is done, run Node. /server.js, then go to http://localhost:3000 and try updating SRC /index.css or SRC /app.js and save it. You’ll see exactly the same effect as in the previous section.

Local refresh

If you run any of the examples above, you’ll see the fact that when you update the JavaScript code, the browser chooses to refresh instead of leaving the page as it was when you updated the CSS. This is because CSS updates are stateless, meaning they just need to be replaced, but JavaScript updates involve maintaining all sorts of states that the Webpack HMR module doesn’t know how to handle, so it takes the most conservative action (refreshing the page) to complete the code update. If we want to implement a partial refresh of JavaScript, we need to manually reset the state. For the example in this article, we can add the following to SRC /index.js:

if (module.hot) {
  module.hot.accept('./app.js'.function() {
    console.log('Accepting the updated from ./app.js');

    let initValue = null;
    let appElement = document.getElementById('app');
    while (appElement.firstChild) {
      let child = appElement.lastChild;
      if (child.nodeName.toLocaleLowerCase() === 'input') {
        initValue = child.value;
      }
      appElement.removeChild(child);
    }
    setup(initValue);
  });
}
Copy the code

The above code snippet, we first determine interface module. The hot is available (by HotModuleReplacementPlugin exposure), if available, we will through the module. The hot. Accept method to listen. / app. Js update module, In its callback, we first cache the value of the current input field through initValue, then remove all child nodes with the id app, and finally call the setup function to recreate the relevant nodes.

Run the example again in either of the above ways, enter something in the input box, modify./ SRC /app.js, and save. Now the page has been updated with the JavaScript code in the form of a partial refresh and maintains the page state as follows:

Of course, in addition to using the module. Hot outside, also can use the import meta. WebpackHot (can only be used in strict ESM), relevant details see webpack.docschina.org/api/hot-mod… , will not be elaborated here.

The principle of analysis

We introduced the basic usage of Webpack DevServer & HMR. In this section, we rely on to its webpack – dev – middleware, webpack – hot – middleware and HotModuleReplacementPlugin implementation principle were introduced simply.

webpack-dev-middleware

The main job of Webpack-dev-Middleware is to listen for module changes and respond to requests with the latest module content, and the core code looks like this (the code has been simplified to make it easier to understand) :

const path = require('path');
const memfs = require('memfs');
const mime = require("mime-types");

function getPaths(context) {
  const { stats } = context;
  return (stats.stats ? stats.stats : [stats]).map(({ compilation }) = > ({
    outputPath: compilation.getPath(compilation.outputOptions.path),
    publicPath: compilation.getPath(compilation.outputOptions.publicPath),
  }));
}

function getFilenameFromRequest(context, req) {
  const paths = getPaths(context);
  const baseUrl = `${req.protocol}: / /${req.get('host')}`;
  const url = new URL(req.url, baseUrl);

  for (const { publicPath, outputPath } of paths) {
    const publicPathUrl = new URL(publicPath, baseUrl);
    if (url.pathname && url.pathname.startsWith(publicPathUrl.pathname)) {
      let filename = outputPath;
      const pathname = url.pathname.substr(publicPathUrl.pathname.length);
      if (pathname) {
        filename = path.join(outputPath, pathname);
      }
      let fileStats;
      try {
        fileStats = context.outputFileSystem.statSync(filename);
      } catch (_) {
        continue;
      }
      if (fileStats.isFile()) {
        return filename;
      }
      if (fileStats.isDirectory()) {
        filename = path.join(filename, 'index.html');
        try {
          fileStats = context.outputFileSystem.statSync(filename);
        } catch (_) {
          continue;
        }
        if (fileStats.isFile()) {
          returnfilename; }}}}return undefined;
}

function ready(context, req, callback) {
  if (context.isReady) {
    callback(context.stats);
    return;
  }

  const name = req && req.url || callback.name;
  context.logger.info(`Wait until bundle finished${name ? ` :${name}` : ""}`);
  context.callbacks.push(callback);
}

function main(compiler) {
  /** * Context Settings */
  const context = {
    isReady: false.stats: null.callbacks: [].outputFileSystem: null.logger: compiler.getInfrastructureLogger('webpack-dev-middleware'),};/** * Memory file system Settings */
  const memeoryFileSystem = memfs.createFsFromVolume(new memfs.Volume());
  memeoryFileSystem.join = path.join.bind(path);
  context.outputFileSystem = memeoryFileSystem;
  compiler.outputFileSystem = memeoryFileSystem;

  /** * Compiler hook set */
  function invalid() {
    if (context.isReady) {
      context.logger.info('Compilation starting... ');
    }
    context.isReady = false;
    context.stats = undefined;
  }
  compiler.hooks.watchRun.tap('webpack-dev-middleware', invalid);
  compiler.hooks.invalid.tap('webpack-dev-middleware', invalid);
  compiler.hooks.done.tap('webpack-dev-middleware'.(stats) = > {
    context.stats = stats;
    context.isReady = true;
    process.nextTick(() = > {
      if(! context.isReady) {return;
      }
      context.logger.info('Compilation finished');
      const callbacks = context.callbacks;
      context.callbacks = [];
      callbacks.forEach(callback= > callback(context.stats))
    });
  });

  /** * Enable listening */
  const watchOptions = compiler.options.watchOptions || {};
  compiler.watch(watchOptions, (error) = > {
    if(error) { context.logger.error(error); }});/** * Express middleware */
  return async function(req, res, next) {
    const method = req.method;
    if (['GET'.'HEAD'].indexOf(method) === -1) {
      await next();
      return;
    }

    ready(context, req, async() = > {const filename = getFilenameFromRequest(context, req);
      if(! filename) {await next();
        return;
      }
      const contentType = mime.contentType(path.extname(filename));
      if (contentType) {
        res.setHeader('Content-Type', contentType);
      }
      res.send(context.outputFileSystem.readFileSync(filename));
    });
  };
};

module.exports = main;
Copy the code

In the main function, we do the following things:

  • Define the variable context to set the context;

  • In the development environment, general use memory to store the resources after the packaging, so here we create memeoryFileSystem memory file system through memfs module, and assign it to the compiler. OutputFileSystem, In this way, Webpack stores packaged resources in memory.

  • Then listen for the watchRun, invalid, and done hooks of the Compiler.

    • inwatchRuninvalidIn the callback, resetcontext.isReadycontext.stats;
    • indoneIs set firstcontext.isReadycontext.statsValue, and then callprocess.nextTickTo delay triggeringcontextCallback, which is delayed because if the resource has changed at this point, thencontextResources retrieved in the callback may be invalid and fired in the next taskcontextCallback to avoid resource invalidation.
  • After setting the compiler’s hook function listening, the Compiler. Watch method is called to start the Webpack process in listening mode.

  • Finally, the middleware that handles the packaged resource request is returned according to the express custom Middleware format.

In a concrete implementation of Express Middleware:

  • If it is not a GET or HEAD request, then call next method directly to hand the request to other middleware for processing, otherwise go to the next step;

  • Call ready and get the requested resource’s path (filename) by calling getFilenameFromRequest in the callback, then set the Content-Type header and send the file contents to the requester. This among them:

    • inreadyIn the function, ifcontext.isReadyThe value oftrue, call the callback directly, otherwise add it tocontext.callbacksIn order to incompiler.doneThe hook’s callback is triggered.
    • ingetFilenameFromRequestFunction, we first calculate the path of the requested resource, if the path exists and is a file, directly return; If the path exists and is a directory, matches those under the pathindex.htmlIf yes, return to the pathindex.html, otherwise returnsundefined.

In this section we take a brief look at the core implementation of Webpack-dev-Middleware and summarize the flow as follows:

  • Set Webpack’s file system to memory file system;
  • Listening to thewatchRun,invaliddoneA hook;
  • Run Webpack’s packaging process in listening mode;
  • throughexpress middlewareIntercepts and responds to resource requests.

webpack-hot-middleware

Webpack-hot-middleware’s main job is to listen for changes in modules and push them to clients, who then replace them. Unlike Webpack-dev-middleware, Webpack-hot-middleware has both server and client components, and the core implementation of webpack-hot-middleware is dissected below (the code has been minimized for ease of understanding).

The service side

function createEventStream() {
  let clientId = 0;
  let clients = {};
  function everyClient(callback) {
    Object.keys(clients).forEach(id= > callback(clients[id]));
  }

  return {
    handler: (req, res) = > {
      const headers = {
        'Access-Control-Allow-Origin': The '*'.'Content-Type': 'text/event-stream; charset=utf-8'.'Cache-Control': 'no-cache, no-transform'.// While behind nginx, event stream should not be buffered:
        // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
        'X-Accel-Buffering': 'no'};constisHttp1 = ! (parseInt(req.httpVersion) >= 2);
      if (isHttp1) {
        req.socket.setKeepAlive(true);
        Object.assign(headers, {
          'Connection': 'keep-alive'}); } res.writeHead(200, headers);
      res.write('\n');
      const id = clientId++;
      clients[id] = res;
      req.on('close'.function () {
        if(! res.finished) { res.end() };delete clients[id];
      });
    },
    publish: (payload) = > {
      everyClient(client= > {
        client.write('data: ' + JSON.stringify(payload) + '\n\n'); }); }}; };function publishStats(action, context) {
  const stats = context.stats.toJson({
    all: false.cached: true.children: true.modules: true.timings: true.hash: true}); [stats.children && stats.children.length ? stats.children: [stats]].forEach(() = > {
    context.logger.info(`Webpack built ${stats.hash} in ${stats.time} ms`);
    context.eventStream.publish({
      action,
      time: stats.time,
      hash: stats.hash,
      warnings: stats.warnings || [],
      errors: stats.errors || [],
      modules: stats.modules.reduce((result, moduleItem) = > ({
        ...result,
        [moduleItem.id]: moduleItem.name,
      }), {}),
    });
  });
}

function main(compiler) {
  /** * Context Settings */
  const context = {
    stats: null.path: '/__webpack_hmr'.eventStream: createEventStream(),
    logger: compiler.getInfrastructureLogger('webpack-hot-middleware'),};/** * Compiler hook set */
  compiler.hooks.invalid.tap('webpack-hot-middleware'.() = > {
    context.stats = null;
    context.logger.info('Webpack building... ');
    context.eventStream.publish({ action: 'building' });
  });
  compiler.hooks.done.tap('webpack-hot-middleware'.(stats) = > {
    context.stats = stats;
    publishStats('built', context);
  });

  /** * Express middleware */
  return function(req, res, next) {
    const url = new URL(req.url, `${req.protocol}: / /${req.get('host')}`);
    if(url.pathname ! == context.path) {return next();
    }
    context.eventStream.handler(req, res);
    if (context.stats) {
      publishStats('sync', context); }}}module.exports = main;
Copy the code

In the main function, we do the following things:

  • Define the variable context to set the context. Note the implementation of createEventStream, which uses the EventSource for server push.

  • Then listen for the invalid and done hooks of the Compiler, where:

    • ininvalidIn the callback, resetcontext.statsAnd push it to the clientbuildingEvents;
    • indoneIn the callback, setcontext.statsAnd push it to the clientbuiltEvents.
  • In an implementation of Express Middleware:

    • If requestedpathnameDon’t for/__webpack_hmr, then directly callnextMethod to pass the request to other middleware for processing, otherwise proceed to the next step;
    • throughcontext.eventStream.handlerThe call converts the current request toEventSourceLong links to maintain long-term communication with clients;
    • Then check whether it is setcontext.statsIf the value is met, it will be pushed to the clientsyncEvents.

The client

By analyzing the implementation of the server, it can be seen that the server needs to push events to the client, and the client naturally needs to listen for related events and process them. The core logic of the client is as follows:

let lastHash;
function upToDate(hash) {
  if (hash) { 
    lastHash = hash;
  }
  return lastHash == __webpack_hash__;
}

function applyCallback(err) {
  if (err) {
    console.warn(`[HMR] Update check failed: ${err.stack || err.message}`);
    return;
  }
  if (!upToDate()) {
    checkServer();
  }
}

const applyOptions = {
  ignoreUnaccepted: true.ignoreDeclined: true.ignoreErrored: true.onUnaccepted: (data) = > {
    console.warn(`Ignored an update to unaccepted module ${data.chain.join('- >')}`);
  },
  onDeclined: (data) = > {
    console.warn(`Ignored an update to declined module ${data.chain.join('- >')}`);
  },
  onErrored: (data) = > {
    console.error(data.error);
    console.warn(`Ignored an error while updating module ${data.moduleId} (${data.type}) `); }};function checkServer() {
  const checkCallback = (err) = > {
    if (err) {
      console.warn(`[HMR] Update check failed: ${err.stack || err.message}`);
      return;
    }
    module.hot.apply(applyOptions, applyCallback)
      .then(_= > applyCallback(null))
      .catch(applyCallback);
  };
  module.hot.check(false, checkCallback)
    .then(_= > checkCallback(null))
    .catch(checkCallback);
}

function parseMessage(message) {
  switch (message.action) {
    case 'building':
      console.log('[HMR] bundle rebuilding');
      break;
    case 'built':
      console.log(`[HMR] bundle ${message.hash} rebuilt in ${message.time} ms`);
    case 'sync':
      if(! upToDate(message.hash) &&module.hot.status() === 'idle') {
        console.log('[HMR] Checking for updates on the server... ');
        checkServer();
      }
      break;
    default:
      console.error(`[HMR] unknown message action:${message.action}`);
      break; }}function connect() {
  const source = new window.EventSource('/__webpack_hmr');
  source.onopen = () = > {
    console.log('[HMR] connected');
  };
  source.onerror = () = > {
    source.close();
  };
  source.onmessage = (event) = > {
    try {
      parseMessage(JSON.parse(event.data));
    } catch (error) {
      console.warn('Invalid HMR message: ' + event.data + '\n'+ error); }}; } connect();Copy the code
  • inconnectFunction, we mainly useEventSourceThe path to the server is/__webpack_hmrRequest to create a long link and then inonmessageIs called in a callback to theparseMessageFunction to process the information sent by the server;
  • inparseMessageFunction, if the message type issyncIn the message,hashWith the current latesthashIs not consistent andmodule.hot.statusThe return value ofidle, the callcheckServerFunctions;
  • incheckServerFunction, we do it by callingmodule.hot.checkAnd called in its callbackmodule.hot.applyTo complete the module update.

summary

In this section, we briefly analyze the core implementation of Webpack-hot-Middleware. Since the module update requires the cooperation of the server and the client, its implementation is divided into two parts: server and client:

  • On the server, useexpress middlewareIntercept path is/__webpack_hmrRequest to convert it to a long link so thatinvaliddoneAfter the hook is triggered, relevant events can be pushed to the client.
  • In the client, useEventSourceEstablish a long link with the server and listenonmessageEvent, which parses the message after receiving it to complete the module update operation.

HotModuleReplacementPlugin

As mentioned earlier, in webpack – hot – middleware in the client code, we call the module. The relevant methods in the hot, these methods by HotModuleReplacementPlugin injection, in this section, we to the brief analysis of its implementation.

Check HotModuleReplacementPlugin implementation, in the apply method, HotModuleReplacementPlugin done through listening compiler.hooks.com pilation hooks to support dynamic replace logic module Settings.

Depend on the set

if(compilation.compiler ! == compiler)return;

// Add a dependency to the module.hot.accept interface
compilation.dependencyFactories.set(
  ModuleHotAcceptDependency,
  normalModuleFactory
);
compilation.dependencyTemplates.set(
  ModuleHotAcceptDependency,
  new ModuleHotAcceptDependency.Template()
);
// Omit other module.hot.* interface dependency setup codes here

/ / add the import. Meta. WebpackHot. Rely on the accept interface
compilation.dependencyFactories.set(
  ImportMetaHotAcceptDependency,
  normalModuleFactory
);
compilation.dependencyTemplates.set(
  ImportMetaHotAcceptDependency,
  new ImportMetaHotAcceptDependency.Template()
);
// Omit other import.meta. WebpackHot.* interface dependency setup code
Copy the code

In the code snippet above:

  • Judge the present firstcompilationSubordinate to thecompilerWhether or notapplymethodscompilerIf the arguments are equal, return if they are not, otherwise continue (because this plug-in should not affect the execution of the subcompilation);
  • Then throughcompilation.dependencyTemplates.setCall separate Settingsmodule.hot.*import.meta.webpackHot.*Interface dependencies (used in thesealPhase generates relevant code);

compilation.hooks.record

Update some properties in records by listening on the Compilation.hooks. Record hook:

let hotIndex = 0;
const fullHashChunkModuleHashes = {};
const chunkModuleHashes = {};

compilation.hooks.record.tap(
  "HotModuleReplacementPlugin".(compilation, records) = > {
    if (records.hash === compilation.hash) return;

    const chunkGraph = compilation.chunkGraph;
    records.hash = compilation.hash;
    records.hotIndex = hotIndex;
    records.fullHashChunkModuleHashes = fullHashChunkModuleHashes;
    records.chunkModuleHashes = chunkModuleHashes;
    records.chunkHashes = {};
    records.chunkRuntime = {};
    for (const chunk of compilation.chunks) {
      records.chunkHashes[chunk.id] = chunk.hash;
      records.chunkRuntime[chunk.id] = getRuntimeKey(chunk.runtime);
    }
    records.chunkModuleIds = {};
    for (const chunk of compilation.chunks) {
      records.chunkModuleIds[chunk.id] = Array.from(
        chunkGraph.getOrderedChunkModulesIterable(
          chunk,
          compareModulesById(chunkGraph)
        ),
        m= >chunkGraph.getModuleId(m) ); }});Copy the code

compilation.hooks.fullHash

By listening to the compilation. Hooks. FullHash hooks (the runtime is added after the trigger) to calculate what the module has changed and store it into updatedModules:

const updatedModules = new TupleSet();
const fullHashModules = new TupleSet();
const nonCodeGeneratedModules = new TupleSet();

compilation.hooks.fullHash.tap("HotModuleReplacementPlugin".hash= > {
  const chunkGraph = compilation.chunkGraph;
  const records = compilation.records;
  for (const chunk of compilation.chunks) {
    const getModuleHash = module= > {
      if (compilation.codeGenerationResults.has(module, chunk.runtime)) {
        return compilation.codeGenerationResults.getHash(
          module,
          chunk.runtime
        );
      } else {
        nonCodeGeneratedModules.add(module, chunk.runtime);
        return chunkGraph.getModuleHash(module, chunk.runtime); }};const fullHashModulesInThisChunk = chunkGraph.getChunkFullHashModulesSet(chunk);
    // Set the value of fullHashModules
    const modules = chunkGraph.getChunkModulesIterable(chunk);
    if(modules ! = =undefined) {
      if (records.chunkModuleHashes) {
        if(fullHashModulesInThisChunk ! = =undefined) {
          for (const module of modules) {
            const key = `${chunk.id}|The ${module.identifier()}`;
            const hash = getModuleHash(module);
            if (fullHashModulesInThisChunk.has(module)) {
              if(records.fullHashChunkModuleHashes[key] ! == hash) { updatedModules.add(module, chunk);
              }
              fullHashChunkModuleHashes[key] = hash;
            } else {
              if(records.chunkModuleHashes[key] ! == hash) { updatedModules.add(module, chunk); } chunkModuleHashes[key] = hash; }}}else {
          // Set chunkModuleHashes}}else {
        / / set the value of fullHashChunkModuleHashes and chunkModuleHashes
      }
    }
  }

  hotIndex = records.hotIndex || 0;
  if (updatedModules.size > 0) hotIndex++;

  hash.update(`${hotIndex}`);
});
Copy the code

Hook compilation. Hooks. There are many interference in fullHash logic, their purpose is to calculate the fullHashModules, fullHashChunkModuleHashes, chunkModuleHashes, such as the value of a variable, With the interference removed, the core logic of the callback is to compare whether the hash value of the module in chunk has changed, and if so, add the related module and chunk to updatedModules.

compilation.hooks.processAssets

By listening to the compilation. Hooks. ProcessAssets hooks to generate [hash]. Hot – update. Js and [hash]. Hot – update. Json file:

compilation.hooks.processAssets.tap(
  {
    name: "HotModuleReplacementPlugin".stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
  },
  () = > {
    const chunkGraph = compilation.chunkGraph;
    const records = compilation.records;
    if (records.hash === compilation.hash) return;
    if(! records.chunkModuleHashes || ! records.chunkHashes || ! records.chunkModuleIds) {return;
    }
  
    // Compare whether the hash of modules in chunk has changed. If so, add related modules and chunks to updatedModules.
    // Update chunkModuleHashes value.

    const hotUpdateMainContentByRuntime = new Map(a);let allOldRuntime;
    // Collect obsolete runtimes by traversing the records.chunkRuntime key and store them in allOldRuntime;
    / / by traversal allOldRuntime forEachRuntime call and set the value of hotUpdateMainContentByRuntime.
    if (hotUpdateMainContentByRuntime.size === 0) return;
 
    const allModules = new Map(a);// List of all modules (for later verification of which modules have been removed completely)
    // set the value of allModules through compilation.modules.

    const completelyRemovedModules = new Set(a);for (const key of Object.keys(records.chunkHashes)) {
      const oldRuntime = keyToRuntime(records.chunkRuntime[key]);
      const remainingModules = [];
      ChunkModuleIds [key] to set the values of remainingModules and completelyRemovedModules.
      let chunkId;
      let newModules;
      let newRuntimeModules;
      let newFullHashModules;
      let newDependentHashModules;
      let newRuntime;
      let removedFromRuntime;
      const currentChunk = find(
        compilation.chunks,
        chunk= > `${chunk.id}` === key
      );
      if (currentChunk) {
        // Set the newRuntime value.
        if (newRuntime === undefined) continue;
        // Set newModules, newRuntimeModules, newFullHashModules, newDependentHashModules based on updatedModules;
        // Set the removedFromRuntime value based on oldRuntime and newRuntime.
      } else {
        // Because chunk has been deleted, set the values of removedFromRuntime and newRuntime to oldRuntime
      }
      if (removedFromRuntime) {
        / / update according to remainingModules and newRuntime hotUpdateMainContentByRuntime
      }

      // Generate the [hash].hot-update.js file
      if ((newModules && newModules.length > 0) || (newRuntimeModules && newRuntimeModules.length > 0)) {
        const hotUpdateChunk = new HotUpdateChunk();
        // Check whether backward compatibility of the Webpack 4 API is enabled
        if (backCompat)
          ChunkGraph.setChunkGraphForChunk(hotUpdateChunk, chunkGraph);
        hotUpdateChunk.id = chunkId;
        hotUpdateChunk.runtime = newRuntime;
        if (currentChunk) {                                                             
          for (const group of currentChunk.groupsIterable)
            hotUpdateChunk.addGroup(group);
        }
        chunkGraph.attachModules(hotUpdateChunk, newModules || []);
        chunkGraph.attachRuntimeModules(
          hotUpdateChunk,
          newRuntimeModules || []
        );
        if (newFullHashModules) {
          chunkGraph.attachFullHashModules(
            hotUpdateChunk,
            newFullHashModules
          );
        }
        if (newDependentHashModules) {
          chunkGraph.attachDependentHashModules(
            hotUpdateChunk,
            newDependentHashModules
          );
        }
        const renderManifest = compilation.getRenderManifest({
          chunk: hotUpdateChunk,
          hash: records.hash,
          fullHash: records.hash,
          outputOptions: compilation.outputOptions,
          moduleTemplates: compilation.moduleTemplates,
          dependencyTemplates: compilation.dependencyTemplates,
          codeGenerationResults: compilation.codeGenerationResults,
          runtimeTemplate: compilation.runtimeTemplate,
          moduleGraph: compilation.moduleGraph,
          chunkGraph
        });
        for (const entry of renderManifest) {
          let filename;
          let assetInfo;
          if ("filename" in entry) {
            filename = entry.filename;
            assetInfo = entry.info;
          } else{({path: filename, info: assetInfo } =
              compilation.getPathWithInfo(
                entry.filenameTemplate,
                entry.pathOptions
              ));
          }
          const source = entry.render();
          compilation.additionalChunkAssets.push(filename);
          compilation.emitAsset(filename, source, {
            hotModuleReplacement: true. assetInfo });if (currentChunk) {
            currentChunk.files.add(filename);
            compilation.hooks.chunkAsset.call(currentChunk, filename);
          }
        }
        forEachRuntime(newRuntime, runtime= >{ hotUpdateMainContentByRuntime .get(runtime) .updatedChunkIds.add(chunkId); }); }}const completelyRemovedModulesArray = Array.from(
      completelyRemovedModules
    );
    const hotUpdateMainContentByFilename = new Map(a);/ / set the value of hotUpdateMainContentByFilename, including attributes:
    // removedChunkIds, removedModules, updatedChunkIds and assetInfo.
    for (const {
      removedChunkIds,
      removedModules,
      updatedChunkIds,
      filename,
      assetInfo
    } of hotUpdateMainContentByRuntime.values()) {
        / / set the value of hotUpdateMainContentByFilename:
        // key is the value of filename. The properties are: removedChunkIds, removedModules, updatedChunkIds, and assetInfo.
    }

    // Generate the [hash].hot-update.json file
    for (const [
      filename,
      { removedChunkIds, removedModules, updatedChunkIds, assetInfo }
    ] of hotUpdateMainContentByFilename) {
      const hotUpdateMainJson = {
        c: Array.from(updatedChunkIds),
        r: Array.from(removedChunkIds),
        m:
          removedModules.size === 0
            ? completelyRemovedModulesArray
            : completelyRemovedModulesArray.concat(
                Array.from(removedModules, m= >
                  chunkGraph.getModuleId(m)
                )
              )
      };
      const source = new RawSource(JSON.stringify(hotUpdateMainJson));
      compilation.emitAsset(filename, source, {
        hotModuleReplacement: true. assetInfo }); }});Copy the code

Hook compilation. Hooks. ProcessAssets logic is more, here is to calculate newModules, hotUpdateMainContentByFilename variable replacement code to comment, view the simplified version of the implementation, The main task of the hook is generated according to newModules, hotUpdateMainContentByFilename variables [hash]. Hot – update. Js and [hash]. Hot – update. Json file. The Webpack-hot-middleware client calls module.hot.check and module.hot.apply internally after receiving a sync message. And completes the module update operation accordingly.

compilation.hooks.additionalTreeRuntimeRequirements

Through listening compilation. Hooks. AdditionalTreeRuntimeRequirements hooks, Settings and instantiate HMR need runtime, This ensures that when Webpack is packaged, the relevant Runtime code is incorporated into the resulting code.

compilation.hooks.additionalTreeRuntimeRequirements.tap(
  "HotModuleReplacementPlugin".(chunk, runtimeRequirements) = > {
    runtimeRequirements.add(RuntimeGlobals.hmrDownloadManifest);
    runtimeRequirements.add(RuntimeGlobals.hmrDownloadUpdateHandlers);
    runtimeRequirements.add(RuntimeGlobals.interceptModuleExecution);
    runtimeRequirements.add(RuntimeGlobals.moduleCache);
    compilation.addRuntimeModule(
      chunk,
      newHotModuleReplacementRuntimeModule() ); });Copy the code

Code conversion

Running the example above, and looking at the resulting code, we see that our hot update code consists of:

if (module.hot) {
  module.hot.accept('./app.js'.function() {
    console.log('Accepting the updated from ./app.js');

    let initValue = null;
    let appElement = document.getElementById('app');
    while (appElement.firstChild) {
      let child = appElement.lastChild;
      if (child.nodeName.toLocaleLowerCase() === 'input') {
        initValue = child.value;
      }
      appElement.removeChild(child);
    }
    setup(initValue);
  });
}
Copy the code

Becomes:

if (true) {
  module.hot.accept('./app.js'.function() {
    console.log('Accepting the updated from ./app.js');

    let initValue = null;
    let appElement = document.getElementById('app');
    while (appElement.firstChild) {
      let child = appElement.lastChild;
      if (child.nodeName.toLocaleLowerCase() === 'input') {
        initValue = child.value;
      }
      appElement.removeChild(child);
    }
    (0, _app__WEBPACK_IMPORTED_MODULE_1__.setup)(initValue);});
  }
}
Copy the code

This is because the HotModuleReplacementPlugin by setting the JavaScriptParser on the translation:

const applyModuleHot = parser= > {
  parser.hooks.evaluateIdentifier.for("module.hot").tap(
    {
      name: "HotModuleReplacementPlugin".before: "NodeStuffPlugin"
    },
    expr= > {
      return evaluateToIdentifier(
        "module.hot"."module".() = > ["hot"].true)(expr); }); parser.hooks.call .for("module.hot.accept")
    .tap(
      "HotModuleReplacementPlugin",
      createAcceptHandler(parser, ModuleHotAcceptDependency)
    );
};

normalModuleFactory.hooks.parser
  .for("javascript/auto")
  .tap("HotModuleReplacementPlugin".parser= > {
    applyModuleHot(parser);
    // omit other logic...
  });
normalModuleFactory.hooks.parser
  .for("javascript/dynamic")
  .tap("HotModuleReplacementPlugin".parser= > {
    applyModuleHot(parser);
  });
Copy the code

The above code, we through monitoring hook normalModuleFactory. Hooks. The parser, and in its applyModuleHot callback handler:

  • By listening on hooksparser.hooks.evaluateIdentifiermatchingmodule.hotThe evaluation expression (here isif (module.hot)) and then convert it totrue;
  • By listening on hooksparser.hooks.callmatchingmodule.hot.acceptCall, and then set up the necessary dependencies to generate the final code during the code generation phase with a code Generator in conjunction with the dependency template.

summary

This section of HotModuleReplacementPlugin implementation has carried on the simple analysis, here briefly summarizes the main process:

  • Add the necessary dependencies;
  • throughJavaScriptParserTransform module update code in our business code;
  • throughcompilation.hooks.recordcompilation.hooks.fullHashHooks that calculate and recordmoduleA series of information before and after the update;
  • According to the previousmoduleUpdate information incompilation.hooks.recordGenerated in the hook callback[hash].hot-update.js[hash].hot-update.jsonFiles so that clients can dynamically update modules based on these files.

Here it is important to note that in HarmonyImportDependencyParserPlugin, Will by calling HotModuleReplacementPlugin. GetParserHooks method to get associated with hotAcceptCallback and hotAcceptWithoutCallback JavaScriptParser Hooks, and respectively set HarmonyAcceptImportDependency and HarmonyAcceptDependency depend on:

// Other irrelevant code has been removed......
const HotModuleReplacementPlugin = require(".. /HotModuleReplacementPlugin");
module.exports = class HarmonyImportDependencyParserPlugin {
  apply(parser) {
    const { hotAcceptCallback, hotAcceptWithoutCallback } = HotModuleReplacementPlugin.getParserHooks(parser);
    hotAcceptCallback.tap(
      "HarmonyImportDependencyParserPlugin".(expr, requests) = > {
        if(! HarmonyExports.isEnabled(parser.state)) {// This is not a harmony module, skip it
          return;
        }
        const dependencies = requests.map(request= > {
          const dep = new HarmonyAcceptImportDependency(request);
          dep.loc = expr.loc;
          parser.state.module.addDependency(dep);
          return dep;
        });
        if (dependencies.length > 0) {
          const dep = new HarmonyAcceptDependency(
            expr.range,
            dependencies,
            true); dep.loc = expr.loc; parser.state.module.addDependency(dep); }}); hotAcceptWithoutCallback.tap("HarmonyImportDependencyParserPlugin".(expr, requests) = > {
        // Same as hotAcceptCallback, omit...}); }}Copy the code

HMR runtime

By above knowable, HotModuleReplacementPlugin through compilation. Hooks. AdditionalTreeRuntimeRequirements to set the HMR required runtime code, Module.hot. check, module.hot.apply

module.hot.check

Module. Hot. Check is actually called lib/HMR/HotModuleReplacement runtime. HotCheck of js function, its definition is as follows:

function hotCheck(applyOnUpdate) {
  if(currentStatus ! = ="idle") {
    throw new Error("check() is only allowed in idle status");
  }
  return setStatus("check")
    .then($hmrDownloadManifest$)
    .then(function (update) {
      if(! update) {return setStatus(applyInvalidatedModules() ? "ready" : "idle").then(
          function () {
            return null; }); }return setStatus("prepare").then(function () {
        var updatedModules = [];
        blockingPromises = [];
        currentUpdateApplyHandlers = [];
        return Promise.all(
          Object.keys($hmrDownloadUpdateHandlers$).reduce(function (promises, key) {
            $hmrDownloadUpdateHandlers$[key](
              update.c,
              update.r,
              update.m,
              promises,
              currentUpdateApplyHandlers,
              updatedModules
            );
            return promises;
          },
          [])
        ).then(function () {
          return waitForBlockingPromises(function () {
            if (applyOnUpdate) {
              return internalApply(applyOnUpdate);
            } else {
              return setStatus("ready").then(function () {
                returnupdatedModules; }); }}); }); }); }); }Copy the code

Webpack replaces several variables in the above code snippet when it is packaged:

  • $hmrDownloadManifest$Replace with__webpack_require__.hmrM, for loading[hash]-hot-update.jsonFile;
  • $hmrDownloadUpdateHandlers$Replace with__webpack_require__.hmrC, for loading[hash]-hot-update.jsFile;

Json and [hash]-hot-update.js files, and then execute the corresponding logic according to the applyOnUpdate value. The process is as follows:

  • If the current state is not Idle, throw an exception. Otherwise, go to the next step.

  • Set the current status to check with setStatus and load the [hash]-hot-update.json file with __webpack_require__.hmrM.

  • After the file is successfully loaded, if the update parameter is null in the callback, set the current state according to the return value of the applyInvalidatedModules call and return null in the callback, otherwise go to the next step;

  • Use setStatus to set the current state to prepare and convert __webpack_require__.hmrC to a Promise array to load the [hash]-hot-update.js file in parallel.

  • After the file loads successfully, call waitForBlockingPromises to wait for all pending requests, and then do the following different things based on the value of the applyOnUpdate argument:

    • ifapplyOnUpdatetrue, the implementation ofinternalApplyDependency replacement;
    • ifapplyOnUpdatefalseThrough thesetStatusSets the current state toreadyAnd returns in a callbackupdatedModules.

module.hot.apply

Module. Hot. Apply actually called lib/HMR/HotModuleReplacement runtime. HotApply of js function, its definition is as follows:

function hotApply(options) {
  if(currentStatus ! = ="ready") {
    return Promise.resolve().then(function () {
      throw new Error("apply() is only allowed in ready status");
    });
  }
  return internalApply(options);
}

function internalApply(options) {
  options = options || {};

  applyInvalidatedModules();

  var results = currentUpdateApplyHandlers.map(function (handler) {
    return handler(options);
  });

  currentUpdateApplyHandlers = undefined;

  var errors = results
    .map(function (r) {
      return r.error;
    })
    .filter(Boolean);

  if (errors.length > 0) {
    return setStatus("abort").then(function () {
      throw errors[0];
    });
  }

  // Now in "dispose" phase
  var disposePromise = setStatus("dispose");

  results.forEach(function (result) {
    if (result.dispose) result.dispose();
  });

  // Now in "apply" phase
  var applyPromise = setStatus("apply");

  var error;
  var reportError = function (err) {
    if(! error) error = err; };var outdatedModules = [];
  results.forEach(function (result) {
    if (result.apply) {
      var modules = result.apply(reportError);
      if (modules) {
        for (var i = 0; i < modules.length; i++) { outdatedModules.push(modules[i]); }}}});return Promise.all([disposePromise, applyPromise]).then(function () {
    // handle errors in accept handlers and self accepted module load
    if (error) {
      return setStatus("fail").then(function () {
        throw error;
      });
    }

    if (queuedInvalidatedModules) {
      return internalApply(options).then(function (list) {
        outdatedModules.forEach(function (moduleId) {
          if (list.indexOf(moduleId) < 0) list.push(moduleId);
        });
        return list;
      });
    }

    return setStatus("idle").then(function () {
      return outdatedModules;
    });
  });
}
Copy the code

In hotApply, if the current state is not ready, an exception is thrown; otherwise, internalApply is called. InternalApply runs as follows:

  • callapplyInvalidatedModulesUsed to perform the client in the callmodule.hot.invalidateIs the specified callback function.
  • Then throughcurrentUpdateApplyHandlersTo collectapplyHandleThe result of the execution of themodule.hot.applyIs the result of the specified callback function.
  • throughsetStatusSets the current state todisposeAnd remove obsolete modules.
  • throughsetStatusSets the current state toapplyAnd update the corresponding modules.

section

In this section, we briefly analyze the implementation of module.hot.check and module.hot.apply in the HMR runtime. Module. Hot also provides other API (webpack.docschina.org/api/hot-mod…). Due to space constraints, I will not analyze them here.

conclusion

This article gives a comprehensive analysis and introduction to Webpack DevServer & HMR:

  • First we introduced the use of Webpack DevServer & HMR introduced that we can passwebpack-cliTo quickly enable the feature, or borrow itwebpack api,webpack-dev-middleware,webpack-hot-middlewareHotModuleReplacementPluginTo self-enable;
  • And then we introducedwebpack-dev-middleware,webpack-hot-middlewareHotModuleReplacementPluginThe realization principle of whichwebpack-hot-middlewareIncluding client and server two parts, andHotModuleReplacementPluginIn addition to its own complex logic, it also needsotModuleReplacement.runtime.jsHarmonyImportDependencyParserPluginThe auxiliary support.

Webpack system is too large, this paper only gives a brief description of the implementation of core processes and methods, for the missing part, I sincerely hope to study together with you. Finally wish everyone happy code every day! ^_^