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

原由

某天,某測試說:「這個頁面在 IE8 下白屏,9也白。。」javascript

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

次日,某測試說:「IE 又白了。。」java

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

原諒我不由又黑了一把 IE。es6

有人可能會想,都要淘汰了,還有什麼好講的?github

也許幾年後,確實沒用了,但目前咱們的系統仍是要對 ie8+ 作兼容,由於確實還有個別用戶,儘管他沒朋友。。。瀏覽器

記錄下本次在 IE 下踩得坑,讓後面的同窗可以再也不在這上面浪費時間了。babel

通過

測試

首先,看下面代碼(如下測試在 IE9)async

class Test extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <div>{this.props.content}</div>;
  }
}

module.exports = Test;

這段代碼跑的妥妥的,沒什麼問題。函數

通常來講,babel 在轉換繼承時,可能會出現兼容問題,那麼,再看這一段

class Test extends React.Component {
  constructor(props) {
    super(props);
  }
  test() {
      console.log('test');
  }
  render() {
    return <div>{this.props.content}</div>;
  }
}

Test.defaultProps = {
  content: "測試"
};

class Test2 extends Test {
  constructor(props) {
    super(props);
    this.test();
  }
}

Test2.displayName = 'Test2';

module.exports = Test2;

這段代碼一樣也能夠正常運行

也就是說在上述這兩種狀況下,不作任何處理(前提是已經加載了 es5-shim/es5-sham),在 IE9 下均可以正常運行。

而後咱們再看下會跑掛的代碼

class Test extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      test: 1,
    };
  }
  test() {
      console.log(this.state.value);
  }
  render() {
    return <div>{this.props.content}</div>;
  }
}

Test.defaultProps = {
  content: "測試"
};

class Test2 extends Test {
  constructor(props) {
    super(props);
    // SCRIPT5007: 沒法獲取屬性 "value" 的值,對象爲 null 或未定義
    this.test();
    
    // SCRIPT5007: 沒法獲取屬性 "b" 的值,對象爲 null 或未定義
    this.a = this.props.b;
  }
}
// undefined
console.log(Test2.defaultProps);

Test2.displayName = 'Test2';

module.exports = Test2;

這段代碼在高級瀏覽器中是沒問題的,在 IE9 中會出現註釋所描述的問題

從這些問題分析,可得出3個結論

  1. 在構造函數裏的定義的屬性沒法被繼承

  2. 在構造函數裏不能使用 this.props.xx

  3. 類屬性或方法是沒法被繼承的

也就是說,只要規避了這三個條件的話,不作任何處理(前提是已經加載了 es5-shim/es5-sham),在 IE9 下均可以正常運行。

第二點,是徹底能夠避免的,切記在 constructor 直接使用 props.xxx, 不要再用 this.props.xxx

第三點,也是能夠徹底避免的,由於從理論上來講,類屬性就不應被繼承,若是想使用父類的類屬性能夠直接Test2.defaultProps = Test.defaultProps;

第一點,可避免,但沒法徹底避免

緣由

第一點,有時是沒法徹底避免的,那麼就要查詢緣由,才能找到解決方案

咱們把 babel 轉義後的代碼放出來就能查出緣由了

'use strict';

var _createClass = function () {
  ...
}();

function _classCallCheck(instance, Constructor) { 
  ...
}

function _possibleConstructorReturn(self, call) { 
  ...
  // 這個方法只是作了下判斷,返回第一個或第二參數
  return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { 
  ...; 
  // 這裏的 _inherits 是經過將子類的原型[[prototype]]指向了父類,因此若是在高級瀏覽器下,子類的能夠繼承到類屬性
  // 根本問題也是出在這裏,IE9 下既沒有 `setPrototypeOf` 也沒有 `__proto__`
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}

var Test = function (_React$Component) {
  ...
  return Test;
}(React.Component);

Test.defaultProps = {
  content: "測試"
};

var Test2 = function (_Test) {
  _inherits(Test2, _Test);

  function Test2(props) {
    _classCallCheck(this, Test2);
     // 這裏的 this 會經過 _possibleConstructorReturn,來獲取父類構造函數裏定義的屬性
     // _possibleConstructorReturn 只是作了下判斷,若是第二個參數獲得了正確執行,則返回執行結果,不然返回第一個參數,也就是子類的 this
     // 也就是說問題出在 Object.getPrototypeOf 
     // 在 _inherits 中將子類的原型指向了父類, 這裏經過 getPrototypeOf 來獲取父類,其實就是 _Test
     // Object.getPrototypeOf 不能正確的執行,致使了子類沒法繼承到在構造函數裏定義的屬性或方法,也沒法繼承到類屬性或方法
    var _this2 = _possibleConstructorReturn(this, Object.getPrototypeOf(Test2).call(this, props));

    _this2.test();
    console.log(_this2.props.children);
    return _this2;
  }

  return Test2;
}(Test);

console.log(Test2.defaultProps);

Test2.displayName = 'Test';

module.exports = Test2;

經過上述的代碼註釋,能夠得出有兩處問題須要解決

  1. 正確的獲取父類(解決沒法繼承到在構造函數裏定義的屬性或方法)

  2. 正確的將子類的原型指向了父類(解決沒法繼承到類屬性或方法)

解決方案

經過文檔的查詢,發現只要開啓 es2015-classes 的 loose 模式便可解決第一個問題

loose 模式

Babel have two modes:

  • A normal mode follows the semantics of ECMAScript 6 as closely as possible.

  • A loose mode produces simpler ES5 code.

Babel 有兩種模式:

  • 儘量符合 ES6 語義的 normal 模式。

  • 提供更簡單 ES5 代碼的 loose 模式。

儘管官方是更推薦使用 normal 模式,但爲了兼容 IE,咱們目前也只能開啓 loose 模式。

在 babel6 中,主要是經過 babel-preset-2015 這個插件,來進行轉義的
咱們看下 babel-preset-2015

plugins: [
    require("babel-plugin-transform-es2015-template-literals"),
    require("babel-plugin-transform-es2015-literals"),
    require("babel-plugin-transform-es2015-function-name"),
    ...
    require("babel-plugin-transform-es2015-classes"),
    ...
    require("babel-plugin-transform-es2015-typeof-symbol"),
    require("babel-plugin-transform-es2015-modules-commonjs"),
    [require("babel-plugin-transform-regenerator"), { async: false, asyncGenerators: false }],
  ]

是一堆對應轉義的插件,從命名上也可看出了大概,好比 babel-plugin-transform-es2015-classes 就是作類的轉義的,也就是咱們只需把它開啓 loose 模式,便可解決咱們的一個問題

[require('babel-plugin-transform-es2015-classes'), {loose: true}],

看下開啓了 loose 模式的代碼,你會發現它的確更接近 ES5

var Test = function (_React$Component) {
  ...
  // 這裏是 ES5 的寫法
  Test.prototype.test = function test() {
    console.log(this.state.value);
  };
  /* normal 模式是這樣的
  {
    key: 'test',
    value: function test() {
      console.log(this.state.value);
    }
  }
  */
  return Test;
}(React.Component);

var Test2 = function (_Test) {
  _inherits(Test2, _Test);

  function Test2(props) {
    _classCallCheck(this, Test2);
    // 這裏直接拿到了父類 _Test, 即解決了沒法繼承到在構造函數裏定義的屬性或方法
    var _this2 = _possibleConstructorReturn(this, _Test.call(this, props));

    _this2.test();
    return _this2;
  }

  return Test2;
}(Test);

咱們能夠經過去安裝 babel-preset-es2015-loose, 這個插件來開啓 loose 模式。

但從咱們團隊的 老司機 口中

image

獲得了一個更好插件babel-preset-es2015-ie,看下這個插件的代碼,發現它和原來的 babel-preset-2015 只有兩行區別

[
  [require('babel-plugin-transform-es2015-classes'), {loose: true}],
  require('babel-plugin-transform-proto-to-assign'),
]

恰好解決咱們上述碰到的兩個問題

這個 babel-plugin-transform-proto-to-assign 插件會生成一個 _defaults 方法來處理原型

function _inherits(subClass, superClass) { 
  ...; 
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : _defaults(subClass, superClass);
}
function _defaults(obj, defaults) {
 var keys = Object.getOwnPropertyNames(defaults);
  for (var i = 0; i < keys.length; i++) {
   var key = keys[i]; 
   var value = Object.getOwnPropertyDescriptor(defaults, key);
    if (value && value.configurable && obj[key] === undefined) {
     Object.defineProperty(obj, key, value); 
     } 
   }
  return obj;
}

這個插件正確的將子類的原型指向了父類(解決沒法繼承到類屬性或方法)

總結

本文講述低版本瀏覽器報錯的緣由和解決方案

  • 一方面是提示下在構造函數裏不要使用 this.props.xx

  • 另外一方面也對繼承的機制有了更好的理解

在此次項目中發如今低版本瀏覽器跑不起來的兩點主要緣由:

  1. SCRIPT5007: 沒法獲取屬性 xxx 的值,對象爲 null 或未定義,這種狀況通常是組件繼承後,沒法繼承到在構造函數裏定義的屬性或方法,一樣類屬性或方法也一樣沒法繼承

  2. SCRIPT438: 對象不支持 xxx 屬性或方法,這種狀況通常是使用了 es六、es7 的高級語法,Object.assgin Object.keys 等,這種狀況在移動端的一些 ‘神機’ 也同樣會掛。

第一點本文已經分析,預知第二點講解請見下篇。

備註:下篇會主要介紹下如何讓 用了 Object.assign 的那位同窗能夠繼續用,又不會被削。

相關文章
相關標籤/搜索