定位解析一个因脚本劫持导致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实现动态加载脚本的方法.分享给大家供大家参考,具体如下: 最近公司的前端地图产品需要做一下模块划分,希望用户用到哪一块的功能再加载哪一块的模块,这样可以提高用户体验. 所以到处查 ...
随机推荐
- 解读 SSDB、LevelDB 和 RocksDB 到 GaussDB(for Redis) 的迁移
摘要:本期将详细介绍 SSDB.LevelDB 和 RocksDB 到 GaussDB(for Redis)的迁移. 本文分享自华为云社区<华为云PB级数据库GaussDB(for Redis) ...
- 8种图数据库对 NULL 属性值支持情况
摘要:在语义网等图模型中,遵循开放世界假设,对于数据中未包含的事实,都认为是未知的而非假的. 本文分享自华为云社区<图数据库对 NULL 属性值支持情况>,原文作者:你好_TT . NUL ...
- Prometheus搭乘华为云GaussDB(for Influx):让监控数据更安全
摘要:GaussDB(for Influx)是一款分布式架构,云原生的时序数据库.可无缝被Prometheus集成,在协议上原生支持Prometheus远端存储对接至GaussDB(for Influ ...
- IAST 初探:博采众长、精准定位、DevOps友好
之前的文章中,我们了解了 SAST 和 DAST,本文将介绍将两者优势相结合的安全测试技术--IAST. ✦ ✦ 交互式应用安全测试(IAST)是一个自动识别和诊断应用程序和 API 漏洞的技术,它结 ...
- 火山引擎DataTester:一个爆款游戏产品,是如何用A/B测试打磨出来的?
随着国内游戏用户数量趋于饱和,中国游戏产业也从高速成长期逐渐转型,市场成熟度提升,竞争趋于精细化. 随着游戏出海以及私域流量运营的挑战,游戏企业对数据分析的使用需求和依赖度进一步提高.而在游戏研发立项 ...
- 从此告别写 SQL!DataLeap 帮你零门槛完成“数据探查”
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 在日常数据处理工作中,产品.运营.研发或数据分析师经常会面临数据量大且混乱.质量参差不齐的问题,需要花费大量时间和 ...
- Windows 2012 上网慢如何解决
解决步骤:1.执行netsh int tcp show global 查看默认TCP全局参数等相关设置 Windows 2012 默认ECN 功能是开启的,将其关闭即可 以管理员的身份运行下列 ...
- hyper-v虚拟机中ubuntu连不上网络的解决办法
首先重启下hyper-v的服务,看下情况: 1.检查hyper-v相关的服务有没有开启 2.如果开启了服务,unbuntu仍然不能连网,则在ubtuntu中进行接下来的步骤: 2.1 设置网络连接为N ...
- POJ: 2236 Wireless Network 题解
POJ 2236 Wireless Network 加工并储存数据的数据结构 并查集 这是并查集的基本应用,两台修好的电脑若距离d内则加入合并.不过不小心的话会TLE,比如: #include < ...
- Educational Codeforces Round 92 (Rated for Div. 2) A~C
原作者为 RioTian@cnblogs, 本作品采用 CC 4.0 BY 进行许可,转载请注明出处. 最近写学习了一下网络爬虫,但昨天晚上的CF让人感觉实力明显退步,又滚回来刷题了QAQ... 比赛 ...