理想是丰满的,现实是骨感的,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实现兼容。
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捕获到。
)(dom) (e) ).(dom) (e) ).(dom) { addEvent( dom(e) )).(dom) { addEvent( dom(e) ))
低版本的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.
这个默认会被清掉
首先建立一个页面,里面有三个JS,其实前两个文件也能单独打包的。
index.js的源码是这样的,业务线开发时是直接上JSX与es6,为了兼容IE6-8,请不要在业务代码上用Object.defineProperty与Proxy