preface

Summary of personal excerpts, not original

webpack-dev-server

webpack-dev-server/bin/webpack-dev-server.js

let configYargsPath;
try {
  require.resolve('webpack-cli/bin/config/config-yargs');
  configYargsPath = 'webpack-cli/bin/config/config-yargs';
} catch (e) {
  configYargsPath = 'webpack-cli/bin/config-yargs';
}

let convertArgvPath;
try {
  require.resolve('webpack-cli/bin/utils/convert-argv');
  convertArgvPath = 'webpack-cli/bin/utils/convert-argv';
} catch (e) {
  convertArgvPath = 'webpack-cli/bin/convert-argv';
}

function startDevServer(config, options) {

  let compiler;

  try {
    // 2. Calling the Webpack function returns an instance of webpack Compiler
    compiler = webpack(config);
  } catch (err) {
  }

  try {
    // 3. Instantiate webpack-dev-server
    server = new Server(compiler, options, log);
  } catch (err) {
  }

  if (options.socket) {
  } else {
    // 4. Call server instance listen
    server.listen(options.port, options.host, (err) = > {
      if (err) {
        throwerr; }}); }}// 1. Process parameters and start
processOptions(config, argv, (config, options) = > {
  startDevServer(config, options);
});
Copy the code

(1) At the beginning, two files under the Webpack-CLI module were called to configure command line prompt options respectively, and the config of Webpack was collected from the command line and configuration file. Call processOptions to pass the collected parameters to the startDevServer method. Note that config is required by webpack and options is required by wepack-dev-server. StartDevServer calls the Webpack function to get the instantiated result Compiler. Compiler is passed to the Server for instantiation, that is, to instantiate webpack-dev-server. Finally, the Listen method in the instance is called to listen.

webpack-dev-server/lib/Server.js

class Server {
  constructor(compiler, options = {}, _log) {
    this.compiler = compiler;
    this.options = options;
    // 1. Provide default parameters for some options
    normalizeOptions(this.compiler, this.options);
    / / 2. The webpack compiler changes webpack - dev - server/lib/utils/updateCompiler js
    / / set up a hot - if option, automatic to HotModuleReplacementPlugin webpack configuration
    // - Inject some client code: the websocket client of Webpack relies on sockJS/websocket + webSocket client business code + webpack/hot/dev-server in hot mode
    updateCompiler(this.compiler, this.options);
    // 3. Add some hooks, which focus on webpack Compiler's done hooks after each compile (which triggers the _sendStats method to broadcast messages to the client)
    this.setupHooks();
    // 4. Instantiate express server
    this.setupApp();
    // 5. Set webpack-dev-middleware to handle static resources
    this.setupDevMiddleware();
    6. Create an HTTP server
    this.createServer();
  }


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

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

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


  createServer() {
    this.listeningApp = http.createServer(this.app);

    this.listeningApp.on('error'.(err) = > {
      this.log.error(err);
    });
  }


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

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

  createSocketServer() {
    const SocketServerImplementation = this.socketServerImplementation;
    this.socketServer = new SocketServerImplementation(this);

    this.socketServer.onConnection((connection, headers) = > {
      // Save the client connection after connection
      this.sockets.push(connection);

      if (this.hot) {
        // The hot option broadcasts a hot message first
        this.sockWrite([connection], 'hot');
      }

      this._sendStats([connection], this.getStats(this._stats), true);
    });
  }


  // eslint-disable-next-line
  sockWrite(sockets, type, data) {
    sockets.forEach((socket) = > {
      this.socketServer.send(socket, JSON.stringify({ type, data }));
    });
  }


