了不起的 Webpack HMR 学习指南(下)「含源码讲解」

了不起的WebpackHMR学习指南(下)「含源码讲解」

作者:王平安

转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q

目录

了不起的 Webpack HMR 学习指南(上)「含源码讲解」

了不起的 Webpack HMR 学习指南(下)「含源码讲解」 本篇

5.浏览器端发布消息

当 hash 消息发送完成后,socket 还会发送一条 ok 的消息告知 Webpack-dev-server,由于客户端(Client)并不请求热更新代码,也不执行热更新模块操作,因此通过 emit 一个 "webpackHotUpdate" 消息,将工作转交回 Webpack。

//webpack-dev-server\client\index.js
//1.处理ok消息Line135
varonSocketMsg={
//...
ok:functionok(){
sendMsg('Ok');
if(useWarningOverlay||useErrorOverlay)overlay.clear();
if(initial)returninitial=false;//eslint-disable-lineno-return-assign
reloadApp();
},
//...
}

//2.处理刷新APPLine218
functionreloadApp(){
//...
if(_hot){
//动态加载emitter
varhotEmitter=require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate',currentHash);
if(typeofself!=='undefined'&&self.window){
//broadcastupdatetowindow
self.postMessage('webpackHotUpdate'+currentHash,'*');
}
}
//...
}

6.传递 hash 到 HMR

Webpack/hot/dev-server 监听浏览器端 webpackHotUpdate 消息,将新模块 hash 值传到客户端 HMR 核心中枢的 HotModuleReplacement.runtime ,并调用 check 方法检测更新,判断是浏览器刷新还是模块热更新。如果是浏览器刷新的话,则没有后续步骤咯~~

//webpack\hot\dev-server.js
//1.监听webpackHotUpdateLine42
varhotEmitter=require("./emitter");
hotEmitter.on("webpackHotUpdate",function(currentHash){
lastHash=currentHash;
if(!upToDate()&&module.hot.status()==="idle"){
log("info","[HMR]Checkingforupdatesontheserver...");
check();
}
});

