# 深入webpack热更新

webpack热更新(简称HMR)给web开发者带来了极大的便利,它可以在修改代码后自动编译并替换修改的部分,还能保持当前页面状态,省去了不必要的页面刷新,节省了web开发者的时间。

# HMR的执行过程

首先webpack-dev-server启动本地服务,让浏览器可以请求本地静态资源。浏览器与本地服务建立websocket连接,webpack调用chokidar模块来监听文件变化,当用户修改文件后,chokidar通知webpack哪些文件发生了变化,webpack编译修改的文件,webpack-dev-server将重新编辑后的文件的hash发送给浏览器,浏览器以hash为参数发起请求获取更新的文件,获取文件后替换生效。

# HMR详细执行过程

首先webpack-dev-server启动本地服务

查看源码webpack-dev-server/lib/Services.js,发现实际上就是实例化了一个express

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

浏览器与本地服务建立一个websocket连接

浏览器端与本地服务建立通讯的代码哪来的?这样来的,webpack启用热更新,就会自动在入口文件插入浏览器端与本地服务通信的代码

webpack调用chokidar模块来监听文件变化

webpack内部会实例化一个编译器compilercompiler.watch内部依赖了chokidar模块来监听文件变化,chokidar那么又是根据什么来判断文件发生了变化?而且webpack执行在node环境中,为什么不直接用fs.watchfs.watchFile来监听文件变化?

实际上chokidar内部也依赖了fs.watchfs.watchFile来监听文件变化,但这不是它的全部,由于fs.watchfs.watchFile这两个API存在一些问题:

Nodejs fs.watch

  • 在MacOS上使用Sublime编辑文件,不会报告文件修改事件
  • 经常一次事件报告两次
  • 报告的大多数事件是rename,在window系统上删除文件也报rename
  • 没有提供简便的方法来递归监视文件树

Nodejs fs.watchFile

  • 事件处理几乎一样糟糕
  • 也没有提供任何递归监视
  • 轮询方式监视文件,导致高CPU占用

Nodejs官方文档上有说明:fs.watch的API在各个平台上并非100%一致,在某些情况下不可用,比如Docker上。

为了解决以上问题,chokidar将内部核心内容分为两块,分别是nodefs-handler.jsfsEvents-handler.js,macOS使用fsEvents-handler.js监听文件变化,其它系统用nodefs-handler.js,用户可以通过options.useFsEvents配置强制使用fsEvents-handler.js来监听文件变化。fsEvents-handler.js内部又依赖fsevents模块,fsevents集成了C++从系统底层来监听文件变化,一图胜千言

node_modules/chokidar/index.js核心代码:

// node_modules/chokidar/index.js

var NodeFsHandler = require('./lib/nodefs-handler');
var FsEventsHandler = require('./lib/fsevents-handler');

function importHandler(handler) {
  Object.keys(handler.prototype).forEach(function(method) {
    FSWatcher.prototype[method] = handler.prototype[method];
  });
}
// FSWatcher 继承了NodeFsHandler 和 FsEventsHandler的方法
importHandler(NodeFsHandler);
if (FsEventsHandler.canUse()) importHandler(FsEventsHandler);

// 增加文件监听
FSWatcher.prototype.add = function(paths) {
  // 判断用户配置以及系统是否支持fsevents,优先用fsevents监听文件变化
  if (this.options.useFsEvents && FsEventsHandler.canUse()) {
    // _addToFsEvents方法继承自FsEventsHandler
    paths.forEach(this._addToFsEvents, this);
  } else {
    asyncEach(paths, function(path, next) {
      // 否则还是用NodeFs监听文件
      // _addToNodeFs方法继承自NodeFsHandler
      this._addToNodeFs(path, !_internal, 0, 0, _origAdd, function(err, res) {
        if (res) this._emitReady();
        next(err, res);
      }.bind(this));
    }
  }
}

// Export FSWatcher class
exports.FSWatcher = FSWatcher;

浏览器以hash为参数发起请求获取更新的文件,获取文件后替换生效

webpack-dev-server启动之后通过websocket会发一个hash告诉浏览器,下次如果有热更新,你就发起一个[hash].hot-update.json的请求,我会返回更新的模块名称,以及再下次发起请求的hash,格式如下

let data = {
  c: {
    // 更新的模块是terminal
    terminal: true
  },
  // 下次再有模块更新,用这个hash发起请求
  h: "086d0e7074570fb43ad5"
}

然后,浏览器注入标签加载更新的chunk文件,文件内容是一个自动执行的全局函数webpackHotUpdatewebpackHotUpdate的参数是chunkId以及chunk包含的模块ID和内容

webpackHotUpdate([chunkId], moreModules)

webpackHotUpdate是webpack挂载在window上的全局函数,用来下载热更新模块(moreModules),moreModules是模块ID和模块内容的映射关系

{
  [moduleId]: (function (module, exports) {
    eval('代码模块内容')
  })
}

webpackHotUpdate挂在window对象上

window["webpackHotUpdate"] = function webpackHotUpdateCallback(chunkId, moreModules) {
  hotAddUpdateChunk(chunkId, moreModules);
}

function hotAddUpdateChunk(chunkId, moreModules) {
  // 遍历模块ID,保存到hotUpdate
  for (var moduleId in moreModules) {
    if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      hotUpdate[moduleId] = moreModules[moduleId];
    }
  }
  if (--hotWaitingFiles === 0 && hotChunksLoading === 0) {
    hotUpdateDownloaded();
  }
}

function hotUpdateDownloaded() {
  var outdatedModules = [];
  // 遍历hotUpdate,push到outdatedModules,用于删除过时的模块
  for (var id in hotUpdate) {
    outdatedModules.push(id);
  }
}

删除过时的模块,__webpack_require__[moduleId]更新代码

function hotApply(options){
  var queue = outdatedModules.slice();
  while (queue.length > 0) {
    // 删除缓存
    delete installedModules[moduleId];
    // 删除过时的依赖
    delete outdatedDependencies[moduleId];
  }
  // 保存模块ID,便于更新代码
  var outdatedSelfAcceptedModules = [];
	for (i = 0; i < outdatedModules.length; i++) {
    outdatedSelfAcceptedModules.push({
      module: moduleId
    });
  }

  for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
    var item = outdatedSelfAcceptedModules[i];
    moduleId = item.module;
    //更新代码
    __webpack_require__(moduleId);
  }
}

一图胜千言

HMR大概就是这样

上次更新: 10/23/2021, 10:33:11 PM