ES6 + Webpack + React + Babel 如何在低版本浏览器上愉快的玩耍(下)

回顾

原由:javascript

某天,某测试说:“这个页面在 IE8 下白屏,9也白。。”
某前端开发: 吭哧吭哧。。。一上午的时间就过去了,搞定了。
次日,某测试说:“IE 又白了。。”
某前端开发: 嘿咻嘿咻。。。谁用的 Object.assign,出来我保证削不屎你。html

上篇,咱们主要抛出了两个问题,并给出了第一个问题的解决方案。前端

  1. SCRIPT5007: 没法获取属性 xxx 的值,对象为 null 或未定义,这种状况通常是组件继承后,没法继承到在构造函数里定义的属性或方法,一样类属性或方法也一样没法继承java

  2. SCRIPT438: 对象不支持 xxx 属性或方法,这种状况通常是使用了 es六、es7 的高级语法,Object.assign Object.values 等,这种状况在移动端的一些 ‘神机’ 也同样会挂。react

本篇将给出第二个问题的解决方案, 并对第一个问题的解决方案有了更新的进展。webpack

文章略长,请耐心看~嘿嘿嘿~git

image

正文开始

想要不支持该方法的浏览器支持,无非两种办法es6

  1. 局部引用,引入一个相同的方法代替,其缺点则是使用起来比较麻烦,每一个用到的文件都要去引入。github

  2. 全局实现,与之相反的方法是使用 polyfill ,其优势即是使用方便,缺点则是会全局污染,特别是实例方法,涉及到修改其 prototype ,不是你的类,你去修改它原型是不推荐的。web

针对这两种办法,提供出如下几种方案,供你们参考

方案一:引入额外的库

拿最经常使用的 assign 来讲,能够这样

import assign from 'object-assign';
assign({}, {});

其实这种也是咱们以前的使用方式,缺点就是须要去找到对应的库,好比 Promise 咱们可使用 lie

另外一方面一旦有人没有按照这个规则,而直接使用了 Object.assign,那这我的就可能被削。

方案二:全局引入 babel-polyfill

在项目的程序入口

import 'babel-polyfill';

babel 提供了这个 polyfill,有了它,你就能够尽情使用高级方法,包括 Object.values [].includes Set generator Promise 等等。其底层依赖的是 core-js

可是这种方案显然有些暴力, polyfill 构建并 uglify 后的大小为 98k,gzip 后为32.6k,32k 对与移动端仍是有点大的。

性能与使用是否方便本身权衡,好比离线包后或也能够接受。

方案三:手动引入 core-js

这个方案也稍微有些麻烦, core-js 里实现了大部分 e六、es7 的高级语法,具体列表能够去这里查看 https://github.com/babel/babe...

我先截取一部分作下参考

Object: {
      assign: "object/assign",
      create: "object/create",
      defineProperties: "object/define-properties",
      defineProperty: "object/define-property",
      entries: "object/entries",
      freeze: "object/freeze",
      ...
  }

具体怎么使用呢?找到要使用的方法的值,如:assign 是 "object/assign",将其拼接至一个固定路径。

import assign from 'core-js/library/fn/object/assign'

import 'core-js/fn/object/assign'

这里包含上述所说的局部使用和全局实现的两种

直接引入 'core-js/fn/' 下的即为全局实现,你能够在程序入口引入你想使用的,这样相对于方案二避免了多余的库的引入

引入 'core-js/library/fn/' 下的即为局部使用,和方案一同样,只是省去了本身去寻找类库。

可是,实际使用,import 要写辣么长的路径,仍是感受有些麻烦。

方案四:使用 babel-plugin-transform-runtime

本文会重点介绍下这个插件

先看下如何使用

// without options
{
  "plugins": ["transform-runtime"]
}