varcheck=functioncheck(){
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方*L法**ine167
functionhotCheck(apply){
if(hotStatus!=="idle")thrownewError("check()isonlyallowedinidlestatus");
hotApplyOnUpdate=apply;
hotSetStatus("check");
returnhotDownloadManifest(hotRequestTimeout).then(function(update){
//...
});
}

7.检测是否存在更新

当 HotModuleReplacement.runtime 调用 check 方法时,会调用 JsonpMainTemplate.runtime 中的 hotDownloadUpdateChunk (获取最新模块代码)和 hotDownloadManifest (获取是否有更新文件)两个方法,这两个方法的源码,在下一步展开。

//webpack\lib\HotModuleReplacement.runtime.js
//1.调用HotModuleReplacement.runtime定义hotDownloadUpdateChunk方*L法**ine171
functionhotCheck(apply){
if(hotStatus!=="idle")thrownewError("check()isonlyallowedinidlestatus");
hotApplyOnUpdate=apply;
hotSetStatus("check");
returnhotDownloadManifest(hotRequestTimeout).then(function(update){
//...
{
//hotEnsureUpdateChunk方法中会调用hotDownloadUpdateChunk
hotEnsureUpdateChunk(chunkId);
}
});
}

其中 hotEnsureUpdateChunk 方法中会调用 hotDownloadUpdateChunk :

//webpack\lib\HotModuleReplacement.runtime.jsLine215
functionhotEnsureUpdateChunk(chunkId){
if(!hotAvailableFilesMap[chunkId]){
hotWaitingFilesMap[chunkId]=true;
}else{
hotRequestedFilesMap[chunkId]=true;
hotWaitingFiles++;
hotDownloadUpdateChunk(chunkId);
}
}

8.请求更新最新文件列表

在调用 check 方法时,会先调用 JsonpMainTemplate.runtime 中的 hotDownloadManifest 方法, 通过向服务端发起 AJAX 请求获取是否有更新文件,如果有的话将 mainfest 返回给浏览器端。

了不起的WebpackHMR学习指南(下)「含源码讲解」

这边涉及一些原生 XMLHttpRequest,就不全部贴出了~

//webpack\lib\JsonpMainTemplate.runtime.js
//hotDownloadManifest定义Line22
functionhotDownloadManifest(requestTimeout){
returnnewPromise(function(resolve,reject){
try{
varrequest=newXMLHttpRequest();
varrequestPath=$require$.p+$hotMainFilename$;
request.open("GET",requestPath,true);
request.timeout=requestTimeout;
request.send(null);
}catch(err){
returnreject(err);
}
request.onreadystatechange=function(){
//...
};
});
}

9.请求更新最新模块代码

在 hotDownloadManifest 方法中,还会执行 hotDownloadUpdateChunk 方法,通过 JSONP 请求最新的模块代码,并将代码返回给 HMR runtime 。

了不起的WebpackHMR学习指南(下)「含源码讲解」

然后 HMR runtime 会将新代码进一步处理,判断是浏览器刷新还是模块热更新

//webpack\lib\JsonpMainTemplate.runtime.js
//hotDownloadManifest定义Line12
functionhotDownloadUpdateChunk(chunkId){
//创建script标签,发起JSONP请求
varhead=document.getElementsByTagName("head")[0];
varscript=document.createElement("script");
script.type="text/javascript";
script.charset="utf-8";
script.src=$require$.p+$hotChunkFilename$;
$crossOriginLoading$;
head.appendChild(script);
}

10.更新模块和疫赖引用

这一步是整个模块热更新(HMR)的核心步骤,通过 HMR runtime 的 hotApply 方法,移除过期模块和代码,并添加新的模块和代码实现热更新。

从 hotApply 方法可以看出,模块热替换主要分三个阶段:

  1. 找出过期模块 outdatedModules 和过期疫赖 outdatedDependencies;
//webpack\lib\HotModuleReplacement.runtime.js
//找出outdatedModules和outdatedDependenciesLine342
functionhotApply(){
//...
varoutdatedDependencies={};
varoutdatedModules=[];
functiongetAffectedStuff(updateModuleId){
varoutdatedModules=[updateModuleId];
varoutdatedDependencies={};
//...
return{
type:"accepted",
moduleId:updateModuleId,
outdatedModules:outdatedModules,
outdatedDependencies:outdatedDependencies
};
};
functionaddAllToSet(a,b){
for(vari=0;i<b.length;i++){
varitem=b[i];
if(a.indexOf(item)<0)
a.push(item);
}
}
for(varidinhotUpdate){
if(Object.prototype.hasOwnProperty.call(hotUpdate,id)){
//...省略多余代码
if(hotUpdate[id]){
result=getAffectedStuff(moduleId);
}
if(doApply){
for(moduleIdinresult.outdatedDependencies){
//添加到outdatedDependencies
addAllToSet(outdatedDependencies[moduleId],result.outdatedDependencies[moduleId]);
}
}
if(doDispose){
//添加到outdatedModules
addAllToSet(outdatedModules,[result.moduleId]);
appliedUpdate[moduleId]=warnUnexpectedRequire;
}
}
}
}
  1. 从缓存中删除过期模块、疫赖和所有子元素的引用;
//webpack\lib\HotModuleReplacement.runtime.js
//从缓存中删除过期模块、依赖和所有子元素的引用Line442
functionhotApply(){
//...
varidx;
varqueue=outdatedModules.slice();
while(queue.length>0){
moduleId=queue.pop();
module=installedModules[moduleId];
//...
//移除缓存中的模块
deleteinstalledModules[moduleId];
//移除过期依赖中不需要使用的处理方法
deleteoutdatedDependencies[moduleId];
//移除所有子元素的引用
for(j=0;j<module.children.length;j++){
varchild=installedModules[module.children[j]];
if(!child)continue;
idx=child.parents.indexOf(moduleId);
if(idx>=0){
child.parents.splice(idx,1);
}
}
}
//从模块子组件中删除过时的依赖项
vardependency;
varmoduleOutdatedDependencies;
for(moduleIdinoutdatedDependencies){
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);
}
}
}
}
}
  1. 将新模块代码添加到 modules 中,当下次调用 __webpack_require__ (webpack 重写的 require 方法)方法的时候,就是获取到了新的模块代码了。
