图解webpack5热更新HRM

前置问题

  1. 本地文件改变,webpack是如何知道并且触发编译的?
  2. 浏览器是如何知道本地代码重新编译,并且迅速请求了新生成的文件的?
  3. webpack本地服务器是如何告知浏览器?
  4. 浏览器获得这些文件又是如何热更新的?热更新的流程是什么?

前置知识点

一. 代码改变时自动编译的几种方法

摘录自 webpack官方文档
1.webpack's Watch Mode
2.webpack-dev-server
3.webpack-dev-middleware

Watch Mode

{
  "scripts": {
    "watch": "webpack --watch",
    "build": "webpack"
  }
}

命令行增加watch指令,当我们改变文件时,webpack会自动编译改变的模块,但是我们得手动刷新浏览器才能看到变化。

webpack-dev-server

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  devServer: {
        static: './dist',
  },
  optimization: {
        runtimeChunk: 'single',// 多入口时配置
  }
};

package.json

{
  "scripts": {
      "watch": "webpack --watch",
      "start": "webpack serve --open",
      "build": "webpack"
  }
}

webpack.config.js配置devServer属性,命令行增加webpack serve --open指令,当我们改变文件时,webpack会自动编译改变的模块,并且自动刷新浏览器

webpack-dev-middleware

webpack-dev-middleware内置于webpack-dev-server,主要是用于监测代码文件变化,处理文件编译等流程,我们也可以将它单独拿出来进行其它场景的开发,比如结合express实现文件编译监听功能。
server.js

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

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

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(
  webpackDevMiddleware(compiler, {
        publicPath: config.output.publicPath,
  })
);

// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

package.json

{
  "scripts": {
        "watch": "webpack --watch",
        "start": "webpack serve --open",
        "server": "node server.js",
        "build": "webpack"
  }
}

二. 入口调试

package.json

1.在Server.js中设置debugger断点
2.在package.json中设置node --inpect-brk的自动断点
3.运行命令后会自动跳转到Server.js

"scripts": {
  "dev-Server": "node --inspect-brk ./node_modules/webpack-dev-server/bin/webpack-dev-server.js",
  "dev-client": "webpack-dev-server",
},

本地客户端和服务端debugger相关文件说明

客户端入口文件entry

1.编译形成index.html文件入口
2.在index.html入口文件中编译形成index.js,将webpack-dev-server/client/index.jswebpack/hot/dev-server.js注入到index.js中,进行webSocket的建立和监听,实时进行热更新操作
3.调试时使用客户端的链接http://localhost:8080/

入口entry注入:
// 1. node_modules/webpack-dev-server/client/index.js
// 2. node_modules/webpack/hot/dev-server.js
服务端启动

1.监听文件变化,进行编译
2.将编译结果发送给客户端进行数据的更新操作
3.调试时使用 node --inspect-brk ./node_modules/webpack-dev-server/bin/webpack-dev-server.js

编译过程中启动的JS文件:
// 3. node_modules/webpack-dev-server/lib/Server.js
// 4. node_modules/webpack-dev-server/lib/servers/WebsocketServer.js

源码分析流程

一. 热更新总体流程图解

1. 整体流程图

2. 流程图文字分析

