理想是豐滿的,現實是骨感的,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實現兼容。
- Array.prototype.forEach
- Function.prototype.bind
- JSON
- window.console
- Object.keys
- Object.is
- Object.assign
- 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 聯系我。