高性能迷你React框架anu在低版本IE的實踐


理想是豐滿的,現實是骨感的,react早期的版本雖然號稱支持IE8,但是頁面總會不自覺切換到奇異模式下,導致報錯。因此必須讓react連IE6,7都支持,這才是最安全。但React本身並不支持IE6,7,因此anu使有用武之地了。

https://github.com/RubyLouvre/anu

但光是anu不行,兼容IE是一個系統性的工程,涉及到打包壓縮,各種polyfill墊片。

首先說一下anu如何支持低版本瀏覽器。anu本身沒有用到太高級的API,像Object.defineProperty, Object.seal, Object.freeze, Proxy, WeakMap等無法 模擬的新API,anu一個也沒有用,而const, let, 箭頭函數,es6模塊,通過babel編譯就可以搞定了。

而框架用到的一些es5,es6方法,我已經提供了一個叫polyfill的文件為大家准備好,大家也可以使用bable.polyfill實現兼容。

  1. Array.prototype.forEach
  2. Function.prototype.bind
  3. JSON
  4. window.console
  5. Object.keys
  6. Object.is
  7. Object.assign
  8. Array.isArray

https://github.com/RubyLouvre/anu/blob/master/dist/polyfill.js

剩下就是事件系統的兼容。React為了實現一個全能的事件系統,3萬行的react-dom,有一半是在搞事件的。事件系統之所以這么難寫,是因為React要實現整個標准事件流,從捕獲階段到target階段再到冒泡階段。如果能獲取事件源對象到document這一路經過的所有元素,就能實現事件流了。但是在IE下,只有冒泡階段,並且許多重要的表單事件不支持冒泡到document。為了事件冒泡,自jQuery時代起,前端高手們已經摸索出一套方案了。使用另一個相似的事件來偽裝不冒泡事件,冒泡到document后,然后變成原來的事件觸發對應的事件。

比如說IE下,使用focusin冒充focus, focusout冒充blur。chrome下,則通過addEventListener的第三個參加為true,強制讓focus, blur被document捕獲到。

//Ie6-9
if(msie < 9){
  eventHooks.onFocus = function(dom) {
    addEvent(dom, "focusin", function(e) {
      addEvent.fire(dom, "focus");
    });
  };
  eventHooks.onBlur = function(dom) {
    addEvent(dom, "blurout", function(e) {
      addEvent.fire(dom, "blur");
    });
  };
}else{
eventHooks.onFocus = function(dom) {
  addEvent(
    dom,
    "focus",
    function(e) {
      addEvent.fire(dom, "focus");
    },
    true
  );
};
eventHooks.onBlur = function(dom) {
  addEvent(
    dom,
    "blur",
    function(e) {
      addEvent.fire(dom, "blur");
    },
    true
  );
};
}

低版本的oninput, onchange事件是一個麻煩,它們最多冒泡到form元素上。並且IE也沒有oninput,只有一個相似的onpropertychange事件。IE9,IE10的oninput其實也有許多BUG,但大家要求放低些,我們也不用理會IE9,IE10的oninput事件。IE6-8的oninput事件,我們是直接在元素上綁定onpropertychange事件,然后觸發一個datasetchanged 事件冒泡到document上,並且這個datasetchanged事件對象帶有一個__type__屬性,用來說明它原先冒充的事件。

function fixIEInput(dom, name) {
  addEvent(dom, "propertychange", function(e) {
    if (e.propertyName === "value") {
      addEvent.fire(dom, "input");
    }
  });
}

addEvent.fire = function dispatchIEEvent(dom, type, obj) {
    try {
      var hackEvent = document.createEventObject();
      if (obj) {
        Object.assign(hackEvent, obj);
      }
      hackEvent.__type__ = type;
      //IE6-8觸發事件必須保證在DOM樹中,否則報"SCRIPT16389: 未指明的錯誤"
      dom.fireEvent("ondatasetchanged", hackEvent);
    } catch (e) {}
  };


function dispatchEvent(e) {//document上綁定的事件派發器
  var __type__ = e.__type__ || e.type;
  e = new SyntheticEvent(e);
  var target = e.target;
  var paths = [];//獲取整個冒泡的路徑
  do {
    var events = target.__events;
    if (events) {
      paths.push({ dom: target, props: events });
    }
  } while ((target = target.parentNode) && target.nodeType === 1);
  // ...略
}