// with options
{
  "plugins": [
    ["transform-runtime", {
     "helpers": false, // defaults to true; v6.12.0 (2016-07-27) 新增;
      "polyfill": true, // defaults to true
      "regenerator": true, // defaults to true
      // v6.15.0 (2016-08-31) 新增
      // defaults to "babel-runtime"
      // 能够这样配置
      // moduleName: path.dirname(require.resolve('babel-runtime/package'))
      "moduleName": "babel-runtime"
    }]
  ]
}

该插件会作三件事情

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 (Object.assign) and built-ins (Promise).

  • Removes the inline babel helpers and uses the module babel-runtime/helpers instead.

  • 第一件,若是你想使用 generator , 有两个办法,一个就是引入 bable-polyfill 这个你们伙儿,另外一个就是使用这个插件,不然你会看到这个错误

    Uncaught ReferenceError: regeneratorRuntime is not defined
  • 第二件,就是能帮助咱们解决一些高级语法的问题,它会在构建时帮你自动引入,用到什么引什么。

可是它的缺陷是它只能帮咱们引入静态方法和一些内建模块,如 Object.assign Promise 等。实例方法是不会作转换的,如 "foobar".includes("foo") ,官方提示在这里:

NOTE: Instance methods such as "foobar".includes("foo") will not work since that would require modification of existing builtins (Use babel-polyfill for that).

翻译一下就是,不要越俎代庖,不是你的东西你别乱碰,欠儿欠儿的。

image

因此这个方案不会像方案二那样为所欲为的使用,但其实也基本够用了。

没有的实例方法能够采用方案三委屈下。

我的仍是比较推荐这两种合体的方案。

须要注意的一点是:

开启 polyfill 后,会与 export * from 'xx' 有冲突

请看构建后的代码:

