前端热更新-下篇

1,538 阅读3分钟

上一篇分享 中讲到热更新服务端都做了什么,总结如下

那热更新过程中浏览器都做了哪些,从上一篇分享中可以知道

  • xxx/node_modules/webpack-dev-server/client/index.js xxx/node_modules/webpack/hot/dev-server.js 被打包到前端代码中

  • 代码改变之后,webpack会通知浏览器有代码改变,然后浏览器拉取最新的代码

监听代码变更 onSocketMessage

webpack-dev-server/client/index.js

var socket = require('./socket');
var sendMessage = require('./utils/sendMessage');
var reloadApp = require('./utils/reloadApp');

![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3390035f94f041f29d538c8bc1522f53~tplv-k3u1fbpfcp-watermark.image)
var onSocketMessage = {
  invalid: function invalid() {
    sendMessage('Invalid');
  },
  hash: function hash(_hash) {
    status.currentHash = _hash;
  },
  ok: function ok() {
    reloadApp(options, status);
  },
};

// 启动socket服务, 注册hash,ok等消息类型的回调函数
socket(socketUrl, onSocketMessage);

是否触发热更新 reloadApp

作用:开启热更新配置,走热更新,如果没有开启,那么走浏览器重新刷新,如果项目嵌入在iframe中,那么重新刷新浏览器 webpack-dev-server/client/utils/reloadApp.js

function reloadApp(_ref, _ref2) {
  var hotReload = _ref.hotReload,
      hot = _ref.hot,
      liveReload = _ref.liveReload;
  var isUnloading = _ref2.isUnloading,
      currentHash = _ref2.currentHash;

  if (isUnloading || !hotReload) {
    return;
  }
  
  // 开启热更新
  if (hot) {
    [log.info](http://log.info/)('[WDS] App hot update...');
    var hotEmitter = require('webpack/hot/emitter');
    // 触发webpackHotUpdate事件
    hotEmitter.emit('webpackHotUpdate', currentHash);

    if (typeof self !== 'undefined' && self.window) {
      // broadcast update to window
      self.postMessage("webpackHotUpdate".concat(currentHash), '*');
    }
  } // allow refreshing the page only if liveReload isn't disabled
  else if (liveReload) {
  // 热加载,刷新浏览器
      var rootWindow = self; // use parent window for reload (in case we're in an iframe with no valid src)

      var intervalId = self.setInterval(function () {
        if (rootWindow.location.protocol !== 'about:') {
          // reload immediately if protocol is valid
          applyReload(rootWindow, intervalId);
        } else {
          rootWindow = rootWindow.parent;
          // 触发顶层刷新,如果项目是在iframe中
          if (rootWindow.parent === rootWindow) {
            // if parent equals current window we've reached the root which would continue forever, so trigger a reload anyways
            applyReload(rootWindow, intervalId);
          }
        }
      });
    }
  function applyReload(rootWindow, intervalId) {
    clearInterval(intervalId);
    [log.info](http://log.info/)('[WDS] App updated. Reloading...');
    rootWindow.location.reload();
  }
}

监听 webpackHotUpdate 事件,然后调用 module.hot.checkmodule.hot.apply 方法, 如果没有开始hmr的话,没有module.hot 属性,开启hmr后HotModuleReplacementPlugin这个插件在module上加上了hot属性


if (module.hot) {
        var lastHash;
        var upToDate = function upToDate() {
                return lastHash.indexOf(__webpack_hash__) >= 0;
        };
        var log = require("./log");
        var check = function check() {
                module.hot
                        .check()
                        .then(function(updatedModules) {
                              return module.hot.apply({})            
                        })
                        .catch(function(err) {
                        });
        };
        var hotEmitter = require("./emitter");
        hotEmitter.on("webpackHotUpdate", function(currentHash) {
                lastHash = currentHash;
                // 与上一次的hash不同
                if (!upToDate()) {
                        check()
                }
        });
} else {
        throw new Error("[HMR] Hot Module Replacement is disabled.");
}

module.hot.check

作用: 1 下载hot-update.json文件 2 下载hot-update.js文件 3 执行下载的hot-update.js文件 webpack/bootstrap

function hotCheck(apply) {
                 hotApplyOnUpdate = apply;
                 hotSetStatus("check");
                 // 下载json文件
                 return hotDownloadManifest(hotRequestTimeout).then(function(update) {
                         hotSetStatus("prepare");
                         var promise = new Promise(function(resolve, reject) {
                                 hotDeferred = {
                                         resolve: resolve,
                                         reject: reject
                                 };
                         });
                         hotUpdate = {};
                         for(var chunkId in installedChunks)
                         // eslint-disable-next-line no-lone-blocks
                         {
                                 // 下载改变后的js文件
                                 hotEnsureUpdateChunk(chunkId);
                         }

                 });
         }

hotEnsureUpdateChunk

 function hotEnsureUpdateChunk(chunkId) {
       // 下载js
       hotDownloadUpdateChunk(chunkId);      
 }
 
function hotDownloadUpdateChunk(chunkId) {
     // 通过script 标签加载更新后的js
     var script = document.createElement("script");
     script.charset = "utf-8";
     script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
     if (null) script.crossOrigin = null;
     document.head.appendChild(script);
}

执行下载后的hot-update.js 文件,调用 webpackHotUpdate 方法

webpackHotUpdate("main",{
"./src/App.js": (function(module, __webpack_exports__, __webpack_require__) {
    // 改变后的代码
}
})

webpackHotUpdate

 this["webpackHotUpdate"] = // eslint-disable-next-line no-unused-vars
 function webpackHotUpdateCallback(chunkId, moreModules) {
         hotAddUpdateChunk(chunkId, moreModules);
         if (parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules);
 };
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)) {
                     // 将要更新的moduleId 移到 hotUpdate
                     hotUpdate[moduleId] = moreModules[moduleId];
             }
     }
     if (--hotWaitingFiles === 0 && hotChunksLoading === 0) {
             hotUpdateDownloaded();
     }
}

