理想是豐滿的,現實是骨感的,react早期的版本雖然號稱支援IE8,但是頁面總會不自覺切換到奇異模式下,導致報錯。因此必須讓react連IE6,7都支援,這才是最安全。但React本身並不支援IE6,7,因此anu使有用武之地了。
https://github.com/RubyLouvre…
但光是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實現相容。
- Array.prototype.forEach
- Function.prototype.bind
- JSON
- window.console
- Object.keys
- Object.is
- Object.assign
- 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捕獲到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//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__屬性,用來說明它原先冒充的事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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原型鏈的大招搞定它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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就擁有這個檔案
1 |
npm install anujs |
下面通過一個示例介紹如何使用ReactIE.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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來配置,裡面用到一個
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "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 聯絡我。