...
/***/ },
/* 106 */
/***/ function(module, exports, __webpack_require__) {

    'use strict';
    // 这是什么鬼。
    import _Object$defineProperty from 'babel-runtime/core-js/object/define-property';
    import _Object$keys from 'babel-runtime/core-js/object/keys';
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    ...

截止 2016-09-10,官方还没有解决此 issue, 只有先避开 export * from 'xx' 这种写法。或在这里找答案。

  • 第三件,是会引入一些 helper 来代替每次都生成的通用函数,看个例子就明白了

原来构建好的代码每一个模块都有相似这种代码:

function _classCallCheck(instance, Constructor)...

function _possibleConstructorReturn(self, call)...

function _inherits(subClass, superClass)...

开启 helper 后:

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');

var _inherits2 = require('babel-runtime/helpers/inherits');

这样统一引用了 helper,去处了冗余,看起来也更优雅了。

在 v6.12.0 以前 helper 也是默认开启的,没有配置可改,其余的 ployfill regenerator 都是有配置能够设置的。也许是推荐你使用 helper 。

可是 v6.12.0 (2016-07-27) 增长了 helper 的配置。为何呢?

我最开始用这个插件的时候也很诧异,按道理来讲,去除了冗余代码,代码的体积应该变小才对,但实际测试却变大了,我测试时是未经 uglify 的代码从 18k 增长到了 78k,查看构建模块增长了将近 100 个 详情

缘由是从 babel-runtime 里引入的 helper 依赖不少,所有都是兼容最底层的。好比 Object.create typeof 这种方法所有被重写了。

后来 gaearon 大神都忍不了了,他测试的结果是增长了 5kB min+gzip 详情

因而有了 helper 这个配置项。

另外还有一点,若是开启了 helper 的话,你会发现以前引用的 babel-plugin-transform-proto-to-assign 就失效了,虽然他原本就不应被使用,后面会讲到。

因此目前看来这个 helper 不用也罢。

再说下 moduleName 这个参数是干什么的?

还记得开启 helper 后的代码吗

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

看下这个路径,若是是本地项目安装了 babel-runtime 是没问题的,但若是你是用的通用构建工具,好比 nowa,全部的构建依赖库都是在公共的地方,毕竟 babel 太太了。这里就会报错了。

Cannot resolve module babel-runtime/regenerator

gaearon 大神在写 create-react-app 时也发现了这个问题, 详情

虽然这个问题能够经过 webpack 的 resolve.root 来解决,可是 gaearon 大神看其不爽,以为依赖 webpack 不够优雅,#3612 因而乎就有了 moduleName 这个参数,已发布 v6.15.0 (2016-08-31)。

放弃 loose 模式, 放弃 ie8

上篇中提到了开启了 loose 模式来解决低版本浏览器没法继承到在构造函数里定义的属性或方法。

咱们是经过 babel-preset-es2015-ie 这个插件,主要是改写了 babel-plugin-transform-es2015-classes: {loose: true} 和添加了插件 babel-plugin-transform-proto-to-assign(解决类方法继承的问题)

babel-preset-es2015 v6.13.0 (2016-08-04) 时,presets 已经支持了参数配置,能够直接开启 loose 模式。

它内部会把开启一些插件的 loose 模式,不仅是babel-plugin-transform-es2015-classes

{
  presets: [
    ["es2015", { "loose": true }]
  ]
}

这样咱们就能够直接使用 babel-preset-es2015,至于 babel-plugin-transform-proto-to-assign 能够单独配置,也可不使用,由于类方法原本就不应被继承,要使用就直接 Parent.defaultProps 就能够了。

在上文中并无提到开启 loose 模式的另外一个缘由是解决 ie8 下的两个 es3 属性名关键字的问题,由于上文测试均在 ie9 上,因此上述的方案也是停留在必须支持 ie8。

那么若是咱们放弃了 ie8 ,看一看是否是会海阔天空。

babel-plugin-transform-es2015-classes v6.14.0 (2016-08-23) 一个 ‘大胡子哥’(原谅我不认识他) 修复了 __proto__ 这个问题 #3527 Fix class inheritance in IE <=10 without loose mode.
这样咱们就能够在 ie9+ 上使用正常的 es6 模式了。

毕竟咱们该向前看,loose 模式有点后退的赶脚。

这篇文章也表达了不推荐使用 loose 模式

Con: You risk getting problems later on, when you switch from transpiled ES6 to native ES6. That is rarely a risk worth taking.

固然,若是真的离不开 ie8,就针对 es3 关键字的问题引用两个插件便可

require('babel-plugin-transform-es3-member-expression-literals'),
require('babel-plugin-transform-es3-property-literals'),

咱们再稍微看下‘大胡子哥’的修改,其实很简单,也很巧妙,看一行关键代码

// 修改后生成的代码多了一个 先取 `xxx.__proto__` 再使用 `Object.getPrototypeOf`
  var _this = _possibleConstructorReturn(this, (Test.__proto__ || Object.getPrototypeOf(Test)).call(this, props));

回顾下 inherits 方法的实现

function _inherits(subClass, superClass) {
    ...
    // 虽然 ie9/10 不支持 `__proto__`,这里只是做为了普通对象给予赋值,`Object.getPrototypeOf` 获取不到但能够直接 `.__proto__` 获取
  Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
  ...

若是你看懂了实现方式,不知道你有没有发现 babel-plugin-transform-proto-to-assign(解决类方法继承的问题)这个家伙真的不能用了

function _inherits(subClass, superClass) { 
  ...
  // 由于它会将 `__proto__` 转为 `_default` 
  Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : _defaults(subClass, superClass);
}

这样上述的修复就无效了。切记不能使用,仍是那句话,类方法原本就不应被继承。

最后看下终极方案的通用配置

{
  plugins: [
    ["transform-runtime", {
      "helpers": false,
      "polyfill": true,
      "regenerator": true
    }],
    'add-module-exports',
    'transform-es3-member-expression-literals',
    'transform-es3-property-literals',
  ],
  "presets": [
    'react',
    'es2015',
    'stage-1'
  ],
}

更简单、完整的解决方案,请查看 nowa

感谢阅读。

参考连接

广告时间: 请献出你的小星星

相关文章
相关标签/搜索