问题现象
项目运行在IE11中产生报错,错误如下:
该错误会阻塞页面加载,导致页面空白。
问题分析
根据报错的行和列信息找到引发错误的代码
// core-js@3.6.5
var TypedArrayConstructorsList = {
Int8Array: 1,
Uint8Array: 1,
Uint8ClampedArray: 1,
Int16Array: 2,
Uint16Array: 2,
Int32Array: 4,
Uint32Array: 4,
Float32Array: 4,
Float64Array: 8
};
var exportTypedArrayMethod = function (KEY, property, forced) {
if (!DESCRIPTORS) return;
// 当 KEY 为 slice 方法时,forced 值为true
if (forced) for (var ARRAY in TypedArrayConstructorsList) {
var TypedArrayConstructor = global[ARRAY];
if (TypedArrayConstructor && has(TypedArrayConstructor.prototype, KEY)) {
delete TypedArrayConstructor.prototype[KEY]; // 报错行
}
}
if (!TypedArrayPrototype[KEY] || forced) {
redefine(TypedArrayPrototype, KEY, forced ? property
: NATIVE_ARRAY_BUFFER_VIEWS && Int8ArrayPrototype[KEY] || property);
}
};
给报错行打上断点得到:
现在已明确在具体执行下面这行代码时报错
// 这行代码执行的前提是 Float32Array 存在并且 Float32Array 的原型上存在 slice 方法
delete Float32Array.prototype['slice'];
而在 use strict
严格模式下对象属性删除是有限制的,只有configurable
设置为true
的对象属性,才能被删除。
'use strict';
Object.defineProperty(Array.prototype, 'fn', { value: function() {return 1;} }); // configurable 默认为false
delete Array.prototype.fn; // 报错:strict 模式下不允许对“fn”调用 Delete
设置 configuration
为 true
'use strict';
Object.defineProperty(Array.prototype, 'fn', { configurable: true , value: function() {return 1;} });
delete Array.prototype.fn; // 删除成功
至此找到了报错的真正原因。
上面还有一个容易忽略的点,就是IE11下 Float32Array
的原型上的 slice
方法是谁给添加的?
正常情况下应该是不存在 slice
方法的
下面主要围绕这个问题进行排查
问题排查
排查项目中页面
逐个注释掉 router/config.tsx
中的路由,观察是否报错,经过排查发现,当项目中没有页面时依旧报错,因此排除页面引发该错误的可能。
第三方依赖排查
逐个注释掉 pages/main/index.html
中依赖的第三方依赖资源文件,发现当去掉高德地图js资源的时候,页面可以正常加载并且无报错
<script
crossorigin="anonymous"
type="text/javascript"
src="https://webapi.amap.com/maps?v=2.0&key=xxx"
></script>
定位高德地图返回的资源内容,定位关键词 Float32Array.prototype['slice']
,得到
// 注意 defineProperty 未配置 configurable 属性,那么默认为 false
Float32Array.prototype.slice || Object.defineProperty(Float32Array.prototype, "slice", {
value: function(t, e) {
return new Float32Array(Array.prototype.slice.call(this, t, e))
}
})
执行上述代码将为 Float32Array.prototype
上添加自定义的 slice
方法,添加后如果进入到 polyfill-vendor
里面,那么由于 Float32Array && has(Float32Array.prototype, 'slice')
条件返回为 true
,必然会执行删除逻辑 delete Float32Array.prototype['slice'];
,由于在严格模式下 slice
方法并未设置 configurable
,因此导致最终的报错。
上面加粗部分,从代码执行结果上来看确实是进入了 polyfill-vendor
里面,那么在代码执行流程上如何证明?
先看一下项目中中资源加载的顺序(已删除无关资源的引入)
<script
crossorigin="anonymous"
src="//static.resource.com/project/js/polyfill-vendors.js"
></script>
<script
crossorigin="anonymous"
type="text/javascript"
src="https://webapi.amap.com/maps?v=2.0&key=xxx"
></script>
<script
crossorigin="anonymous"
src="//static.resource.com/project/js/pages/main.js"
></script>
加载polyfill-vendors.js
后,将会自动执行 slice
的 polyfill
代码
"use strict";
var $slice = [].slice;
var FORCED = fails(function () {
// eslint-disable-next-line no-undef
new Int8Array(1).slice();
});
// `%TypedArray%.prototype.slice` method
// https://tc39.github.io/ecma262/#sec-%typedarray%.prototype.slice
exportTypedArrayMethod('slice', function slice(start, end) { // 处理 slice 方法的polyfill
var list = $slice.call(aTypedArray(this), start, end);
var C = speciesConstructor(this, this.constructor);
var index = 0;
var length = list.length;
var result = new (aTypedArrayConstructor(C))(length);
while (length > index) result[index] = list[index++];
return result;
}, FORCED);
那么如果 amap
先于 polyfill-vendors
加载完毕,那么在 polyfill-vendors
执行的情况下,就会触发报错。
解决方案
-
AMap
资源加载设置defer
属性,如果存在多个defer script
标签,浏览器(IE9及以下除外)会保证它们按照在 HTML 中出现的顺序执行,不会破坏 JS 脚本之间的依赖关系。
-
由于
AMap
目前只在个别页面使用,因此只需要在实际用到的地方按需加载即可,不需要跟随页面强依赖资源一起加载。// 异步加载远程资源 function loadScript(url) { return new Promise((resolve, reject) => { var script = document.createElement('script'); script.type = 'text/javascript'; if (script.readyState) { script.onreadystatechange = function () { if (script.readyState == 'complete' || script.readyState == 'loaded') { resolve(); } }; } else { script.onload = function () { resolve(); }; } // 开始异步下载的js文件 script.src = url; document.head.appendChild(script); }); }