作者:王平安 转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q 了不起的 Webpack HMR 学习指南(上)「含源码讲解」 了不起的 Webpack HMR 学习指南(下)「含源码讲解」 本篇 当 hash 消息发送完成后,socket 还会发送一条 ok 的消息告知 Webpack-dev-server,由于客户端(Client)并不请求热更新代码,也不执行热更新模块操作,因此通过 emit 一个 "webpackHotUpdate" 消息,将工作转交回 Webpack。 Webpack/hot/dev-server 监听浏览器端 webpackHotUpdate 消息,将新模块 hash 值传到客户端 HMR 核心中枢的 HotModuleReplacement.runtime ,并调用 check 方法检测更新,判断是浏览器刷新还是模块热更新。如果是浏览器刷新的话,则没有后续步骤咯~~ 当 HotModuleReplacement.runtime 调用 check 方法时,会调用 JsonpMainTemplate.runtime 中的 hotDownloadUpdateChunk (获取最新模块代码)和 hotDownloadManifest (获取是否有更新文件)两个方法,这两个方法的源码,在下一步展开。 其中 hotEnsureUpdateChunk 方法中会调用 hotDownloadUpdateChunk : 在调用 check 方法时,会先调用 JsonpMainTemplate.runtime 中的 hotDownloadManifest 方法, 通过向服务端发起 AJAX 请求获取是否有更新文件,如果有的话将 mainfest 返回给浏览器端。 这边涉及一些原生 XMLHttpRequest,就不全部贴出了~ 在 hotDownloadManifest 方法中,还会执行 hotDownloadUpdateChunk 方法,通过 JSONP 请求最新的模块代码,并将代码返回给 HMR runtime 。 然后 HMR runtime 会将新代码进一步处理,判断是浏览器刷新还是模块热更新。 这一步是整个模块热更新(HMR)的核心步骤,通过 HMR runtime 的 hotApply 方法,移除过期模块和代码,并添加新的模块和代码实现热更新。 从 hotApply 方法可以看出,模块热替换主要分三个阶段: hotApply 方法执行之后,新代码已经替换旧代码,但是我们业务代码并不知道这些变化,因此需要通过 accept事件通知应用层使用新的模块进行“局部刷新”,我们在业务中是这么使用: 在热更新过程中,hotApply 过程中可能出现 abort 或者 fail 错误,则热更新退回到刷新浏览器(Browser Reload),整个模块热更新完成。 本文主要和大家分享 Webpack 的 HMR 使用和实现原理及源码分析,在源码分析中,通过一张“Webpack HMR 工作原理解析”图让大家对 HMR 整个工作流程有所了解,HMR 本身源码内容较多,许多细节之处本文没有完整写出,需要各位读者自己慢慢阅读和理解源码。 《图解 Promise 实现原理(二):Promise 链式调用》 《图解 Promise 实现原理(三):Promise 原型方法实现》 《图解 Promise 实现原理(四):Promise 静态方法实现》 《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》 《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》 《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》 《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》 《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》 《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》 《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》 《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》 《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》 《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》 《手把手教你7个有趣的JavaScript 项目-上「附源码」》 《手把手教你7个有趣的JavaScript 项目-下「附源码」》 《JavaScript 使用 mediaDevices API 访问摄像头自拍》 《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》 《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》 《可视化的 js:动态图演示 Promises & Async/Await 的过程》 《Pug 3.0.0正式发布,不再支持 Node.js 6/8》 《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》 《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》 《JavaScript 已进入第三个时代,未来将何去何从?》 《前端上传前预览文件 image、text、json、video、audio「实践」》 《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》 《推荐13个有用的JavaScript数组技巧「值得收藏」》 《36个工作中常用的JavaScript函数片段「值得收藏」》 《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》 《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》 《JavaScript正则深入以及10个非常有意思的正则实战》 《前端开发规范:命名规范、html规范、css规范、js规范》 《100个原生JavaScript代码片段知识点详细汇总【实践】》 《手把手教你深入巩固JavaScript知识体系【思维导图】》 《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》 《身份证号码的正则表达式及验证详解(JavaScript,Regex)》 《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》 《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》 《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》 《面试中教你绕过关于 JavaScript 作用域的 5 个坑》 作者:王平安 转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q 作者:王平安 转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q目录
5.浏览器端发布消息
// webpack-dev-server\client\index.js
// 1. 处理 ok 消息 Line 135
var onSocketMsg = {
// ...
ok: function ok() {
sendMsg('Ok');
if (useWarningOverlay || useErrorOverlay) overlay.clear();
if (initial) return initial = false; // eslint-disable-line no-return-assign
reloadApp();
},
// ...
}
// 2. 处理刷新 APP Line 218
function reloadApp() {
// ...
if (_hot) {
// 动态加载 emitter
var hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
if (typeof self !== 'undefined' && self.window) {
// broadcast update to window
self.postMessage('webpackHotUpdate' + currentHash, '*');
}
}
// ...
}
6.传递 hash 到 HMR
// webpack\hot\dev-server.js
// 1.监听 webpackHotUpdate Line 42
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function(currentHash) {
lastHash = currentHash;
if(!upToDate() && module.hot.status() === "idle") {
log("info", "[HMR] Checking for updates on the server...");
check();
}
});
var check = function check() {
module.hot.check(true).then(function(updatedModules) {
if(!updatedModules) {
// ...
window.location.reload();// 浏览器刷新
return;
}
if(!upToDate()) {
check();
}
}).catch(function(err) { /*...*/});
};
// webpack\lib\HotModuleReplacement.runtime.js
// 3.调用 HotModuleReplacement.runtime 定义的 check 方法 Line 167
function hotCheck(apply) {
if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
hotApplyOnUpdate = apply;
hotSetStatus("check");
return hotDownloadManifest(hotRequestTimeout).then(function(update) {
//...
});
}
7.检测是否存在更新
// webpack\lib\HotModuleReplacement.runtime.js
// 1.调用 HotModuleReplacement.runtime 定义 hotDownloadUpdateChunk 方法 Line 171
function hotCheck(apply) {
if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
hotApplyOnUpdate = apply;
hotSetStatus("check");
return hotDownloadManifest(hotRequestTimeout).then(function(update) {
//...
{
// hotEnsureUpdateChunk 方法中会调用 hotDownloadUpdateChunk
hotEnsureUpdateChunk(chunkId);
}
});
}
// webpack\lib\HotModuleReplacement.runtime.js Line 215
function hotEnsureUpdateChunk(chunkId) {
if(!hotAvailableFilesMap[chunkId]) {
hotWaitingFilesMap[chunkId] = true;
} else {
hotRequestedFilesMap[chunkId] = true;
hotWaitingFiles++;
hotDownloadUpdateChunk(chunkId);
}
}
8.请求更新最新文件列表
// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest 定义 Line 22
function hotDownloadManifest(requestTimeout) {
return new Promise(function(resolve, reject) {
try {
var request = new XMLHttpRequest();
var requestPath = $require$.p + $hotMainFilename$;
request.open("GET", requestPath, true);
request.timeout = requestTimeout;
request.send(null);
} catch(err) {
return reject(err);
}
request.onreadystatechange = function() {
// ...
};
});
}
9.请求更新最新模块代码
// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest 定义 Line 12
function hotDownloadUpdateChunk(chunkId) {
// 创建 script 标签,发起 JSONP 请求
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.type = "text/javascript";
script.charset = "utf-8";
script.src = $require$.p + $hotChunkFilename$;
$crossOriginLoading$;
head.appendChild(script);
}
10.更新模块和疫赖引用
// webpack\lib\HotModuleReplacement.runtime.js
// 找出 outdatedModules 和 outdatedDependencies Line 342
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);
}
}
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;
}
}
}
}
// webpack\lib\HotModuleReplacement.runtime.js
// 从缓存中删除过期模块、依赖和所有子元素的引用 Line 442
function hotApply() {
// ...
var idx;
var queue = outdatedModules.slice();
while(queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
// ...
// 移除缓存中的模块
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);
}
}
}
}
}
// webpack\lib\HotModuleReplacement.runtime.js
// 将新模块代码添加到 modules 中 Line 501
function hotApply() {
// ...
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
}
if (module.hot) {
module.hot.accept('./library.js', function() {
// 使用更新过的 library 模块执行某些操作...
})
}
11.热更新错误处理
// webpack\hot\dev-server.js Line 13
module.hot.check(true).then(function (updatedModules) {
if (!updatedModules) {
return window.location.reload();
}
// ...
}).catch(function (err) {
var status = module.hot.status();
if (["abort", "fail"].indexOf(status) >= 0) {
window.location.reload();
}
});
五、总结
推荐JavaScript经典实例学习资料文章