对Webpack打包兼容低版本IE<9时遇到问题的排查过程。

最近在使用webpack打包时,遇到一个问题:
已经添加了polyfill,且转成es3语法的前提下,新增uglify之后IE<9浏览器报错。故,怀疑是uglify做了坏事,一步步追踪下去,发现原来是不太规范的一个写法引起的IE老版本下自身bug。

关于版本

使用不同版本的webpack2,错误依旧复现。
以下讨论的是webpack2.5.1,内置uglify-js2.8.5。

错误1,缺少标识符

这种错误大多是压缩后,对象的属性没有了引号,或是以一些保留字作为了对象的属性,比如”default”。
这种通过对uglify的配置即可解决。这篇文章总结的很好煦涵说Webpack-IE低版本兼容指南
具体配置可以参考本博客上一篇文章中关于的uglifyJS的配置。

错误2,没有找到某个对象的方法、属性(重点来了!!!)

这个就比较坑了。
表现是,初次访问,没有报错,只有执行某个操作时,才报错。可以推测,仅是进入到某个代码内才报错,也就是说是局部报错。
首先我们通过定位报错的那段代码,发现是个命名函数表达式。类似这样:

1
2
3
4
5
6
7
8
9
10
11
A:
var a = function b() {
console.log(b.list['name']);
b.log();
};
a.list = {};
a.list['name'] = 'jack';
a.log = function(){
console.log('log')
}

再对应至压缩之前的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
B:
var test = function test() {
console.log(test.list['name']);
test.log();
};
test.list = {};
test.list['name'] = 'jack';
test.log = function(){
console.log('log')
}

从未压缩的代码可以看到,其实b是一个在函数体内对a的一个引用。
然后简单的把b改成a,再执行,发现没有报错了。

根据MDN上关于命名函数表达式的定义,可以知道function 的 test 和外部的 test 变量不是一回事。test 这个变量作用域链的原因,在函数内部使用的时候优先找到了 test 这个 function 定义了。

所以uglify把B -> A 是没有问题的。

那么为什么只会在IE<9会报错呢。

这是因为命名表达式在IE<9的一个JScript bugs.

简单来说,是这样的:正常情况下,b只能在函数体内使用,a与b指向同一个内存地址。如果在函数外引用,会报错,未定义。
但是在IE<9,会创建2个独立的函数,分别给a,和b。以至于a !== b.这样上面这个函数在IE9以下执行的时候,会报函数体内的对象找不到对应的方法或属性。在函数外引用,反而不报错。
这里的讨论

1
2
3
4
5
6
7
8
9
10
var a = function b() {
// chrome: true, IE8: false
console.log(a === b);
console.log(b.list['name']);
b.log();
};
// chrome: ReferenceError: b is not defined
// IE8: function
console.log(typeof b)

解决方案

  1. 我们在写原始代码时,不要采用匿名函数表达式。而是需要采用具名函数表达式,且与赋值变量不同的名字。
  2. babel6时,去掉presets: ["es2015"],穷举preset-es2015列表中除了transform-es2015-function-name之外的其他相关plguins。不需要单独安装每一个transform-es2015-*的plugin, 安装babel-preset-es2015就可以了。

为什么babel按照es2015转换时,会自动添加function的name呢

funciton添加name的好处大多是为了方便调试。可以观察到调用栈Call Stack。虽然现在大多数高级浏览器都可以自己找到这个匿名函数,但是,当匿名函数的层级比较深时,就找不到了。或者不太高级的浏览器,自己也找不到。加了这个之后,就可以方便的看到调用栈了。
另外呢,还可以便于元编程。函数名也已是ES6的标准之一了,会被自动添加。

  • babel6 打包时配置presets: ["es2015"],会包括transform-es2015-function-name, 它的作用就是将es2015 function.name特性应用到所有function中。

  • 在babel5的时候有个blacklist选项可以关掉一些不想要的特性。但是babel6的时候去掉了这个配置选项。解决办法是穷举babel的plugins,里面剔除transform-es2015-function-name。

1
2
3
4
5
6
比如:
presets: [
['env', {
blacklist: ['babel-plugin-transform-async-to-generator']
}]
]
1
2
3
4
5
6
7
8
9
10
这样就使得,如果一个函数采用匿名表达式的方式,经过babel转义过后会被转换成:
var test = function () {
console.log(test.age)
};
test.age = 9;
var test = function test() {
console.log(test.age)
};
test.age = 9;

最后一个问题

按照UglifyJS2文档,配置了support-ie8: true之后,就可以避免NFE的问题了。但为啥实际测试中还是未果呢。源码继续追踪中。。

参考文章

写在最最后

一开始怀疑是webpack版本问题,验证不是之后。

进而怀疑是因为webpack默认为匿名函数添加函数名,认为是webpack做的这件事。这里遗漏了,webpack其实自身什么都不做,只是一个框架。其他的转换什么的是由各插件做的。

后来在babel在线实验上验证通用会为匿名函数添加函数名。这时可以证实是babel做了这件事。

接下来,怀疑是uglify的问题,但却忘了,只有在IE<9才会报错,在Chrome下是正常的,这时应该怀疑是某种写法在IE下有兼容性问题。