Always curious about webpack hot update, after reading some materials. Try writing a simple demo.

step1

The first step is to implement a module handling tool similar to WebPack. Here is a brief introduction to these files using a demo from a geek Time course:

Simple - webpack ├ ─ package - lock. Json ├ ─ package. The json ├ ─ simplepack. Config. Js ├ ─ SRC | ├ ─ the greeting. Js | ├ ─ index. The HTML | └ index. Js ├ ─ lib | ├ ─ compiler. Js | ├ ─ index. The js | └ parser. JsCopy the code
  • Simplepack.config. js: similar to webpack.config.js
  • SRC: source
  • Lib: Similar to Webpack. Parser. js uses the Babylon library to analyze dependencies from the AST tree

step2

1. The process

Step 2, open a WebSocket service and insert the communication script into the HTML. In webpack, the client will receive hot, liveReload, invalid, hash, and still- OK notifications. Among them, the wanring and OK types will trigger reloadApp functions. This function raises the webpackHotUpdate event, which handles the following events:

hotEmitter.on("webpackHotUpdate".function (currentHash) {
  lastHash = currentHash;
  if(! upToDate() &&module.hot.status() === "idle") {
    log("info"."[HMR] Checking for updates on the server..."); check(); }}); log("info"."[HMR] Waiting for update signal from WDS...");

function check() {
  module.hot
    .check(true)
    .then(function (updatedModules) {
    if(! updatedModules) { log("warning"."[HMR] Cannot find update. Need to do a full reload!");
      log(
        "warning"."[HMR] (Probably because of restarting the webpack-dev-server)"
      );
      window.location.reload();
      return;
    }

    if(! upToDate()) { check(); } __webpack_require__(374)(updatedModules, updatedModules);

    if (upToDate()) {
      log("info"."[HMR] App is up to date.");
    }
  })
    .catch(function (err) {
    / / a little
  });
};
Copy the code

The update is executed in the module.hot.check method. If the updated module is not found, refresh the page. Continue looking for the hot.check method

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

SetStatus changes currentStatus, which is skipped. The __webpack_require__.hmrm () method initiates the request with fetch. After that, the hmrC method is executed. According to my search, there is only one method called jsonp, which performs some loading according to chunkId, and some unprovable and useful code, such as __webpack_require__.f. And then trigger a change, is truly internalApply, this function is mainly the currentUpdateApplyHandlers inside each object runs round the dispose method and the apply methods. The object is generated from a function called applyHandler, which is a little long. Two methods are defined:

  • Dispose: Change the various states of an unwanted module, calledmodule.hot._disposeHandlersmethods
  • Apply: update module, reload module, callmodule.hot._acceptedDependencies[dependency]

And then, where do these underlined methods come from? __webpack_require__ initializes object properties such as parent, children, and hot for each module. These methods are in the hot object. Take _acceptedDependencies, which you add when you call the Accept method in your module. Never seen this before? I haven’t used it either. I found it by looking at the Webpack website

+ if (module.hot) {
+   module.hot.accept('./print.js'.function() {+console.log('Accepting the updated printMe module! '); + printMe(); + +}})Copy the code

Frameworks like Vue and React implement this method in the corresponding loader, so we don’t need to write it ourselves.

2.demo

The above process is too complicated to handle. I want to simplify things a little bit. First, the modified code returns directly in the socket; Second, parent-child relationships between modules are not analyzed. I will write a module name, the module after the update process is also written to call the accept function, also only listen for changes to the file. On the basis of step1 demo modified, modified as follows

2.1 Some Configuration changes

'use strict';

const path = require('path');

module.exports = {
    entry: path.join(__dirname, './src/index.js'),
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js'
    },
    template: {
        src: './src/index.html'.replaceTag: '<! -- [123] -- > '
    },
    mode: 'server'
};
Copy the code

Template configuration to specify HTML to insert JS, and mode configuration to distinguish between packaged and local run

2.2 Lib /index.js part changed

const Compiler = require('./compiler');
const options = require('.. /simplepack.config');
const path = require('path');
const fs = require('fs');
const WebSocket = require('ws');
const socketScript = `  `;