addEvent.fire這個方法在不同瀏覽器的實現是不一樣的,這里顯示的IE6-8的版本,IE9及標准瀏覽器是使用document.createEvent, initEvent, dispatchEvent等API來創建事件對象與觸發事件。在IE6-8中,則需要用document.createEventObject創建事件對象,fireEvent來觸發事件。

ondatasetchanged事件是IE一個非常偏門的事件,因為IE的 fireEvent只能觸發它官網上列舉的幾十個事件,不能觸發自定義事件。而ondatasetchanged事件在IE9,chrome, firefox等瀏覽器中是當成一個自定義事件來對待,但那時它是使用elem.dispatchEvent來觸發了。ondatasetchanged是一個能冒泡的事件,只是充作信使,將我們要修改的屬性帶到document上。

此是其一,onchange事件也要通過ondatasetchanged來冒充,因為IE下它也不能冒泡到document。onchange事件在IE還是有許多BUG(或叫差異點)。checkbox, radio的onchange事件必須在失去焦點時才觸發,因此我們在內部用onclick來觸發,而select元素在單選時候下,用戶選中了某個option, select.value會變成option的value值,但在IE6-8下它竟然不會發生改變。最絕的是select元素也不讓你修改value值,后來我奠出修改HTMLSelectElement原型鏈的大招搞定它。

  try {
    Object.defineProperty(HTMLSelectElement.prototype, "value", {
      set: function(v) {
        this._fixIEValue = v;
      },
      get: function() {
        return this._fixIEValue;
      }
    });
  } catch (e) {}

function fixIEChange(dom, name) {
  //IE6-8, radio, checkbox的點擊事件必須在失去焦點時才觸發
  var eventType = dom.type === "radio" || dom.type === "checkbox"
    ? "click"
    : "change";
  addEvent(dom, eventType, function(e) {
    if (dom.type === "select-one") {
      var idx = dom.selectedIndex,
        option,
        attr;
      if (idx > -1) {
        //IE 下select.value不會改變
        option = dom.options[idx];
        attr = option.attributes.value;
        dom.value = attr && attr.specified ? option.value : option.text;
      }
    }
    addEvent.fire(dom, "change");
  });
}

此外,滾動事件的兼容性也非常多,但在React官網中,統一大家用onWheel接口來調用,在內部實現則需要我們根據瀏覽器分別用onmousewheel, onwheel, DOMMouseScroll來模擬了。

當然還有很多很多細節,這里就不一一列舉了。為了防止像React那樣代碼膨脹,針對舊版本的事件兼容,我都移到ieEvent.js文件中。然后基於它,打包了一個專門針對舊版本IE的ReactIE

https://github.com/RubyLouvre/anu/tree/master/dist

大家也可以通過npm安裝,1.0.2就擁有這個文件

npm install anujs

下面通過一個示例介紹如何使用ReactIE.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">

    <script src="./dist/polyfill.js"></script>
    <script src="./dist/ReactIE.js"></script>
    <script src="./dist/index9.js"></script>

</head>

<body>

    <div>這個默認會被清掉</div>
    <div id='example'></div>


</body>

</html>

首先建立一個頁面,里面有三個JS,其實前兩個文件也能單獨打包的。

index.js的源碼是這樣的,業務線開發時是直接上JSX與es6,為了兼容IE6-8,請不要在業務代碼上用Object.defineProperty與Proxy