hotUpdateDownloaded

 function hotUpdateDownloaded() {
         hotSetStatus("ready");
         // 这个deffered是在hotCheck中定义的
         var deferred = hotDeferred;
         hotDeferred = null;
         if (!deferred) return;
                 var outdatedModules = [];
                 for (var id in hotUpdate) {
                         if (Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
                                 outdatedModules.push(toModuleId(id));
                         }
                 }
                 // 跳出module.hot.check
                 deferred.resolve(outdatedModules);
         }
 }
 
 var check = function check() {
        module.hot
                .check()
                .then(function(updatedModules) {
                      return module.hot.apply({})            
                })
                .catch(function(err) {
                });
};

module.hot.apply

作用

  • 找出过期模块 outdatedModules 和过期依赖 outdatedDependencies
function hotApply() { 
  // ...
  var outdatedDependencies = {};
  var outdatedModules = [];
  // 这个方法会手机过期模块以及哪些模块依赖过期模块
  function getAffectedStuff(updateModuleId) {
    var outdatedModules = [updateModuleId];
    var outdatedDependencies = {};
    // ...
    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) < 0)
              a.push(item);
      }
  }
  // 这个hotUpdate:在 hotAddUpdateChunk 中给hotUpdate 赋值了
  for(var id in hotUpdate) {
      if(Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
          // ... 省略多余代码
          if(hotUpdate[id]) {
              result = getAffectedStuff(moduleId);
          }
          if(doApply) {
              for(moduleId in result.outdatedDependencies) {
                 // 添加到 outdatedDependencies
                  addAllToSet(outdatedDependencies[moduleId], result.outdatedDependencies[moduleId]);
              }
          }
          if(doDispose) {
              // 添加到 outdatedModules
              addAllToSet(outdatedModules, [result.moduleId]);
              appliedUpdate[moduleId] = warnUnexpectedRequire;
          }
      }
  }
}
  • 从缓存中删除过期模块、依赖和所有子元素的引用;

function hotApply() {
   // ...
    var idx;
    var queue = outdatedModules.slice();
    while(queue.length > 0) {
        moduleId = queue.pop();
        module = installedModules[moduleId];
        // ...
        // 移除缓存中的模块,这样就可以通过weback_require 重新执行代码了
        delete installedModules[moduleId];
        // 移除过期依赖中不需要使用的处理方法
        delete outdatedDependencies[moduleId];
        // 移除所有子元素的引用
        for(j = 0; j < module.children.length; j++) {
            var child = installedModules[module.children[j]];
            if(!child) continue;
            idx = child.parents.indexOf(moduleId);
            if(idx >= 0) {
                child.parents.splice(idx, 1);
            }
        }
    } 
  // 从模块子组件中删除过时的依赖项
  var dependency;
  var moduleOutdatedDependencies;
  for(moduleId in outdatedDependencies) {
   if(Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {
    module = installedModules[moduleId];
    if(module) {
     moduleOutdatedDependencies = outdatedDependencies[moduleId];
     for(j = 0; j < moduleOutdatedDependencies.length; j++) {
      dependency = moduleOutdatedDependencies[j];
      idx = module.children.indexOf(dependency);
      if(idx >= 0) module.children.splice(idx, 1);
     }
    }
   }
  }
  • 将新模块代码添加到 modules 中,当下次调用 __webpack_require__ (webpack 重写的 require 方法)方法的时候,就是获取到了新的模块代码了

function hotApply() {
   // ...
    for(moduleId in appliedUpdate) {
        if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
            modules[moduleId] = appliedUpdate[moduleId];
        }
    }
}

参考文档

【Webpack】627- 了不起的 Webpack HMR 学习指南(含源码分析)_pingan8787-CSDN博客