const compiler = new Compiler(options)
if (options.mode === 'server') {
  const Koa = require('koa');
  const app = new Koa();
  // The server section provides resources
  compiler.hooks.afterEmitFiles.tapPromise('afterEmitFiles'.res= > {
    let html = fs.readFileSync(path.resolve(__dirname, '.. / ', options.template.src)).toString();
    html = html.replace(options.template.replaceTag, `<script>${res}</script>`)
    html = html.replace('</head>', socketScript + '</head>')
    app.use(async ctx => {
      ctx.header['content-type'] = 'text/html';
      ctx.body = html;
    });
    app.listen(8010);
    return Promise.resolve(console.log("Koa runs at: http://127.0.0.1:8010"));
  })
  // The socket section provides updates
  let wss = new WebSocket.Server({ port: 8090 });
  let wsList = [];
  let greetingFile = path.resolve(__dirname, '.. /src/greeting.js');
  fs.watch(greetingFile, () = > {
    compiler.run('./greeting.js');
    compiler.hooks.build.tapPromise('rebuild'.res= > {
      wsList.forEach(item= > item.ws.send(JSON.stringify(res)));
      return Promise.resolve(console.log('rebuild'));
    });
  });
  wss.on('connection'.function connection(ws, req) {
    const ip = req.socket.remoteAddress;
    wsList.push({ ip, ws })

    ws.on('message'.function incoming(message) {
      console.log('message')
      console.log('received: %s', message);
    });
    ws.on('close'.function () {
      console.log('close')
      wsList = wsList.filter(item= > item.ip !== ip);
    })
  });
}
compiler.run();
Copy the code

This section mainly adds a Websocket communication, and the code accepted by the client has been replaced with Function. Listen for file changes, recompile. Build. tapPromise is triggered when the run ends, and the client is notified of the update

2.3 Some changes in lib/ Compiler.js


const fs = require('fs');
const path = require('path');
const { getAST, getDependencis, transform } = require('./parser');
const { AsyncSeriesWaterfallHook } = require('tapable');

module.exports = class Compiler {
    constructor(options) {
        const { entry, output } = options;
        this.entry = entry;
        this.output = output;
        this.modules = [];
        this.mode = options.mode;
        this.hooks = {
            afterEmitFiles: new AsyncSeriesWaterfallHook(['arg1']),
            build: new AsyncSeriesWaterfallHook(['arg1'])}}run(filename = null) {
        if (filename) {
            var newModulesList = [];
            const entryModule = this.buildModule(filename);
            newModulesList.push(entryModule);
            newModulesList.map((_module) = > {
                _module.dependencies.map((dependency) = > {
                    newModulesList.push(this.buildModule(dependency));
                });
            });
            / / contrast
            const diff = [];
            newModulesList.forEach(newModule= > {
               const target = this.modules.find(item= > item.filename === newModule.filename);

               if(! target || target.transformCode ! == newModule.transformCode) { diff.push({ filename,content: newModule.transformCode }); }})this.hooks.build.promise(newModulesList);
        } else {
            const entryModule = this.buildModule(this.entry, true);
            this.modules.push(entryModule);
            this.modules.map((_module) = > {
                _module.dependencies.map((dependency) = > {
                    this.modules.push(this.buildModule(dependency));
                });
            });
            this.emitFiles(); }}buildModule(filename, isEntry) {
        let ast;
        if (isEntry) {
            ast = getAST(filename);
        } else {
            let absolutePath = path.join(process.cwd(), './src', filename);
            ast = getAST(absolutePath);
        }

        return {
          filename,
          dependencies: getDependencis(ast),
          transformCode: transform(ast)
        };
    }

    emitFiles() { 
        const outputPath = path.join(this.output.path, this.output.filename);
        let modules = ' ';
        this.modules.map((_module) = > {
            modules += ` '${ _module.filename }': function (require, module, exports) { ${ _module.transformCode }}, `
        });
        const bundle = `
            var modules = {${modules}};
            function __require__(fileName) {
                const fn = modules[fileName];

                const module = { exports : {
                    accept () {}
                } };

                fn(__require__, module, module.exports);

                return module.exports;
            }
            __require__('The ${this.entry}');
        `;
            
        if (this.mode ! = ='server') {
            fs.writeFileSync(outputPath, bundle, 'utf-8');
        }
        this.hooks.afterEmitFiles.promise(bundle); }};Copy the code

And finally, compile.js. Two hooks have been added to inform the outside world that compilation has finished. The run function adds handling for compilation of a module. Since I’m only listening for one dependency, map and foreach can be saved. The emitFiles function makes some changes to the bundle for easy replacement and update.

The effect

Under the SRC HTML

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
</head>
<body>
  <p id="p">123</p>
</body>
<! - [123] -- >
</html>
Copy the code

Under the SRC index. Js

import { greeting } from './greeting.js';
greeting();
Copy the code

Under the SRC the greeting. Js

var count = 0;
export function greeting() {
    document.querySelector('#p').innerHTML = count;
}
export function accept () {
    count++;
    document.querySelector('#p').innerHTML = count;
}
Copy the code

The effect is as follows: