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

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

https://github.com/RubyLouvre...html

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

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

而框架用到的一些es5,es6方法,我已經提供了一個叫polyfill的文件爲你們準備好,你們也可使用bable.polyfill實現兼容。node

  1. Array.prototype.forEachreact

  2. Function.prototype.bindwebpack

  3. JSONgit

  4. window.consolees6

  5. Object.keysgithub

  6. Object.is

  7. Object.assign

  8. Array.isArray

https://github.com/RubyLouvre...

剩下就是事件系統的兼容。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...

你們也能夠經過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/fedl... 這篇文章

最後你們能夠經過加Q 79641290 聯繫我。

相關文章
相關標籤/搜索