本文作者:IMWeb 何璇 原文出处:IMWeb社区 未经同意,禁止转载
有个项目要兼容IE8-10
某天,胆大的某前端开发由于业务需要升级了项目依赖IMUI,升级了项目构建(babel 5.x
=> babel 6.x
),于是...这个页面在IE下就白屏了。忙乎了一天加班到深夜,觉得实在是坑多,这里记录一下。
坑越来越深
经过分析,主要有这么几个兼容性问题:
react/react-dom依赖版本问题
这点比较好解决,将react的版本降至0.14.x
即可,然后将imui中用到新特性的组件代码给删除(比如PureComponent
)。
对象不支持 xxx 属性或方法
这种情况一般是使用了es6,es7的高级语法,解决方案有很多种:
- 局部引入额外的库
import assign from 'object-assign'
- 全局引入polyfill(会污染全局,例如
babel-polyfill
) - 使用
babel-plugin-transform-runtime
这里就不详细说了,大家可以使用corejs
方案,支持局部使用和全局实现。
import assign from 'core-js/library/fn/object/assign'; // 局部使用
import 'core-js/fn/object/assign'; // 全局实现
至于第三种方案,下面会详细说...
类继承问题
关于这个问题,网上也已有不少文章做了阐述,主要是因为babel-plugin-transform-es2015-classes
对类继承的编译存在兼容性问题:
- 对一些内置的类(
Date
,Array
)继承会存在问题 - 对一些不支持
__proto__
(IE <= 10)的浏览器,不会被正确继承
第一个问题简单,可以使用babel-plugin-transform-builtin-extend
解决。
第二个问题,让我们来看一个例子:
代码语言:javascript复制// class App extends React.component {
// constructor(props) {
// super(props);
// }
// }
// 被编译为
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " typeof superClass);
}
// 这里使用了Object.create来创建以superClass的原型为原型的对象,重写了子类原型来实现继承,并将constructor指回subClass
// 在es3中可以借助寄生式继承的方式,以避免经典原型链继承的缺点(多执行一遍父类的构造函数以及子类原型上冗余父类的实例属性)
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, enumerable: false, writable: true, configurable: true }
});
// 这里为什么要使用setPrototypeOf或__proto__呢?结合下面的$0
// 为了子类能够继承父类的静态属性和方法
// 由于IE9,10会执行__proto__,因此下面的$0根本无法调用到父类构造函数,因此无法继承父类的实例属性
if (superClass) Object.setPrototypeOf ?
Object.setPrototypeOf(subClass, superClass) :
subClass.__proto__ = superClass;
}
var App = function (_React$component) {
_inherits(App, _React$component);
function App(props) {
_classCallCheck(this, App);
return _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).call(this, props)); // Mark: $0
}
return App;
}(React.component);
怎么解决,可以添加一个polyfill来解决(请查看下面参考链接中的从babel编译es6类继承的一个坑说起)
或者使用babel提供的loose模式,编译结果如下:
代码语言:javascript复制// ...
// 省略
// ...
var App = function (_React$component) {
_inherits(App, _React$component);
function App(props) {
_classCallCheck(this, App);
// 注意这里是直接调用了父类的构造函数
return _possibleConstructorReturn(this, _React$component.call(this, props));
}
return App;
}(React.component);
缺少标识符
大家想必都知道IE8中,保留字是不允许被当做键值的,比如var obj = { default: 1 }
。
而es6的模块体系中,大家都喜欢使用export default xxx
来输出模块的默认值,这就尴尬了...babel编译后的代码在IE8上会直接报错,运行不了:
// import util from 'util';
// export default xxx;
// 会被编译成...
'use strict';
// IE8也不支持defineProperty,开启loose模式即可
Object.defineProperty(exports, "__esModule", {
value: true
});
var _util = require('util');
var _util2 = _interopRequireDefault(_util);
// 注意就是这行!!!
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
exports.default = {};
而babel本身也提供了两个插件解决这个问题
- es3-member-expression-literals
- es3-property-literals
本来直接打算在项目中的.babelrc
文件中加上插件配置即可,但是加上了在某些情形下依然会报这个错误:
{
"presets": [
["es2015", { "loose": true }],
"react",
"stage-0"
],
"plugins": [
[
"transform-builtin-extend", {
"globals": ["Error", "Array"],
"approximate": true
}
],
"es3-member-expression-literals",
"es3-property-literals"
]
}
天真的你以为这样就完了么,其实babel在升级到6.x版本后,将一些编译工作都分拆出去做成plugin,但是这两个plugin的实现是不太稳定的,项目代码的编译结果是部分模块的default加上了引号,部分模块没有(拿了一个比较复杂的模块试验了是稳定重现的),具体想了解的同学可以去看看issues或者源码:
- https://github.com/babel/babel/issues/3903
- https://github.com/babel/babel/issues/4367
- https://github.com/babel/babel/issues/4168
最终的解决方案应该是用稳定的es3ify
,由于项目中用的构建工具是fis3
,这里给出fis3的示例(Webpack的同学用es3ify-loader
即可):
fis.match('src/**.{js,jsx}', {
rExt: 'js',
parser: [
fis.plugin('babel-imweb'),
fis.plugin('es3ify')
]
})
缺少函数
前面说道,可以使用babel-plugin-transform-runtime
来引入polyfill来解决高级用法的问题。但其实这个插件存在的原因是因为babel编译结果需要借助一下helpers
函数(比如_extend),会放在模块编译结果的开始部分,造成冗余。而IMUI作为一个UI组件库供别人使用,正需要使用这个插件,避免污染全局的polyfill。让我们来看看官方是怎么说的:
The
runtime
transformer plugin does three things:
- Automatically requires
babel-runtime/regenerator
when you use generators/async functions. - Automatically requires
babel-runtime/core-js
and maps ES6 static methods and built-ins. - Removes the inline Babel helpers and uses the module
babel-runtime/helpers
instead.
其实这个插件本身也没什么问题,但结合了我们的模块化工具modjs
,就成了深坑。babel-runtime的编译结果依赖corejs
里会带有这样的代码:
// babel-runtime/helpers/inherits
var _setPrototypeOf = require("../core-js/object/set-prototype-of");
...
exports.default = function (subClass, superClass) {
...
// 注意这里的对于Object.setPrototypeOf改为了_setPrototypeOf2.default
if (superClass) _setPrototypeOf2.default ? (0, _setPrototypeOf2.default)(subClass, superClass) : subClass.__proto__ = superClass;
};
而modjs
有这样的代码:
require = function(id) {
...
var mod = modulesMap[id] = {
exports: {}
};
...
...
var factoryConf = factoryMap[id];
...
// 直接返回值
if (typeof factoryConf.factory !== 'function') {
mod.exports = factoryConf.factory;
return mod.exports;
}
...
...
// 注意这里导致core-js/object/set-prototype-of的exports为{}
// 所以上面的Babel编译结果代码运行就报错了
mod.exports = factoryConf.factory.apply(global, args) || mod.exports || {};
return mod.exports;
};
所以导致运行时出现缺少函数的报错。
总结
总之,IE真的是毒瘤...
参考链接
- ES6 Webpack React Babel 如何在低版本浏览器上愉快的玩耍(下)
- 从babel编译es6类继承的一个坑说起
- http://babeljs.io/docs/usage/caveats/