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

感謝閱讀。

參考連接

廣告時間: 請獻出你的小星星

相關文章
相關標籤/搜索