//webpack\lib\HotModuleReplacement.runtime.js
//将新模块代码添加到modules中Line501
functionhotApply(){
//...
for(moduleIdinappliedUpdate){
if(Object.prototype.hasOwnProperty.call(appliedUpdate,moduleId)){
modules[moduleId]=appliedUpdate[moduleId];
}
}
}

hotApply 方法执行之后,新代码已经替换旧代码,但是我们业务代码并不知道这些变化,因此需要通过 accept事件通知应用层使用新的模块进行“局部刷新”,我们在业务中是这么使用:

if(module.hot){
module.hot.accept('./library.js',function(){
//使用更新过的library模块执行某些操作...
})
}

11.热更新错误处理

在热更新过程中,hotApply 过程中可能出现 abort 或者 fail 错误,则热更新退回到刷新浏览器(Browser Reload),整个模块热更新完成。

//webpack\hot\dev-server.jsLine13
module.hot.check(true).then(function(updatedModules){
if(!updatedModules){
returnwindow.location.reload();
}
//...
}).catch(function(err){
varstatus=module.hot.status();
if(["abort","fail"].indexOf(status)>=0){
window.location.reload();
}
});

五、总结

本文主要和大家分享 Webpack 的 HMR 使用和实现原理及源码分析,在源码分析中,通过一张“Webpack HMR 工作原理解析”图让大家对 HMR 整个工作流程有所了解,HMR 本身源码内容较多,许多细节之处本文没有完整写出,需要各位读者自己慢慢阅读和理解源码。

推荐JavaScript经典实例学习资料文章

《10个打开了我新世界大门的 WebAPI(上)「实践」》

《10个打开了我新世界大门的 WebAPI(中)「实践」》

《10个打开了我新世界大门的 WebAPI(下)「实践」》

《「图文」ESLint 在中大型团队的应用实践》

《Deno是代码的浏览器,你认同吗?》

《前端存储除了 localStorage 还有啥?》

《Javascript 多线程编程​的前世今生》

《微前端方案 qiankun(实践及总结)》

《「图文」V8 垃圾回收原来这么简单?》

《Webpack 5模块联邦引发微前端的革命?》

《基于 Web 端的人脸识别身份验证「实践」》

《「前端进阶」高性能渲染十万条数据(时间分片)》

《「前端进阶」高性能渲染十万条数据(虚拟列表)》

《图解 Promise 实现原理(一):基础实现》

《图解 Promise 实现原理(二):Promise 链式调用》

《图解 Promise 实现原理(三):Promise 原型方法实现》

《图解 Promise 实现原理(四):Promise 静态方法实现》

《实践教你从零构建前端 Lint 工作流「干货」》

《高性能多级多选级联组件开发「JS篇」》

《深入浅出讲解Node.js CLI 工具最佳实战》

《延迟加载图像以提高Web网站性能的五种方法「实践」》

《比较 JavaScript 对象的四种方式「实践」》

《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》

《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》

《前端如何一次性处理10万条数据「进阶篇」》

《推荐三款正则可视化工具「JS篇」》

《如何让用户选择是否离开当前页面?「JS篇」》

《JavaScript开发人员更喜欢Deno的五大原因》

《仅用18行JavaScript实现一个倒数计时器》

《图文细说JavaScript 的运行机制》

《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》

《推荐Web程序员常用的15个源代码编辑器》

《10个实用的JS技巧「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》

《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》

《深入JavaScript教你内存泄漏如何防范》

《手把手教你7个有趣的JavaScript 项目-上「附源码」》

《手把手教你7个有趣的JavaScript 项目-下「附源码」》

《JavaScript 使用 mediaDevices API 访问摄像头自拍》

《手把手教你前端代码如何做错误上报「JS篇」》

《一文让你彻底搞懂移动前端和Web 前端区别在哪里》

《63个JavaScript 正则大礼包「值得收藏」》

《提高你的 JavaScript 技能10 个问答题》

《JavaScript图表库的5个首选》

《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》

《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》

《教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」》

《可视化的 js:动态图演示 Promises & Async/Await 的过程》

《原生JS封装拖动验证滑块你会吗?「实践」》

《如何实现高性能的在线 PDF 预览》

《细说使用字体库加密数据-仿58同城》

《Node.js要完了吗?》

《Pug 3.0.0正式发布,不再支持 Node.js 6/8》