class Select extends React.Component{
     constructor() {
        super()
        this.state = {
            value: 'bbb'
        }
        this.onChange = this.onChange.bind(this)
    }
    onChange(e){
       console.log(e.target.value)
       this.setState({
           value: e.target.value
       })
    }
    render() {
        return <div><select  value={this.state.value} onChange={this.onChange}>
            <option value='aaa'>aaa</option>
            <option value='bbb'>bbb</option>
            <option value='ccc'>ccc</option>
        </select><p>{this.state.value}</p></div>
    }
}
class Input extends React.Component{
     constructor() {
        super()
        this.state = {
            value: 'input'
        }
        this.onInput = this.onInput.bind(this)
    }
    onInput(e){
       this.setState({
           value: e.target.value
       })
    }
    render() {
        return <div><input value={this.state.value} onInput={this.onInput} />{this.state.value}</div>
    }
}
class Radio extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: this.props.value
        }
        this.onChange = this.onChange.bind(this)
    }
    onChange(e){
        console.log(e.target.value)
       this.setState({
           value: e.target.value
       })
    }
    render() {
        return <span><input type='radio' name={this.props.name} value={this.props.value}  onChange={this.onChange} />{this.state.value+''}</span>
    }
}
class Playground extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: '請上下滾動鼠標滾輪'
        }
        this.onWheel = this.onWheel.bind(this)
    }
    onWheel(e){
       this.setState({
           value: e.wheelDelta
       })
    }
    render() {
        return <div style={{width:300,height:300,backgroundColor:'red',display:'inline-block'}} onWheel={this.onWheel} >{this.state.value}</div>
    }
}
class MouseMove extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: '請在綠色區域移動'
        }
        this.onMouseMove = this.onMouseMove.bind(this)
    }
    onMouseMove(e){
       var v = e.pageX+' '+e.pageY;
       this.setState({
           value: v
       })
    }
    render() {
        return <div style={{width:300,height:300,backgroundColor:'#a9ea00',display:'inline-block'}} onMouseMove={this.onMouseMove} >{this.state.value}</div>
    }
}
class FocusEl extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: '點我'
        }
        this.onFocus = this.onFocus.bind(this)
    }
    onFocus(e){
       console.log(e.target.title)
    }
    render() {
        return <input  title={this.props.title} onKeyUp={(e)=>{console.log(e.which)}} style={{width:100,height:50,backgroundColor:'green',display:'inline-block'}} onFocus={this.onFocus} />
    }
}
window.onload = function(){
    window.s = ReactDOM.render( <div><Select /><Input /><Radio name='sex' value="男" /><Radio name='sex' value='女'/>
    <p><Playground /> <MouseMove /><FocusEl title="aaa" /><FocusEl title="bbb" /></p>
    
    </div>, document.getElementById('example'))
}

然后我們建一個webpack.config.js,用的是webpack1

const webpack = require("webpack");
const path = require("path");
const fs = require("fs");
var es3ifyPlugin = require('es3ify-webpack-plugin');

module.exports = {
  context: __dirname,
  entry: {
    index9: "./src/index9.js"
  },
  output: {
    path: __dirname + "/dist/",
    filename: "[name].js"
  },
  plugins: [new es3ifyPlugin()],
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        exclude: path.resolve(__dirname, "node_modules")
      }
    ]
  },

  resolve: {
    //如果不使用anu,就可以把這里注釋掉
    alias: {
      react: "anujs/dist/ReactIE.js",
      "react-dom": "anujs/dist/ReactIE.js"
    }
  }
};

es3ify-webpack-plugin是專門將es5代碼轉換為es3代碼,因為es5是允許用關鍵字,保留字作為對象的方法與屬性,而es3不能。萬一碰上module.default,我們就坑大了。es3ify是一個利器。

babel是通過.babelrc來配置,里面用到一個

 {
     "presets": [
          ["es2015", { "modules": false }], "react"
     ],
     "plugins": [
         [
             "transform-es2015-classes", {
                 "loose": true
             }
         ]
     ]
 }

babel-plugin-transform-es2015-classes記使用loose模式。

babel-preset-es2015后面這樣設置是禁用生成"use strict",也建議直接換成babel-preset-avalon,這是個preset生成的代碼兼容性更好。

如果大家用 uglify-js進行代碼上線,這也要注意一下,這里有許多坑,它默認會把es3ify干的活全部白做了。詳見 https://github.com/zuojj/fedlab/issues/5 這篇文章

new webpack.optimize.UglifyJsPlugin({
    compress: {
        properties: false,
        warnings: false
    },
    output: {
        beautify: true,
        quote_keys: true
    },
    mangle: {
        screw_ie8: false
    },
    sourceMap: false
})

最后大家可以通過加Q 79641290 聯系我。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM