定位解析一个因脚本劫持导致webpack动态加载异常的问题
问题描述
项目现场的前端项目在点击顶部的导航栏切换不同的模块时,会有小概率出现模块加载报错的情况:

我们的前端项目里是有基于react-loadable做的懒加载的,上图的12.be789340.chunk.js就是懒加载需要请求的模块。现场复现问题时出错的模块每次都可能不一样,并且出现问题的频率也挺稳定的,差不多每一二十次就会出现一次这种情况。
在复现出问题时,再看到网络请求的面板:

可以看到,先是有一个正常的js文件请求,接着会再发出一个相同地址的请求但后缀带上了个从没见过的参数。并且看到在最右侧一列,第二个请求发出的地方是12.be789340.chunk.js:3,是在上一个js文件里发出的!
看完请求面板这里,再结合控制台的(missing: xxx.js)报错,几乎可以断定是我们的js脚本被第三方劫持了。劫持了第一个请求后将里边的内容都替换为自己的,加载完后执行的就是它们的代码,然后再重新发送一次请求,这次请求加载到的内容才是我们前端项目里真正的代码。并且还带上了参数用来标识。
webpack动态加载原理
虽然第一个js脚本的请求被劫持了,但不是接着就发送了第二个请求去加载真正的js内容了吗?为何还会报上图的错误呢。这要从webpack动态加载模块的实现说起。
懒加载模块是利用ES10的新特性import()方法来完成的,经过webpack编译后如下:
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
head.appendChild(script);
}
}
return Promise.all(promises);
};
对于需要加载的模块chunkId,流程如下:
设置
installedChunkData[chunkId],标记该模块正在加载。创建
<script/>标签,并插入页面中,开始加载js脚本。加载完js脚本后会立即执行。在由webpack打包出来的chunk中,会执行
webpackJsonpCallback函数。在该函数中,会修改installedChunks[chunkId] = 0,并且还会执行installedChunks[chunkId]数组中的第一个函数也就是上面那个promise的resolve函数,将__webpack_require__.e函数中返回的promise变成成功状态。webpackJsonpCallback函数的代码如下:// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var executeModules = data[2]; // add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data); while(resolves.length) {
resolves.shift()();
} // add entry modules from loaded chunk to deferred list
deferredModules.push.apply(deferredModules, executeModules || []); // run deferred modules when all chunks ready
return checkDeferredModules();
};
执行完后,执行
<script/>的onload回调,也就是上面的onScriptComplete函数。如果加载成功会判断到installedChunks[chunkId] === 0,则无需做任何操作。否则的话,说明资源加载出错,执行reject(error)抛出异常。
捋清了webpack动态加载chunk文件的流程,导致报错问题的真正原因也就清楚了。我们把导致问题的整个流程也梳理一遍:
- webpack的运行时 向页面中插入需要动态加载的chunk的
<script/>标签,并添加onload回调。 <script/>标签发起请求,但是被拦截了并返回篡改后的代码。- 浏览器接收到篡改后的js脚本后立即执行。由于里面并不是我们前端项目中的chunk的内容,所有并不会有执行
installedChunks[chunkId] = 0这一步。 - 第[3]步执行完后,触发
<script/>的onload回调。在回调函数中,因为判断到installedChunks[chunkId] !== 0,所以reject(error)抛出异常。 - 在篡改的代码内容中,最后还会再请求一次真正的chunk内容。而这个chunk中的代码执行后就算设置了
installedChunks[chunkId] = 0并调用resolve()也已经没有作用了,因为对应的promise在前面已经被reject掉了。
解决办法
- 使用https来加密传输的数据。对于运营商劫持的情况,用https连接就可以很大程度上解决问题。
- 对于笔者的这种情况,是由于项目现场内网环境的一些特殊原因造成的并且没法干预,只能想办法绕开:通过前文对导致报错问题流程的梳理,我们知道是因为第一个执行了篡改内容的
<script/>提前先触发了onload回调(即onScriptComplete函数),才导致了webpack报错。因此我们采用的临时解决办法就是覆写Element.prototype.appendChild方法,使得在document.head.appendChild(script)添加<script/>标签并且资源是属于webpack的动态加载的chunk时,就给原script.onload的回调加上一个延时后再执行(但不要超过script.timeout)。因为在chunk中的js代码执行时调用的webpackJsonpCallback函数会将__webpack_require__.e中的promise给resolve掉,所以onload回调是否执行并不影响webpack动态加载的流程,回调中的代码只是处理 在出错时能够抛出异常的逻辑而已。
定位解析一个因脚本劫持导致webpack动态加载异常的问题的更多相关文章
- webpack动态加载打包chunk命名
最近,遇到复杂h5页面开发,为了优化H5首屏加载速度,想到使用按需加载的方式,减少首次加载的JavaScript文件体积,于是将处理过程在这里记录一下,涉及到的主要是以下三点: 使用Webpack如何 ...
- APK动态加载框架(DL)解析
转载请注明出处:http://blog.csdn.net/singwhatiwanna/article/details/39937639 (来自singwhatiwanna的csdn博客) 前言 好久 ...
- webpack : 无法加载文件 C:\Users\Eileen\AppData\Roaming\npm\webpack.ps1,因为在此系统上禁止运行脚本
报错内容: webpack : 无法加载文件 C:\Users\Eileen\AppData\Roaming\npm\webpack.ps1,因为在此系统上禁止运行脚本.有关详细信息,请参阅 http ...
- 对动态加载javascript脚本的研究
有时我们需要在javascript脚本中创建js文件,那么在javascript脚本中创建的js文件又是如何执行的呢?和我们直接在HTML页面种写一个script标签的效果是一样的吗?(关于页面scr ...
- 使用webpack loader加载器
了解webpack请移步webpack初识! 什么是loader loaders 用于转换应用程序的资源文件,他们是运行在nodejs下的函数 使用参数来获取一个资源的来源并且返回一个新的来源(资源的 ...
- 动态加载JS脚本
建立dynamic.js文件,表示动态加载的js文件,里面的内容为: function dynamicJS() { alert("加载完毕"); } 如下方法中的html页面和dy ...
- js动态加载脚本
最近公司的前端地图产品需要做一下模块划分,希望用户用到哪一块的功能再加载哪一块的模块,这样可以提高用户体验. 所以到处查资料研究js动态脚本的加载,不过真让人伤心啊!,网上几乎都是同一篇文章,4种方法 ...
- Webpack模块加载器
一.介绍 Webpack是德国开发者 Tobias Koppers 开发的模块加载器,它能把所有的资源文件(JS.JSX.CSS.CoffeeScript.Less.Sass.Image等)都作为模块 ...
- Windows系统盘符错乱导致桌面无法加载。
问题如下 : 同事有台笔记本更换SSD硬盘,IT职员帮他将新硬盘分好区后再将系统完整Ghost过来,然后装到笔记本上.理论上直接就可以使用了!但结果开机后登陆用户桌面无法显示,屏幕黑屏什么都没有. 问 ...
- js实现动态加载脚本的方法实例汇总
本文实例讲述了js实现动态加载脚本的方法.分享给大家供大家参考,具体如下: 最近公司的前端地图产品需要做一下模块划分,希望用户用到哪一块的功能再加载哪一块的模块,这样可以提高用户体验. 所以到处查 ...
随机推荐
- 裴丹:AIOps 智能运维经验分享
摘要:本文结合裴丹教授过去二十余年在AIOps领域与几十家企业合作.跨多种技术栈的落地经验积累,以及150篇左右学术论文的算法积累,总结出的AIOps落地的一些经验性原则. 本文分享自华为云社区< ...
- 数仓出现“wait in ccn queue”的时候,怎么迅速定位处理?
摘要:现网在使用动态负载管理的时候,经常出现很多wait in ccn的情况,大家处理起来就会认为是hung住或者怎么着了,很着急,但wait ccn其实就是一个等待资源的状态,在此总结一个ccn问题 ...
- 渗透测试 vs 漏洞扫描:差异与不同
渗透测试和漏洞扫描常常被混淆,这两者都通过探索系统来寻找 IT 基础架构中的弱点及易受攻击的地方.阅读本文,带你了解两者之间的差异与不同. 手动 vs 自动 渗透测试是一种手动安全评估方式,网络安全人 ...
- 火山引擎 DataTester:如何做 A/B 实验的假设检验
A/B 实验的核心统计学理论是(双样本)假设检验,是用来判断样本与样本.样本与总体的差异是由 抽样误差 引起还是 本质差别 造成的一种统计推断方法. 假设检验,顾名思义,是一种对自己做出的假设进行数据 ...
- 如何利用 A/B 实验提升产品用户留存? 看字节实战案例给你答案!
技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 产品增长中最为经典的模型为 AARRR 漏斗模型,该模型追求最大化拉新,第一步"获客"(Acqui ...
- SpringBoot Docker Skywalking agent 不生效
SpringBoot Skywalking agent 通过 Dockfile 配置 不生效 ENTRYPOINT ["java","-Djava.security.eg ...
- xv6book阅读 chapter1
xv6book主要研究了xv6如何实现它的类Unix接口,但是其思想和概念不仅仅适用于Unix.任何操作系统都必须将进程多路复用到底层硬件上,相互隔离进程,并提供受控制的进程间通信机制. 1 了解xv ...
- 一文聊透 IP 地址的那些事
IP 地址,是一个大家都耳熟能详的名词.以生活举例,IP 在互联网中的作用就像是寄件时的收件人地址和寄件人地址,收件人地址让信件可以被正确送达,寄件人地址则让收到信的人可以回信. IP 地址作为每一个 ...
- 用 WebRTC 打造一个音乐教育 App,要解决哪些音质难题?
在去年疫情期间,在线教育行业获得了井喷式的发展,这背后的技术功臣非 RTC 莫属.本文将分享 RTC 技术在音乐教育场景下的实践经验. 作者| 逸城 审校| 泰一 音乐教育场景 - 在线陪练 2020 ...
- 【JAVA基础】List处理
List处理 List使用Lists.partition()分片 public static <T> List<List<T>> partition(List< ...