《纯JS手写轮播图(代码逻辑清晰,通俗易懂)》

《JavaScript 20 年 中文版之创立标准》

《值得收藏的前端常用60余种工具方法「JS篇」》

《箭头函数和常规函数之间的 5 个区别》

《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》

《「前端篇」不再为正则烦恼》

《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》

《深入细品浏览器原理「流程图」》

《JavaScript 已进入第三个时代,未来将何去何从?》

《前端上传前预览文件 image、text、json、video、audio「实践」》

《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》

《推荐13个有用的JavaScript数组技巧「值得收藏」》

《前端必备基础知识:window.location 详解》

《不要再依赖CommonJS了》

《犀牛书作者:最该忘记的JavaScript特性》

《36个工作中常用的JavaScript函数片段「值得收藏」》

《Node + H5 实现大文件分片上传、断点续传》

《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》

《【实践总结】关于小程序挣脱枷锁实现批量上传》

《手把手教你前端的各种文件上传攻略和大文件断点续传》

《字节跳动面试官:请你实现一个大文件上传和断点续传》

《谈谈前端关于文件上传*载下**那些事【实践】》

《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》

《最全的 JavaScript 模块化方案和工具》

《「前端进阶」JS中的内存管理》

《JavaScript正则深入以及10个非常有意思的正则实战》

《前端面试者经常忽视的一道JavaScript 面试题》

《一行JS代码实现一个简单的模板字符串替换「实践」》

《JS代码是如何被压缩的「前端高级进阶」》

《前端开发规范:命名规范、html规范、css规范、js规范》

《【规范篇】前端团队代码规范最佳实践》

《100个原生JavaScript代码片段知识点详细汇总【实践】》

《关于前端174道 JavaScript知识点汇总(一)》

《关于前端174道 JavaScript知识点汇总(二)》

《关于前端174道 JavaScript知识点汇总(三)》

《几个非常有意思的javascript知识点总结【实践】》

《都2020年了,你还不会JavaScript 装饰器?》

《JavaScript实现图片合成*载下**》

《70个JavaScript知识点详细总结(上)【实践】》

《70个JavaScript知识点详细总结(下)【实践】》

《开源了一个 JavaScript 版敏感词过滤库》

《送你 43 道 JavaScript 面试题》

《3个很棒的小众JavaScript库,你值得拥有》

《手把手教你深入巩固JavaScript知识体系【思维导图】》

《推荐7个很棒的JavaScript产品步骤引导库》

《Echa哥教你彻底弄懂 JavaScript 执行机制》

《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》

《深入解析高频项目中运用到的知识点汇总【JS篇】》

《JavaScript 工具函数大全【新】》

《从JavaScript中看设计模式(总结)》

《身份证号码的正则表达式及验证详解(JavaScript,Regex)》

《浏览器中实现JavaScript计时器的4种创新方式》

《Three.js 动效方案》

《手把手教你常用的59个JS类方法》

《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》

《深入浅出讲解 js 深拷贝 vs 浅拷贝》

《手把手教你JS开发H5游戏【消灭星星】》

《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》

《手把手教你全方位解读JS中this真正含义【实践】》

《书到用时方恨少,一大波JS开发工具函数来了》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《手把手教你JS 异步编程六种方案【实践】》

《让你减少加班的15条高效JS技巧知识点汇总【实践】》

《手把手教你JS开发H5游戏【黄金矿工】》

《手把手教你JS实现监控浏览器上下左右滚动》

《JS 经典实例知识点整理汇总【实践】》

《2.6万字JS干货分享,带你领略前端魅力【基础篇】》

《2.6万字JS干货分享,带你领略前端魅力【实践篇】》

《简单几步让你的 JS 写得更漂亮》

《恭喜你获得治疗JS this的详细药方》

《谈谈前端关于文件上传*载下**那些事【实践】》

《面试中教你绕过关于 JavaScript 作用域的 5 个坑》

《Jquery插件(常用的插件库)》

《【JS】如何防止重复发送ajax请求》

《JavaScript+Canvas实现自定义画板》

《Continuation 在 JS 中的应用「前端篇」》

作者:王平安

转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q

作者:王平安

转发链接:https://mp.weixin.qq.com/s/Nd1bXGA5uacFN_guP0Gx3Q