  // send stats to a socket or multiple sockets
  _sendStats(sockets, stats, force) {
    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'); }}}Copy the code

(1) The constructor does the following: provides some default parameters and binds the WebPack Compiler hooks. The done hook is used to broadcast webscoket compilation information for WebPack each time the Compiler instance triggers compile completion. Instantiate an Express server, set up Webpack-dev-Middleware to handle processing of static resources, and then parse and create an HTTP server. (2) The listen method called in webpack-dev-server.js is used to listen on the configured port. Listen on the callback to initialize the webSocket server.

End of listening compilation

In the previous section, setupHooks is used to listen every time webPack builds. After each listening compilation, the _sendStats method is called to tell the browser ok, hash, and other events via websocket. The client gets the latest hash value.

Listening for file changes

In the previous section, setupDevMiddleware is used to listen for local file changes. It is important to note that webpack-dev-server is only responsible for starting up the service and prepping it. File-related actions are in Webpack-dev-Middleware. Compiler. watch is used to listen for file changes. Compiler mainly has two functions :(1) to compile and package the local file code. (2) After the compilation, the local file is monitored, if the file changes, then recompile. Among them, the change of monitoring file mainly depends on whether the generation time of the file changes

webapck-dev-middleware

  • webpack-dev-middleware/index.js
module.exports = function wdm(compiler, opts) {
  const options = Object.assign({}, defaults, opts);
  1. Initialize the context
  const context = createContext(compiler, options);

  // start watching
  if(! options.lazy) {// 2. Start webpack compilation
    context.watching = compiler.watch(options.watchOptions, (err) = > {
      if (err) {
        context.log.error(err.stack || err);
        if(err.details) { context.log.error(err.details); }}}); }else {
    // Lazy mode is only webpack compiled once the request comes in
  }

  // 3. Replace webPack's default outputFileSystem with memory-fs
  // fileSystem = new MemoryFileSystem();
  // compiler.outputFileSystem = fileSystem;
  setFs(context, compiler);

  // 3. Run the middleware function to return real middleware
  return middleware(context);
}
Copy the code

(1) This method returns middleware, which is used to handle requests for browser static resources. It initializes the context, defaults to non-lazy mode, and opens webPack’s Watch mode to enable compilation. The function of setFs is to use memory-FS (memory-FS implements the Node FS API memory-based fileSystem) to realize that the resources compiled by Webpack are not stored in hard disk but in memory. Instead, middleware, which actually handles requests, is loaded back on Express.

  • webpack-dev-middleware/lib/middleware.js
module.exports = function wrapper(context) {
  return function middleware(req, res, next) {
    // 1. According to the requested URL address, get the resource path of the webpack output of the absolute path
    let filename = getFilenameFromUrl(
      context.options.publicPath,
      context.compiler,
      req.url
    );

    return new Promise((resolve) = > {
      handleRequest(context, filename, processRequest, req);
      // eslint-disable-next-line consistent-return
      function processRequest() {

        // 2. Read the resource contents from memory
        let content = context.fs.readFileSync(filename);

        // 3. Return to the client
        if (res.send) {
          res.send(content);
        } else{ res.end(content); } resolve(); }}); }; };Copy the code

(1) Webapck-dev-Middleware handles requests: the browser opens the URL https://localhost:3000 and the request is handled by middleware. Middleware uses memory-FS to read requested resources from memory and send them back to the client.

Webscoket communication

(1) After modifying the code, trigger webPack recompilation, and execute the done hook callback after compilation. In server.js, setupHooks calls the _sendStats method to broadcast a message of type hash, and then broadcasts warnings/errors/ OK messages based on the compile information. (2) After the client receives the update, the application will be reloaded. Reload includes refreshing the whole page and updating the changed module. The two modes depend on whether the HOT option is passed in.

  • webpack-dev-server/client/index.js
var onSocketMessage = {
  hot: function hot() {
    options.hot = true;
    log.info('[WDS] Hot Module Replacement enabled.');
  },
  liveReload: function liveReload() {
    options.liveReload = true;
    log.info('[WDS] Live Reloading enabled.');
  },
  hash: function hash(_hash) {
    status.currentHash = _hash;
  },
  ok: function ok() {
    if (options.initial) {
      return options.initial = false;
    } // eslint-disable-line no-return-assignreloadApp(options, status); }}; socket(socketUrl, onSocketMessage);Copy the code

Initialize the Webscoket client and set the response callback function for different types (‘hot’, ‘liveReload’, ‘hash’, ‘OK’, etc.). In server.js, if the hot option is true, the webSocket client will broadcast a hot message when it connects to the Server. After receiving the hot message, the client will set the hot object to true. The server broadcasts the hash message after each compilation, and the client stores the hash value generated by the Webpack compilation temporarily after receiving it. An OK message is broadcast if the compilation is successful without warning or error, and the client receives the OK message and executes the reloadApp in the OK callback to refresh the application.

Trigger hot check

  • webpack/hot/dev-server.js
var lastHash;
 var upToDate = function upToDate() {
  return lastHash.indexOf(__webpack_hash__) >= 0;
 };
 var log = require("./log");
    // check for updates
 var check = function check() {
    // 3. Specific check logic
  module.hot
   .check(true)
   .then(function(updatedModules) {
        // 3.1 Update succeeded
   })
   .catch(function(err) {
    var status = module.hot.status();
        // 3.2 Update failed, degraded to refresh the entire application
    if (["abort"."fail"].indexOf(status) >= 0) {
     log(
      "warning"."[HMR] Cannot apply update. Need to do a full reload!"
     );
     window.location.reload();
    } else {
     log("warning"."[HMR] Update failed: "+ log.formatError(err)); }}); };var hotEmitter = require("./emitter");
  // 1. Register event callback
 hotEmitter.on("webpackHotUpdate".function(currentHash) {
  lastHash = currentHash;
  if(! upToDate() &&module.hot.status() === "idle") {
   log("info"."[HMR] Checking for updates on the server..."); check(); }});Copy the code

(1) When hot: True is set, the client will introduce Webpack/Hot/Emitter, which will trigger a webpackHotUpdate event, passing the hash value. If hot: true is not set, the change will trigger liveReload and refresh the entire page. The check method also checks for updates and, if the update fails, refreshes the entire page to refresh the code. (2) Module update dependency judgment: Module.hot. check is implemented in: Webpack/lib/HotModuleReplacement. Runtime. Js, is webpack built-in HotModuleReplacementPlugin injection in webpack bootstrap the runtime. With this plug-in, each incremental compilation produces two more files, such as: Json, main.3jsbdjh223DF68.hot-update. js, the former represents the JSON list of files that need to be updated, and the latter represents the updated thunk file. The browser then calls the hotDownloadManifest method to download manifest.json, which represents the list of modules that need to be updated. Then download the thunk to be updated in jsonp mode via hotDownloadUpdateChunk(chunkId). When the download is complete, the webpackHotUpdate callback is executed. If you get the updated module in the callback, it will determine whether the module agrees to the change or not. If not, the entire page will be refreshed to force the update. If the change is agreed, the new module replaces the old one. Once the replacement is successful, a callback is executed to update it.

  • module.hot.accept
function hotCreateModule(moduleId) {
  var hot = {
    // private stuff
    _acceptedDependencies: {},
    _declinedDependencies: {},
    _selfAccepted: false._selfDeclined: false._disposeHandlers: []._main: hotCurrentChildModule ! == moduleId,// Module API
    active: true.accept: function(dep, callback) {
        if (dep === undefined) hot._selfAccepted = true;
        else if (typeof dep === "function") hot._selfAccepted = dep;
        else if (typeof dep === "object")
            for (var i = 0; i < dep.length; i++)
                hot._acceptedDependencies[dep[i]] = callback || function() {};
        else hot._acceptedDependencies[dep] = callback || function() {};
    },


    // Management API
    check: hotCheck,
    apply: hotApply,
    / /...
  };
  hotCurrentChildModule = undefined;
  return hot;
}

if(module.hot) {
    module.hot.accept('./ module URL '.function() {
        rerender()
    })
}
Copy the code

For example, there is component ABC, where component A is applied to components B and C respectively. When component A changes, it is not allowed if only component B changes. Because component C also references component A, the page is forced to refresh. The dependency path is used to determine whether a module needs to be updated. And the consent of the above mentioned, represent the ancestors of the module module invokes the module. The hot, accept, and this property from the HotModuleReplacement. Runtime. The generated js module hotCreateModule method.

// HotModuleReplacement.runtime.js
function hotCheck(apply) {
  } h: "ac69EE760bb48d5db5f5 "}
  return hotDownloadManifest(hotRequestTimeout).then(function(update) {
    // The file needs to be updated
    hotAvailableFilesMap = update.c;
    // Updates the hash value for the next hot update
    hotUpdateNewHash = update.h;
    // The system enters the hot update state
    hotSetStatus("prepare");
    
    // 2. Generate a defered Promise to be consumed by the aforementioned promise chain
    var promise = new Promise(function(resolve, reject) {
      hotDeferred = {
        resolve: resolve,
        reject: reject
      };
    });

    hotUpdate = {};
    HotDownloadUpdateChunk is the chunk that initiates a JSONP request to update.
    // The jSONp callback is the HMR Runtime webpackHotUpdate
    {
      hotEnsureUpdateChunk(chunkId);
    }

    return promise;
  });
}
Copy the code
// HotModuleReplacement.runtime.js
function hotAddUpdateChunk(chunkId, moreModules) {
    if(! hotAvailableFilesMap[chunkId] || ! hotRequestedFilesMap[chunkId])return;
    hotRequestedFilesMap[chunkId] = false;
    for (var moduleId in moreModules) {
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { hotUpdate[moduleId] = moreModules[moduleId]; }}if (--hotWaitingFiles === 0 && hotChunksLoading === 0) { hotUpdateDownloaded(); }}Copy the code
// HotModuleReplacement.runtime.js
function hotUpdateDownloaded() {
    hotSetStatus("ready");
    var deferred = hotDeferred;
    hotDeferred = null;
    if(! deferred)return;
    if (hotApplyOnUpdate) {
        // Wrap deferred object in Promise to mark it as a well-handled Promise to
        // avoid triggering uncaught exception warning in Chrome.
        // See https://bugs.chromium.org/p/chromium/issues/detail?id=465666
        Promise.resolve()
            .then(function() {
                return hotApply(hotApplyOnUpdate);
            })
            .then(
                function(result) {
                    deferred.resolve(result);
                },
                function(err) { deferred.reject(err); }); }else {
        var outdatedModules = [];
        for (var id in hotUpdate) {
            if (Object.prototype.hasOwnProperty.call(hotUpdate, id)) { outdatedModules.push(toModuleId(id)); } } deferred.resolve(outdatedModules); }}Copy the code

The above code: hotCheck is used to get updated the chunk of communication and the server, after the download is complete execution webpack/lib/HotModuleReplacement runtime. HotAddUpdateChunk in js. HotAddUpdateChunk assigns values of the moreModules to be updated to the global full hotUpdate. The hotUpdateDownloaded function is to call hotApply to replace the code.

hotApply

  • Old code deleted
function getAffectedStuff(updateModuleId) {
  var outdatedModules = [updateModuleId];
  var outdatedDependencies = {};

  var queue = outdatedModules.map(function(id) {
    return {
      chain: [id],
      id: id
    };
  });
  // 1. Traverse the queue
  while (queue.length > 0) {
    var queueItem = queue.pop();
    var moduleId = queueItem.id;
    var chain = queueItem.chain;
    // 2. Find the old version of the modified module
    module = installedModules[moduleId];

    // 3. If it reaches the root module, return unaccepted
    if (module.hot._main) {
      return {
        type: "unaccepted".chain: chain,
        moduleId: moduleId
      };
    }
    // 4. Iterate through the parent module
    for (var i = 0; i < module.parents.length; i++) {
      var parentId = module.parents[i];
      var parent = installedModules[parentId];

      // 5. If the parent module handles module changes, skip it and continue checking
      if (parent.hot._acceptedDependencies[moduleId]) {
        continue;
      }
      outdatedModules.push(parentId);
      // 6. If not skipped, push to queue and continue checking
      queue.push({
        chain: chain.concat([parentId]),
        id: parentId }); }}// 7. If all dependent paths are accepted, return accepted
  return {
    type: "accepted".moduleId: updateModuleId,
    outdatedModules: outdatedModules,
    outdatedDependencies: outdatedDependencies
  };
}

// ...

var queue = outdatedModules.slice();
while (queue.length > 0) {
    moduleId = queue.pop();
    
    // Delete the old module
    delete installedModules[moduleId];
}
Copy the code

Code like this:

  • Replace new modules
// if (doApply) { appliedUpdate[moduleId] = hotUpdate[moduleId]; addAllToSet(outdatedModules, result.outdatedModules); for (moduleId in result.outdatedDependencies) { if ( Object.prototype.hasOwnProperty.call( result.outdatedDependencies, moduleId ) ) { if (! outdatedDependencies[moduleId]) outdatedDependencies[moduleId] = []; addAllToSet( outdatedDependencies[moduleId], result.outdatedDependencies[moduleId] ); }}} / / replace a new module for (moduleId in appliedUpdate) {if (Object. The prototype. The hasOwnProperty. Call (appliedUpdate, moduleId)) { modules[moduleId] = appliedUpdate[moduleId]; }}Copy the code
  • Associated code module
for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
    var item = outdatedSelfAcceptedModules[i];
    moduleId = item.module;
    hotCurrentParents = [moduleId];
    try {
        $require$(moduleId);
    } catch (err) {
        if (typeof item.errorHandler === "function") {
            try {
                item.errorHandler(err);
            } catch (err2) {
                if (options.onErrored) {
                    options.onErrored({
                        type: "self-accept-error-handler-errored".moduleId: moduleId,
                        error: err2,
                        originalError: err
                    });
                }
                if(! options.ignoreErrored) {if(! error) error = err2; }if (!error) error = err;
            }
        } else {
            if (options.onErrored) {
                options.onErrored({
                    type: "self-accept-errored".moduleId: moduleId,
                    error: err
                });
            }
            if(! options.ignoreErrored) {if(! error) error = err; }}}}Copy the code

Webpack_require (moduleId) executes the code

conclusion

Webpack-dev-server can be used as a command-line tool, with core module dependencies on Webpack and Webpack-dev-middleware. Webapck-dev-server is responsible for starting an Express server to listen for client requests; Instantiate Webpack Compiler; Start the Webscoket server that pushes webpack build information The Webscoket client code and processing logic responsible for injecting bundle.js into and communicating with the server. Webapck-dev-middleware changes the outputFileSystem of webpack compiler to in-memory fileSystem; Start webPack Watch compilation; Handles requests for static resources from the browser and responds to webpack output to the file in memory.

After webpack compilation is complete, an OK message is broadcast to the client. After receiving the message, the client uses liveReload page-level refresh mode or hotReload module hot replacement according to whether hot mode is enabled. HotReload fails, in which case it degrades to page level refresh.

Enable the HOT mode, that is, enable the HMR plug-in. Hot mode will request the updated module to the server, and then go back to the parent module of the module to determine the dependent path. If each dependent path is configured with the business processing callback function required after the module update, it is in the accepted state. Otherwise, the page will be degraded and refreshed. After determining the accepted status, replace and delete the old cache module and parent-child dependent module, then execute the accept method callback function, execute the new module code, introduce the new module, and execute the business processing code.

reference

Mp.weixin.qq.com/s/3fxWbEK22…