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

天晟發表於2016-09-19

回顧

起因

某天,某測試說:“這個頁面在 IE8 下白屏,9也白。。”

某前端開發: 吭哧吭哧。。。一上午的時間就過去了,搞定了。

第二天,某測試說:“IE 又白了。。”

某前端開發: 嘿咻嘿咻。。。誰用的 Object.assign,出來我保證削不屎你。

上篇,我們主要丟擲了兩個問題,並給出了第一個問題的解決方案。

  1. SCRIPT5007: 無法獲取屬性 xxx 的值,物件為 null 或未定義,這種情況一般是元件繼承後,無法繼承到在建構函式裡定義的屬性或方法,同樣類屬性或方法也同樣無法繼承
  2. SCRIPT438: 物件不支援 xxx 屬性或方法,這種情況一般是使用了 es6、es7 的高階語法,Object.assign Object.values 等,這種情況在移動端的一些 ‘神機’ 也一樣會掛。

本篇將給出第二個問題的解決方案, 並對第一個問題的解決方案有了更新的進展。

文章略長,請耐心看~嘿嘿嘿~

正文開始

想要不支援該方法的瀏覽器支援,無非兩種辦法

  1. 區域性引用,引入一個相同的方法代替,其缺點則是使用起來比較麻煩,每個用到的檔案都要去引入。
  2. 全域性實現,與之相反的方法是使用 polyfill ,其優點便是使用方便,缺點則是會全域性汙染,特別是例項方法,涉及到修改其 prototype ,不是你的類,你去修改它原型是不推薦的。

針對這兩種辦法,提供出以下幾種方案,供大家參考


方案一:引入額外的庫

拿最常用的 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 裡實現了大部分 e6、es7 的高階語法,具體列表可以去這裡檢視 https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-runtime/src/definitions.js

我先擷取一部分做下參考

  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).

翻譯一下就是,不要越俎代庖,不是你的東西你別亂碰,欠兒欠兒的。

所以這個方案不會像方案二那樣隨心所欲的使用,但其實也基本夠用了。

沒有的例項方法可以採用方案三委屈下。

個人還是比較推薦這兩種合體的方案。

需要注意的一點是:

開啟 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

感謝閱讀。

參考連結:

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


相關文章