• 高性能迷你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={{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={{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={{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 联系我。

  • 相关阅读:
    5.基于文件验证的vsftp虚拟用户
    linux系统用户登录ftp
    4.ftp用户禁锢
    原码,反码,补码
    8map
    命令jhsdb jmap heap pid 1 报错
    20211112_mysql_8.0docker部署踩坑
    Windows 下 tail 查看日志命令工具分享
    前后端java+vue 实现rsa 加解密与摘要签名算法
    优化TTFB 至500ms内
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/6994238.html
Copyright © 2020-2023  润新知