基于webpack的热重载live reload和热更新HMR(下)

本文涉及的产品
视频直播,500GB 1个月
简介: 在前端应用框架中不管是react还是vue,官方都提供了相应的脚手架方便开发者快速入手,当我们在开发时修改某个js或者css文件时,webpack会自动编译我们的文件,我们刷新浏览器就可以看到编译后的文件。为此我们会想,如果我们修改保存之后,文件被编译、浏览器自动刷新、或者浏览器局部刷新(不刷新整个浏览器),这样的话多好。当然,基于webpack打包工具的相关库已经实现了。下面对此部分流程做简单的分析

webpack 打包完事后,如何通知浏览器呢?如下webpack-dev-server Server.js文件


function Server(compiler, options) {
  // debugger
  // Default options
  if (!options) options = {};
  // webpack 配置中的属性,决定通过热更新的方式
  this.hot = options.hot || options.hotOnly;
  compiler.plugin('done', (stats) => {
    // 这里注册 webpack compiler 对象的事件, 通过websockets 通知客户端浏览器
    debugger
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
  });
  // Init express server
  const app = this.app = new express(); // eslint-disable-line
  app.all('*', (req, res, next) => { // eslint-disable-line
    if (this.checkHost(req.headers)) { return next(); }
    res.send('Invalid Host header');
  });
  // webpackDevMiddleware 监听文件的变换 watch -> build
  // middleware for serving webpack bundle
  this.middleware = webpackDevMiddleware(compiler, options);
  // ...
  this.listeningApp = http.createServer(app);
  // ...
}
// delegate listen call and init sockjs
Server.prototype.listen = function (port, hostname, fn) {
  this.listenHostname = hostname;
  // eslint-disable-next-line
  const returnValue = this.listeningApp.listen(port, hostname, (err) => {
    const sockServer = sockjs.createServer({
      // Use provided up-to-date sockjs-client
      sockjs_url: '/__webpack_dev_server__/sockjs.bundle.js',
      // Limit useless logs
      log(severity, line) {
        if (severity === 'error') {
          log(line);
        }
      }
    });
    sockServer.on('connection', (conn) => {
      if (!conn) return;
      if (!this.checkHost(conn.headers)) {
        this.sockWrite([conn], 'error', 'Invalid Host header');
        conn.close();
        return;
      }
      this.sockets.push(conn);
      conn.on('close', () => {
        const connIndex = this.sockets.indexOf(conn);
        if (connIndex >= 0) {
          this.sockets.splice(connIndex, 1);
        }
      });
      // 这里根据webpackConfig 中的配置 devServer.hot= true 通知客户端浏览 更新代码的方式
      if (this.hot) this.sockWrite([conn], 'hot');
      if (!this._stats) return;
      this._sendStats([conn], this._stats.toJson(clientStats), true);
    });
    if (fn) {
      fn.call(this.listeningApp, err);
    }
  });
  return returnValue;
};
Server.prototype.sockWrite = function (sockets, type, data) {
  sockets.forEach((sock) => {
    sock.write(JSON.stringify({
      type,
      data
    }));
  });
};
// send stats to a socket or multiple sockets
Server.prototype._sendStats = function (sockets, stats, force) {
  if (!force &&
  stats &&
  (!stats.errors || stats.errors.length === 0) &&
  stats.assets &&
  stats.assets.every(asset => !asset.emitted)
  ) { return this.sockWrite(sockets, 'still-ok'); }
  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'); }
};
module.exports = Server;


当客户端浏览器收到消息后 type: ok 消息类型发生时,流程如下: webpack打包后的部分代码


//webpack/hot/dev-server.js 也就是webpack 入口添加的文件
module.hot.check(true).then(function(updatedModules) {}).catch(function(updatedModules) {})
// 进入
function hotCheck(apply) {
  if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
  hotApplyOnUpdate = apply;
  hotSetStatus("check");
  return hotDownloadManifest().then(function(update) {
    // update.c标识对应的chunk是否发生了变化
    hotAvailableFilesMap = update.c;
    hotUpdateNewHash = update.h;
    hotSetStatus("prepare");
    var promise = new Promise(function(resolve, reject) {
    });
    // 开始请求 hot-update.json 文件 
     hotEnsureUpdateChunk(chunkId);
    return promise;
  });
}
//  请求之前webpack 生成的hot-update.json文件
function hotDownloadManifest() { // eslint-disable-line no-unused-vars
  return new Promise(function(resolve, reject) {
    if(typeof XMLHttpRequest === "undefined")
      return reject(new Error("No browser support"));
    try {
      var request = new XMLHttpRequest();
      var requestPath = __webpack_require__.p + "" + hotCurrentHash + ".hot-update.json";
      request.open("GET", requestPath, true);
      request.timeout = 10000;
      request.send(null);
    } catch(err) {
      return reject(err);
    }
    request.onreadystatechange = function() {
      if(request.readyState !== 4) return;
        // ...
        resolve(update);
      }
    };
  });
}
// 请求之前webpack 生成的hot-update.js 文件
function hotDownloadUpdateChunk(chunkId) { // eslint-disable-line no-unused-vars
  var head = document.getElementsByTagName("head")[0];
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.charset = "utf-8";
  script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js";
  head.appendChild(script);
}
// 请求的js文件执行如下代码
function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars
  hotAddUpdateChunk(chunkId, moreModules);
  if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules);
} ;
// 后续部分逻辑...
while(queue.length > 0) {
  moduleId = queue.pop();
  module = installedModules[moduleId];
  if(!module) continue;
  var data = {};
  // Call dispose handlers
  var disposeHandlers = module.hot._disposeHandlers;
  for(j = 0; j < disposeHandlers.length; j++) {
    cb = disposeHandlers[j];
    cb(data);
  }
  hotCurrentModuleData[moduleId] = data;
  // disable module (this disables requires from this module)
  module.hot.active = false;
  // 删除缓存
  // remove module from cache
  delete installedModules[moduleId];
  // remove "parents" references from all children
  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);
    }
  }
}
// 插入变化的模块
// insert new code
for(moduleId in appliedUpdate) {
  if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
    modules[moduleId] = appliedUpdate[moduleId];
  }
}
// 插入模块后, 重新执行js文件,这个过程浏览器是没有刷新的,可以通过浏览器Network看出
// Load self accepted modules
for(i = 0; i < outdatedSelfAcceptedModules.length; i++) {
  var item = outdatedSelfAcceptedModules[i];
  moduleId = item.module;
  hotCurrentParents = [moduleId];
  try {
    __webpack_require__(moduleId);
  } catch(err) {}
}
// __webpack_require__(moduleId);  再次进入 app.js 文件执行 =>
/* 37 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
__webpack_require__(71);
if (true) {
  module.hot.accept((err) => {
    if (err) {
      console.error('Cannot apply HMR update.', err);
    }
  });
}


最后再总结一下整个热更新HMR流程吧:


当我们修改文件并保存时,webpack-dev-server 通过 webpack-dev-middleware 能够拿到webpack打包过程的各个生命周期点, webpack打包过程通过HotModuleReplacementPlugin插件生成hot-update.js和hot-update.json文件,前者是变化的模块字符串信息,后者是本次打包之后module模块所对应的chunk信息以及打包后的hash值,决定客户端浏览器是否更新。 然后webpack-dev-server 通过 websockets把消息发送给客户端浏览器,浏览器收到消息后,分别请求这两个文件,后续为删除installedModules全局缓存对象,并重新赋值,再次执行对应的文件,这样就达到了在无刷新浏览器的条件下,更新变化的模块了,webpack更新模块的代码比较复杂,有的细节没有debug到,到此从Server 到 Client流程以及从Client 到 Server流程也就说清楚了


最后


涉及到的相关技术点有的没有提到,比如webpack的打包流程、webpack中检测文件变化的模块、webpack-dev-middleware相关、webpack-dev-server模块还有请求转发等功能没有说到,这个也不在讨论范围内,有兴趣的可以自己clone 代码查看,如果你对webpack打包流程 debug 过 相信再来了解这些东西会好很多 很多...


可能会有同学会说:看这些有什么作用,当然对我来说是当时的好奇心,通过了解大牛的代码实现,能学习到相关优秀的lib库、增强自己对代码的阅读能力。再有就是了解了一些底层再对其使用时,也能游刃有余。


参考:


1、zhuanlan.zhihu.com/p/30669007


2、fed.taobao.org/blog/taofed…


3、github.com/webpack/tap…  webpack 如何管理生命周期的核心库


4、astexplorer.net  对了解webpack 如何对代码进行ast分析对照很有用



相关文章
|
1月前
|
前端开发
配置 Webpack 实现热更新
【10月更文挑战第23天】还可以进一步深入探讨热更新的具体实现细节、不同场景下的应用案例,以及如何针对特定需求进行优化等方面的内容。通过全面、系统地了解 Webpack 热更新的配置方法,能够更好地利用这一功能,提升项目的开发效率和性能表现。同时,要不断关注 Webpack 及相关技术的发展动态,以便及时掌握最新的热更新技术和最佳实践。
|
1月前
|
缓存 监控 算法
提高 Webpack 热更新的性能
【10月更文挑战第23天】还可以进一步深入探讨热更新性能优化的具体案例、不同场景下的优化策略,以及与其他相关技术的结合应用等方面的内容。通过全面、系统地了解热更新性能优化的方法和技巧,能够更好地利用这一功能,为项目的成功开发提供有力保障。同时,要不断关注技术的发展动态,以便及时掌握最新的热更新技术和最佳实践。
|
1月前
|
监控 前端开发 JavaScript
Webpack 中 HMR 插件的工作原理
【10月更文挑战第23天】可以进一步深入探讨 HMR 工作原理的具体细节、不同场景下的应用案例,以及与其他相关技术的结合应用等方面的内容。通过全面、系统地了解 HMR 插件的工作原理,能够更好地利用这一功能,为项目的成功开发提供有力保障。同时,要不断关注技术的发展动态,以便及时掌握最新的 HMR 技术和最佳实践。
|
1月前
|
自然语言处理 前端开发 开发工具
webpack 热更新
【10月更文挑战第23天】Webpack 热更新是一项非常实用的技术,它为前端开发带来了极大的便利和效率提升。通过深入了解其原理和应用,开发者可以更好地利用热更新功能,提高开发质量和速度。
|
7月前
|
API 开发工具 开发者
webpack热更新原理
Webpack的Hot Module Replacement(HMR)提升开发效率,无需刷新页面即可更新模块。开启HMR需在配置中设`devServer.hot: true`。Webpack构建时插入HMR Runtime,通过WebSocket监听并处理文件变化。当模块改变,Webpack发送更新到浏览器,HMR Runtime找到对应模块进行热替换,保持应用状态。开发者可利用`module.hot` API处理热替换逻辑。
|
7月前
|
前端开发 测试技术 开发者
深入理解 Webpack 热更新原理:提升开发效率的关键
深入理解 Webpack 热更新原理:提升开发效率的关键
|
7月前
|
自然语言处理 JavaScript 前端开发
webpack 的热更新是如何做到的?原理是什么?
webpack 的热更新是如何做到的?原理是什么?
91 0
|
7月前
|
前端开发 JavaScript 算法
面试官:【webpack和vite的区别?vite一定比webpack快吗?vite的缺点是什么?webpack的热更新和vite的热更新区别?】
面试官:【webpack和vite的区别?vite一定比webpack快吗?vite的缺点是什么?webpack的热更新和vite的热更新区别?】
1397 0
|
资源调度 前端开发 JavaScript
配置多入口 Webpack 热更新失效? #120
配置多入口 Webpack 热更新失效? #120
180 0
|
移动开发 前端开发 JavaScript
Webpack 如何配置热更新 #96
Webpack 如何配置热更新 #96
260 0