何璇

2017-10-10 13:28

ES6 + Babel + React低版本浏览器采坑记录

本文作者: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解决。

第二个问题,让我们来看一个例子:

// 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模式,编译结果如下:

// ...
// 省略
// ...
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本身也提供了两个插件解决这个问题

本来直接打算在项目中的.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或者源码:

最终的解决方案应该是用稳定的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真的是毒瘤...

参考链接

1条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。