初始化:本地服务器和客户端初始化
  1. Server:new Server()后会直接调用server.start(),进行服务的启动
  2. Client:初始化过程中注入webpack-dev-server/client/index.jswebpack/hot/dev-server.js到入口文件中
  3. Server:在Server.js中,进行webpack-dev-middleware插件的注册,触发编译以及文件变化的监听
  4. Server:使用express开启本地node服务器
  5. Server和Client:创建WebSocket

    /**
     * 省去细节代码,只保留(核心代码 || 要重点分析的代码)
     */
    class Server {
       async start() {
           await this.normalizeOptions();
           await this.initialize();
    
           if (this.options.webSocketServer) {
               this.createWebSocketServer();
           }
       }
    
       async normalizeOptions() {
           const { options } = this;
           options.client.webSocketURL = {
               protocol: parsedURL.protocol,
               hostname: parsedURL.hostname,
               port: parsedURL.port.length > 0 ? Number(parsedURL.port) : "",
               pathname: parsedURL.pathname,
               username: parsedURL.username,
               password: parsedURL.password,
           };
    
    
           const defaultWebSocketServerOptions = { path: "/ws" };
    
           if (typeof options.webSocketServer === "undefined") {
               options.webSocketServer = {
                   type: defaultWebSocketServerType,
                   options: defaultWebSocketServerOptions,
               };
           }
       }
    
       initialize() {
           compilers.forEach((compiler) => {
               this.addAdditionalEntries(compiler);
    
               if (this.options.hot) {
                   // Apply the HMR plugin
                   const plugin = new webpack.HotModuleReplacementPlugin();
                   plugin.apply(compiler);
               }
           });
    
           this.setupApp();
           this.setupDevMiddleware();
           this.setupMiddlewares();
           this.createServer();
       }
    
       addAdditionalEntries(compiler) {
           let additionalEntries = [];
           if (this.options.webSocketServer) {
    
               additionalEntries.push(
                   `${require.resolve("../client/index.js")}?${webSocketURLStr}`
               );
           }
    
           if (this.options.hot === "only") {
               additionalEntries.push(require.resolve("webpack/hot/only-dev-server"));
           } else if (this.options.hot) {
               additionalEntries.push(require.resolve("webpack/hot/dev-server"));
           }
    
           if (typeof webpack.EntryPlugin !== "undefined") {
               // node_modules/webpack-dev-server/client/index.js?protocol=ws%3A&hostname=0.0.0.0&port=9000&pathname=%2Fws&logging=info&overlay=true&reconnect=10&hot=true&live-reload=true
               // node_modules/webpack/hot/dev-server.js
               for (const additionalEntry of additionalEntries) {
                   new webpack.EntryPlugin(compiler.context, additionalEntry, {
                       name: undefined,
                   }).apply(compiler);
               }
           }
       }
    
       setupDevMiddleware() {
           const webpackDevMiddleware = require("webpack-dev-middleware");
    
           // middleware for serving webpack bundle
           this.middleware = webpackDevMiddleware(
               this.compiler,
               this.options.devMiddleware
           );
       }
    
       setupApp() {
           this.app = new express();
       }
    
       setupMiddlewares() {
           let middlewares = [];
    
           middlewares.push({
               name: "webpack-dev-middleware",
               middleware: this.middleware
           });
    
           // middlewares: Array(6)
           // 0:{ name: 'compression', middleware: ƒ }
           // 1:{ name: 'webpack-dev-middleware', middleware: ƒ }
           // 2:{ name: 'express-static', path: '/', middleware: ƒ }
           // 3:{ name: 'serve-index', path: '/', middleware: ƒ }
           // 4:{ name: 'serve-magic-html', middleware: ƒ }
           // 5:{ name: 'options-middleware', path: '*', middleware: ƒ }
           middlewares.forEach((middleware) => {
               if (typeof middleware === "function") {
                   (this.app).use(middleware);
               } else if (typeof middleware.path !== "undefined") {
                   (this.app).use(middleware.path, middleware.middleware);
               } else {
                   (this.app).use(middleware.middleware);
               }
           });
       }
    
       createServer() {
           this.server = require("http").createServer(
               options,
               this.app
           );
    
           this.server.on("connection", (socket) => {
               // Add socket to list
               this.sockets.push(socket);
           });
       }
    
       createWebSocketServer() {
           this.webSocketServer = new (this.getServerTransport())(this); // this.webSocketServer = new WebsocketServer(this);
    
           if (this.options.hot === true || this.options.hot === "only") {
               this.sendMessage([client], "hot");
           }
           if (this.options.liveReload) {
               this.sendMessage([client], "liveReload");
           }
           this.sendStats([client], this.getStats(this.stats), true);
       }
    
       getServerTransport() {
           let implementation;
           if (this.options.webSocketServer.type === "ws") {
               implementation = require("./servers/WebsocketServer");
           }
           return implementation;
       }
    }
    初始化文件变化Watching.js管理类,并且触发编译

    Server.js在注册webpack-dev-middleware的时候,进行Watching.js的初始化,并且触发第一次编译

    // node_modules/webpack-dev-server/lib/Server.js
    setupDevMiddleware() {
       const webpackDevMiddleware = require("webpack-dev-middleware");
    
       // middleware for serving webpack bundle
       this.middleware = webpackDevMiddleware(
           this.compiler,
           this.options.devMiddleware
       );
    }
    
    // node_modules/webpack-dev-middleware/dist/index.js
    function wdm() {
       const context = { compiler };
    
       setupOutputFileSystem(context); // Start watching
    
       context.compiler.watch(watchOptions, errorHandler);
    }
    
    
    // node_modules/webpack/lib/Compiler.js
    watch(watchOptions, handler) {
       this.watching = new Watching(this, watchOptions, handler);
       return this.watching;
    }
    
    
    // node_modules/webpack/lib/Watching.js
    // Watching.js的constructor()->_invalidate()->_go()
    _go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles) {
       const run = () => {
           this.compiler.compile(onCompiled);
       };
       run();
    }
    
    
    // node_modules/Complier.js
    compile(callback) {
       this.hooks.make.callAsync(compilation, err => {}); //触发编译
    }
    本地服务端-文件变化运行逻辑

    Server.jsWatching.js注册了文件内存系统的监听,文件发生变化时,会触发重新编译

    // node_modules/webpack/lib/webpack.js
    new NodeEnvironmentPlugin({
         infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    
    
    // node_modules/webpack/lib/node/NodeEnvironmentPlugin.js
    compiler.watchFileSystem = new NodeWatchFileSystem(
     compiler.inputFileSystem
    );
    
    
    // node_modules/webpack/lib/Watching.js
    // 第一次会主动触发this._go()进行编译,每次编译结束时注册监听
    _done(err, compilation) {
     //...
     this.watch(
         compilation.fileDependencies,
         compilation.contextDependencies,
         compilation.missingDependencies
     );
     //...
    }
    watch(files, dirs, missing) {
     this.watcher = this.compiler.watchFileSystem.watch(...args, () => {
         this._invalidate(
             fileTimeInfoEntries,
             contextTimeInfoEntries,
             changedFiles,
             removedFiles
         );
         this._onChange();
     });
    }
    _invalidate() {
     this._go(...args);
    }
    _go(fileTimeInfoEntries, contextTimeInfoEntries, changedFiles, removedFiles) {
     const run = () => {
         this.compiler.compile(onCompiled);
     };
     run();
    }
    
    
    // node_modules/Complier.js
    compile(callback) {
     this.hooks.make.callAsync(compilation, err => { }); //触发编译
    }
    本地服务端-通知客户端

    1.监听Webpack编译完成后,主动触发sendStats方法
    2.本地服务端的webSocket主动发送hash命令和ok命令到本地浏览器client端

    class Server {
     setupHooks() {
         // 初始化时注册done的监听事件,编译完成后,调用sendStats方法进行webSocket的命令发送
         this.compiler.hooks.done.tap(
             "webpack-dev-server",
             (stats) => {
                 if (this.webSocketServer) {
                     this.sendStats(this.webSocketServer.clients, this.getStats(stats));
                 }
                 this.stats = stats;
             }
         );
     }
    
     sendStats(clients, stats, force) {
         // 更新当前的hash
         this.currentHash = stats.hash;
       
         // 发送给客户端当前的hash值
         this.sendMessage(clients, "hash", stats.hash);
    
         // 发送给客户端ok的指令
         this.sendMessage(clients, "ok");
     }
    }
    客户端-接收到服务端发来的WebSocket消息

    1.收到type=hashtype=ok两条消息
    图解webpack5热更新HRM_第1张图片

    2.type=hash更新了当前的currentHash
    3.type=ok触发了reloadApp()方法的执行

    var onSocketMessage = {
      hash: function hash(_hash) {
         status.previousHash = status.currentHash;
         status.currentHash = _hash;
      },
      ok: function ok() {
         sendMessage("Ok");
    
         reloadApp(options, status);
      },
    };
    var socketURL = createSocketURL(parsedResourceQuery);
    socket(socketURL, onSocketMessage, options.reconnect);
    
    function reloadApp(_ref, status) {
     function applyReload(rootWindow, intervalId) {
         rootWindow.location.reload();
     }
    
     var search = self.location.search.toLowerCase();
     var allowToHot = search.indexOf("webpack-dev-server-hot=false") === -1;
     var allowToLiveReload = search.indexOf("webpack-dev-server-live-reload=false") === -1;
    
     if (hot && allowToHot) {
         log.info("App hot update...");
         hotEmitter.emit("webpackHotUpdate", status.currentHash);
     }
     else if (liveReload && allowToLiveReload) {
         // 根据条件判断执行applyReload()方法
     }
    }
    客户端-hotEmitter.emit("webpackHotUpdate", status.currentHash)

    1.webpack/hot/dev-server.js接收到hotEmitter的消息后,进行check()方法的调用
    2.module.hot.check(true)触发,然后判断是否需要重启

    var check = function check() {
     module.hot
         .check(true)
         .then(function (updatedModules) {
             if (!updatedModules) {
                 log("warning", "[HMR] Cannot find update. Need to do a full reload!");
                 window.location.reload();
                 return;
             }
         });
    };
    var hotEmitter = require("./emitter");
    hotEmitter.on("webpackHotUpdate", function (currentHash) {
     lastHash = currentHash;
    
     check();
    });
    客户端-module.hot.check
    /node_modules/webpack/lib/hmr/HotModuleReplacement.runtime.js
    在编译形成最终代码时,会注入HotModuleReplacement.runtime.js代码,拦截require,进行createRequirecreateModuleHotObject
    createRequire

    构建当前requestparentchildren,本质是在require的基础上保存各个模块之间的依赖关系,为后面的热更新做准备,因为一个文件的更新必定涉及到另外依赖模块的相关更新

    createModuleHotObject

    构建当前modulehotAPI,后面的热更新都需要通过hotCheckhotApply进行操作

    function __webpack_require__(moduleId) {
       // ...... 省略代码 ......
    
       var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
       __webpack_require__.i.forEach(function (handler) { handler(execOptions); });
    
       // ...... 省略代码 ......
    
       return module.exports;
    }
    
    
    __webpack_require__.i.push(function (options) {
       var module = options.module;
       var require = createRequire(options.require, options.id);
       module.hot = createModuleHotObject(options.id, module);
       module.parents = currentParents;
       module.children = [];
       currentParents = [];
       options.require = require;
    });
    
    
    function createRequire(require, moduleId) {
       var me = installedModules[moduleId];
       // ...... 省略代码 ......
       var fn = function (request) {
           if (me.hot.active) {
               if (installedModules[request]) {
                   var parents = installedModules[request].parents;
                   if (parents.indexOf(moduleId) === -1) {
                       parents.push(moduleId);
                   }
               } else {
                   currentParents = [moduleId];
                   currentChildModule = request;
               }
               if (me.children.indexOf(request) === -1) {
                   me.children.push(request);
               }
           } else {
               currentParents = [];
           }
           return require(request);
       };
       // ...... 省略代码 ......
       return fn;
    }
    
    function createModuleHotObject(moduleId, me) {
     var hot = {
         // ...... 省略代码 ......
         active: true,
         accept: function (dep, callback, errorHandler) {
             // ...... 省略代码 ......
         },
         // ...... 省略代码 ......
         check: hotCheck,
         apply: hotApply,
         // ...... 省略代码 ......
         data: currentModuleData[moduleId]
     };
     currentChildModule = undefined;
     return hot;
    }
    hotCheck

    1.module.hot.check最终会触发hotCheck()方法
    2.__webpack_require__.hmrM:先使用旧的hash值进行hot-update.json文件的请求,得到update = {c:["main"], m:[], r:[]} 的更新内容

    function hotCheck(applyOnUpdate) {
    
       return setStatus("check")
           .then(__webpack_require__.hmrM) // 为fetch("http://localhost:8080/main.fc1c69066ce336693703.hot-update.json")
           .then(function (update) {
              // update = {c:["main"], m:[], r:[]} 更新内容
    
               return setStatus("prepare").then(function () {
                   var updatedModules = [];
                   currentUpdateApplyHandlers = [];
    
                   return Promise.all(
                       Object.keys(__webpack_require__.hmrC).reduce(function (
                           promises,
                           key
                       ) {
                           // key=jsonp
                           // __webpack_require__.hmrC[key](
                           //     update.c,
                           //     update.r,
                           //     update.m,
                           //     promises,
                           //     currentUpdateApplyHandlers,
                           //     updatedModules
                           // ); ===> 转化为jsonp,便于理解
                           __webpack_require__.hmrC.jsonp(update.c, update.r, update.m, promises, currentUpdateApplyHandlers, updatedModules);
                           // chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList
                           return promises;
                       },
                           [])
                   ).then(function () {
                       return waitForBlockingPromises(function () { // 等待所有的promise更新完成
                           if (applyOnUpdate) {
                               // hotCheck(true)
                               return internalApply(applyOnUpdate);
                           } else {
                               return setStatus("ready").then(function () {
                                   return updatedModules;
                               });
                           }
                       });
                   });
               });
           });
    }
    
    __webpack_require__.hmrM = () => {
     if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");
    
     // 保留的是client客户端的域名:
     // __webpack_require__.p = "http://localhost:8080/"
    
     // 保留的是上一次的hash值:
     // __webpack_require__.h = () => ("fc1c69066ce336693703")
    
     // __webpack_require__.hmrF = () => ("main." + __webpack_require__.h() + ".hot-update.json"); 
     // fetch("http://localhost:8080/main.fc1c69066ce336693703.hot-update.json")
     return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) => {
         return response.json();
     });
    };

    3.hot-update.json回调完成后,触发__webpack_require__.hmrC.jsonp()方法执行:
    (1) 创建http://localhost:8080/main.f1bcf354bbddd26daa90.hot-update.js的promise请求,并且加入到promise数组中
    (2) 创建对应的全局执行函数,等待main.xxx.hot-update.js回调后,执行对应的module代码的缓存并且触发对应promiseresolve请求,从而顺利回调internalApply()方法

    // $hmrDownloadUpdateHandlers$.$key$ => runtime转化为: __webpack_require__.hmrC.jsonp 
    __webpack_require__.hmrC.jsonp = function (chunkIds, ...) {
     applyHandlers.push(applyHandler);
    
     chunkIds.forEach(function (chunkId) {
         // 拼接jsonp请求的url
         promises.push($loadUpdateChunk$(chunkId, updatedModulesList));
     });
    };
    
    // 拼接jsonp请求的url
    var waitingUpdateResolves = {};
    function loadUpdateChunk(chunkId, updatedModulesList) {
     return new Promise((resolve, reject) => {
    
       waitingUpdateResolves[chunkId] = resolve;
       
         // __webpack_require__.hu = "" + chunkId + "." + __webpack_require__.h() + ".hot-update.js";
         var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
         __webpack_require__.l(url, loadingEnded);
     });
    }
    
    // document.body.appendChild(new Script()),正式发起get请求(jsonp请求)
    var inProgress = {};
    __webpack_require__.l = (url, done, key, chunkId) => {
     inProgress[url] = [done];
     var onScriptComplete = (prev, event) => {
         var doneFns = inProgress[url];
         delete inProgress[url];
         script.parentNode && script.parentNode.removeChild(script);
         doneFns && doneFns.forEach((fn) => (fn(event)));
     };
     script.onload = onScriptComplete.bind(null, script.onload);
     needAttach && document.head.appendChild(script);
    };
    
    // 返回的http://localhost:8080/main.f1bcf354bbddd26daa90.hot-update.js是一个webpackHotUpdatewebpack_inspect马上执行的函数
    // 如下图所示
    self["webpackHotUpdatewebpack_inspect"] = (chunkId, moreModules, runtime) => {
     for (var moduleId in moreModules) {
         if (__webpack_require__.o(moreModules, moduleId)) {
             currentUpdate[moduleId] = moreModules[moduleId];
             if (currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
         }
     }
     if (runtime) currentUpdateRuntime.push(runtime);
     if (waitingUpdateResolves[chunkId]) {
         waitingUpdateResolves[chunkId]();
         waitingUpdateResolves[chunkId] = undefined;
     }
    };

    图解webpack5热更新HRM_第2张图片

    客户端-module.hot.apply

    1.处理所有涉及模块的热更新策略,有的是当依赖的模块发生更新后,这个模块需要通过重新加载去完成本模块的全量更新,有的是部分热更新,有的是不更新
    2.进行需要update的模块的热更新处理
    3.进行需要delete的模块的热更新处理

    hotApply数组遍历处理
    function internalApply(options) {
     // 这里的currentUpdateApplyHandlers存储的是上面jsonp请求js文件所创建的callback
     var results = currentUpdateApplyHandlers.map(function (handler) {
         return handler(options);
     });
     currentUpdateApplyHandlers = undefined;
    
     results.forEach(function (result) {
       if (result.dispose) result.dispose();
     });
    
     var outdatedModules = [];
     results.forEach(function (result) {
         if (result.apply) {
             // 这里的result的是上面jsonp请求js文件所创建的callback所返回Object的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 () {
    
         return setStatus("idle").then(function () {
             return outdatedModules;
         });
     });
    }
    applyHandler-实际的hotApply处理逻辑
    applyHandler()方法位于/node_modules/webpack/lib/hmr/JavascriptHotModuleReplacement.runtime.js
    总体执行逻辑概括

    1.根据webpack配置拼接出当前moduleId的热更新策略,比如允许热更新,比如不允许热更新等等
    2.根据热更新策略,拼接多个数据结构,为applay()方法代码服务
    3.从internalApply()可以知道,最终会先执行result.dispose(),然后再执行result.apply()方法,
    4.dispose()方法主要执行的逻辑是:
    (1)删除缓存数据
    (2)移除之前注册的回调函数
    (3)移除目前module与其它module的绑定关系(parent和children)
    5.apply()方法主要执行的逻辑是:
    (1)更新全局的window.__webpack_require__对象,存储了所有路径+内容的对象
    (2)执行runtime代码,比如_webpack_require__.h = ()=> {"xxxxxhash值"}
    (3)触发之前hot.accept部署了依赖变化时的回调callBack
    (4)重新加载标识_selfAccepted的module,这种模块会重新require一次

    第一个步骤-1:拼接数据结构

    1.根据getAffectedModuleEffects(moduleId)整理出该moduleId的热更新策略,是否需要热更新
    2.根据多个对象拼凑出disposeapply方法所需要的数据结构

    function applyHandler(options) {
       currentUpdateChunks = undefined;
    
       // at begin all updates modules are outdated
       // the "outdated" status can propagate to parents if they don't accept the children
       var outdatedDependencies = {}; // 使用module.hot.accept部署了依赖发生更新后的回调函数
       var outdatedModules = []; // 当前过期需要更新的modules
       var appliedUpdate = {}; // 准备更新的modules
    
    
       for (var moduleId in currentUpdate) {
           var newModuleFactory = currentUpdate[moduleId];
    
           // 获取之前的配置:该moduleId是否允许热更新
           var result = getAffectedModuleEffects(moduleId);
    
           var doApply = false;
           var doDispose = false;
    
           switch (result.type) {
               // ...
               case "accepted":
                   if (options.onAccepted) options.onAccepted(result);
                   doApply = true;
                   break;
               //...
           }
           if (doApply) {
               appliedUpdate[moduleId] = newModuleFactory;
               //...代码省略... 拼凑出outdatedDependencies过期的依赖,为下面的module.hot.accept(moduleId, function() {})做准备
           }
           if (doDispose) {
               //...代码省略... 处理配置为dispose的情况
           }
    
       }
       currentUpdate = undefined;
    
       // 根据outdatedModules拼凑出需要_selfAccepted=true,即热更新是重新加载一次自己的module的数据到outdatedSelfAcceptedModules中
       var outdatedSelfAcceptedModules = [];
       for (var j = 0; j < outdatedModules.length; j++) {
           var outdatedModuleId = outdatedModules[j];
           // __webpack_require__.c = __webpack_module_cache__
           var module = __webpack_require__.c[outdatedModuleId];
           if (module && (module.hot._selfAccepted || module.hot._main) &&
               appliedUpdate[outdatedModuleId] !== warnUnexpectedRequire &&
               !module.hot._selfInvalidated
           ) {
               // _requireSelf: function () {
               //      currentParents = me.parents.slice();
               //      currentChildModule = _main ? undefined : moduleId;
               //         __webpack_require__(moduleId);
               // },
               outdatedSelfAcceptedModules.push({
                   module: outdatedModuleId,
                   require: module.hot._requireSelf, // 重新加载自己
                   errorHandler: module.hot._selfAccepted
               });
           }
       }
    
       var moduleOutdatedDependencies;
    
       return {
           dispose: function() {...}
           apply: function(reportError) {...}
       };
    }
    第一个步骤-2:getAffectedModuleEffects方法讲解
    function getAffectedModuleEffects(updateModuleId) {
       var outdatedModules = [updateModuleId];
       var outdatedDependencies = {};
    
       var queue = outdatedModules.map(function (id) {
           return {
               chain: [id],
               id: id
           };
       });
       while (queue.length > 0) {
           var queueItem = queue.pop();
           var moduleId = queueItem.id;
           var chain = queueItem.chain;
           var module = __webpack_require__.c[moduleId];
           if (!module || (module.hot._selfAccepted && !module.hot._selfInvalidated)) { continue; }
    
           // ************ 处理不热更新的情况 ************
           if (module.hot._selfDeclined) {
               return {
                   type: "self-declined",
                   chain: chain,
                   moduleId: moduleId
               };
           }
           if (module.hot._main) {
               return {
                   type: "unaccepted",
                   chain: chain,
                   moduleId: moduleId
               };
           }
           // ************ 处理不热更新的情况 ************
    
           for (var i = 0; i < module.parents.length; i++) {
               // module.parents=依赖这个模块的modules
               // 遍历所有依赖这个模块的 modules
               var parentId = module.parents[i];
               var parent = __webpack_require__.c[parentId];
               if (!parent) continue;
               if (parent.hot._declinedDependencies[moduleId]) {
                   // 如果依赖这个模块的parentModule设置了不理会当前moduleId热更新的策略,则不处理该parentModule
                   return {
                       type: "declined",
                       chain: chain.concat([parentId]),
                       moduleId: moduleId,
                       parentId: parentId
                   };
               }
               // 如果已经包含在准备更新的队列中,则不重复添加
               if (outdatedModules.indexOf(parentId) !== -1) continue;
               if (parent.hot._acceptedDependencies[moduleId]) {
                   if (!outdatedDependencies[parentId])
                       outdatedDependencies[parentId] = [];
                   // TODO 这个parentModule设置了监听其依赖module的热更新
                   addAllToSet(outdatedDependencies[parentId], [moduleId]);
                   continue;
               }
               delete outdatedDependencies[parentId];
               outdatedModules.push(parentId); // 添加该parentModuleId到队列中,准备更新
    
               // 加入该parentModuleId到队列中,进行下一轮循环,把parentModule的相关parent也加入到更新中
               queue.push({
                   chain: chain.concat([parentId]),
                   id: parentId
               });
           }
       }
    
       return {
           type: "accepted",
           moduleId: updateModuleId,
           outdatedModules: outdatedModules,
           outdatedDependencies: outdatedDependencies
       };
    }
    
    function addAllToSet(a, b) {
       for (var i = 0; i < b.length; i++) {
           var item = b[i];
           if (a.indexOf(item) === -1) a.push(item);
       }
    }
    第二个步骤:dispose方法
    dispose: function () {
       currentUpdateRemovedChunks.forEach(function (chunkId) {
           delete installedChunks[chunkId];
       });
       currentUpdateRemovedChunks = undefined;
    
       var idx;
       var queue = outdatedModules.slice();
       while (queue.length > 0) {
           var moduleId = queue.pop();
           var module = __webpack_require__.c[moduleId];
           if (!module) continue;
    
           var data = {};
    
           // Call dispose handlers: 回调注册的disposeHandlers
           var disposeHandlers = module.hot._disposeHandlers;
           for (j = 0; j < disposeHandlers.length; j++) {
               disposeHandlers[j].call(null, data);
           }
           // __webpack_require__.hmrD = currentModuleData置为空
           __webpack_require__.hmrD[moduleId] = data;
    
           // disable module (this disables requires from this module)
           module.hot.active = false;
    
           // remove module from cache: 删除module的缓存数据
           delete __webpack_require__.c[moduleId];
    
           // when disposing there is no need to call dispose handler: 删除其它模块对该moduleId的accept回调
           delete outdatedDependencies[moduleId];
    
           // remove "parents" references from all children: 
           // 解除moduleId引用的其它模块跟moduleId的绑定关系,跟下面的解除关系是互相补充的
           // 一个是children,一个是parent
           for (j = 0; j < module.children.length; j++) {
               var child = __webpack_require__.c[module.children[j]];
               if (!child) continue;
               idx = child.parents.indexOf(moduleId);
               if (idx >= 0) {
                   child.parents.splice(idx, 1);
               }
           }
       }
    
       // remove outdated dependency from module children: 
       // 解除引用该moduleId的模块跟moduleId的绑定关系,可以理解为moduleId.parent删除children,跟上面的解除关系是互相补充的
       // 一个是children,一个是parent
       var dependency;
       for (var outdatedModuleId in outdatedDependencies) {
           module = __webpack_require__.c[outdatedModuleId];
           if (module) {
               moduleOutdatedDependencies =
                   outdatedDependencies[outdatedModuleId];
               for (j = 0; j < moduleOutdatedDependencies.length; j++) {
                   dependency = moduleOutdatedDependencies[j];
                   idx = module.children.indexOf(dependency);
                   if (idx >= 0) module.children.splice(idx, 1);
               }
           }
    
       }
    }
    第三个步骤:apply方法
    apply: function (reportError) {
       // insert new code
       for (var updateModuleId in appliedUpdate) {
           // __webpack_require__.m = __webpack_modules__
           // 更新全局的window.__webpack_require__对象,存储了所有路径+内容的对象
           __webpack_require__.m[updateModuleId] = appliedUpdate[updateModuleId];
       }
    
       // run new runtime modules
       // 执行runtime代码,比如_webpack_require__.h = ()=> {"xxxxxhash值"}
       for (var i = 0; i < currentUpdateRuntime.length; i++) {
           currentUpdateRuntime[i](__webpack_require__);
       }
    
       // call accept handlers:触发之前hot.accept部署了依赖变化时的回调callBack
       for (var outdatedModuleId in outdatedDependencies) {
           var module = __webpack_require__.c[outdatedModuleId];
           if (module) {
               moduleOutdatedDependencies =
                   outdatedDependencies[outdatedModuleId];
               var callbacks = [];
               var errorHandlers = [];
               var dependenciesForCallbacks = [];
               for (var j = 0; j < moduleOutdatedDependencies.length; j++) {
                   var dependency = moduleOutdatedDependencies[j];
                   var acceptCallback = module.hot._acceptedDependencies[dependency];
                   var errorHandler = module.hot._acceptedErrorHandlers[dependency];
                   if (acceptCallback) {
                       if (callbacks.indexOf(acceptCallback) !== -1) continue;
                       callbacks.push(acceptCallback);
                       errorHandlers.push(errorHandler);
                       dependenciesForCallbacks.push(dependency);
                   }
               }
               for (var k = 0; k < callbacks.length; k++) {
                   callbacks[k].call(null, moduleOutdatedDependencies);
               }
           }
       }
    
       // Load self accepted modules:重新加载标识_selfAccepted的module,这种模块会重新require一次
       for (var o = 0; o < outdatedSelfAcceptedModules.length; o++) {
           var item = outdatedSelfAcceptedModules[o];
           var moduleId = item.module;
    
           item.require(moduleId);
       }
    
       return outdatedModules;
      }

3. 概括总结

1.构建 bundle.js 的时候,加入一段 HMR runtime 的 js 和一段和本地服务沟通的WebSocket的相关js
2.文件修改会触发 webpack 重新构建,服务器通过向浏览器发送更新消息,浏览器通过 jsonp 拉取更新的模块文件,jsonp 回调触发模块热替换逻辑

二. 其它问题总结

1. hotApply是如何运行的?

1.先进行dipose()进行缓存数据的移除
2.然后再调用apply()进行数据的更新,以及对应注册的accept handler回调

如果没有在accept中写对应的业务代码,热更新后虽然代码已经变化,但是并不会引起已经更新的module.parent或者module.children的方法重新执行一遍,即不会重新从已经更新的module重新获取值

2. hotApply中outdatedModules、appliedUpdate、outdatedSelfAcceptedModules、moduleOutdatedDependencies有什么作用?

outdatedModules

需要更新的modules,比如你改变了test1.js,test2.js,那么这里的outDatedModules=["./test1.js", "./test2.js"]

outdatedSelfAcceptedModules
只有在module注册了accetp()这个方法,才能_selfAccepted=true
outdatedModules解析配置得到的_selfAccepted=truemodules
outdatedDependencies
只有在module.parent注册了accept("[当前的moduleId]", ()=> {})才有这层关系
存储的是key-value的对象,其中key代表的是当前要更新的module的parent,value代表当前的module
moduleOutdatedDependencies

outdatedDependenciesvalues,用于在dispose()apply()中进行短暂缓存数据使用

appliedUpdate

缓存moduleId的数据,如果是apply则缓存新的代码,如果是dipose模式,则缓存一个警告function

if (doApply) {
    appliedUpdate[moduleId] = newModuleFactory;
}
if (doDispose) {
    appliedUpdate[moduleId] = warnUnexpectedRequire;
}

3. 本地文件改变,webpack是如何知道并且触发编译的?

1.将打包内容放入内存中,初始化时触发compiler编译,compiler编译结束时进行文件系统的监听
2.如果文件发生变化,会触发重新编译的回调
3.编译完成后,重新注册监听

参考文章

  1. 轻松理解webpack热更新原理
  2. Webpack - HMR
  3. webpack5 之 HMR 原理探究
  4. webpack官方文档

你可能感兴趣的:(图解webpack5热更新HRM)