您的位置:首页 > 其它

挣脱浏览器的束缚(5) - 哭笑不得的IE Bug

2016-07-29 00:00 246 查看
还记得《ASP.NET AJAX Under the Hood Secrets》吗?这是我在自己的Blog上推荐过的唯一一篇文章(不过更可能是一时兴起)。在这片文章里,Omar Al Zabir提出了他在使用ASP.NET AJAX中的一些经验。其中提到的一点就是:Browsers do not respond when more than two calls are in queue。简单的说,就是在IE中,如果同时建立了超过2两个连接在“连接状态”中,但是没有连接成功(连接成功之后就没有问题了,即使在传输数据),浏览器会停止对其他操作的响应,例如点击超级链接进行页面跳转,直到除了正在尝试的两个连接就没有其他连接时,浏览器才会重新响应用户操作。
出现这个问题一般需要3个条件:

同时建立太多连接,例如一个门户上有许多个模块,它们在同时请求服务器端数据。

响应比较慢,从浏览器发起连接,到服务器端响应连接,所花的时间比较长。

使用IE浏览器,无论IE6还是IE7都会这个问题,而FireFox则一切正常。

在IE7里居然还有这个bug,真是令人哭笑不得。但是我们必须解决这个问题,不是吗?

编写代码来维护一个队列
与《ASP.NET AJAX Under the Hood Secrets》一文中一样,最容易想到的解决方案就是编写代码来维护一个队列。这个队列非常容易编写,代码如下:
if (!window.Global)
{
window.Global = new Object();
}

Global._RequestQueue = function()
{
this._requestDelegateQueue = new Array();

this._requestInProgress = 0;

this._maxConcurrentRequest = 2;
}

Global._RequestQueue.prototype =
{
enqueueRequestDelegate : function(requestDelegate)
{
this._requestDelegateQueue.push(requestDelegate);
this._request();
},

next : function()
{
this._requestInProgress --;
this._request();
},

_request : function()
{
if (this._requestDelegateQueue.length <= 0) return;
if (this._requestInProgress >= this._maxConcurrentRequest) return;

this._requestInProgress ++;
var requestDelegate = this._requestDelegateQueue.shift();
requestDelegate.call(null);
}
}

Global.RequestQueue = new Global._RequestQueue();


我在实现这个队列时使用了最基本的JavaScript,可以让这个实现不依赖于任何AJAX类库。这个实现非常容易实现的,我简单介绍一下它的使用方式。

在需要发起AJAX请求时,不能直接调用最后的方法来发起请求。需要封装一个delegate然后放入队列。

在AJAX请求完成时,调用next方法,可以发起队列中的其他请求。

例如,我们在使用prototype 1.4.0版时我们可以这样:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>Request Queue</title>
<script type="text/javascript" src="js/prototype-1.4.0.js"></script>
<script type="text/javascript" src="js/RequestQueue.js"></script>

<script language="javascript" type="text/javascript">
function requestWithoutQueue()
{
for (var i = 0; i < 10; i++)
{
new Ajax.Request(
url,
{
method: 'post',
onComplete: callback
});
}

function callback(xmlHttpRequest)
{
...
}
}

function requestWithQueue()
{
for (var i = 0; i < 10; i++)
{
var requestDelegate = function()
{
new Ajax.Request(
url,
{
method: 'post',
onComplete: callback,
onFailure: Global.RequestQueue.next,
onException: Global.RequestQueue.next
});
}

Global.RequestQueue.enqueueRequestDelegate(requestDelegate);
}

function callback(xmlHttpRequest)
{
...
Global.RequestQueue.next();
}
}
</script>
</head>
<body>
...
</body>
</html>


在上面的代码中,requestWithoutQueue方法发起了普通的请求,requestWithQueue则使用了Request Queue,大家可以比较一下它们的区别。

使用Request Queue的缺陷
这个Request Queue能够工作正常,但是使用起来实在不方便。为什么?
我们来想一下,如果一个应用已经写的差不多了,我们现在需要在页面里使用这个Request Queue,我们需要怎么做?我们需要修改所有发起请求的地方,改成使用Request Queue的代码,也就是建立一个Request Delegate。而且,我们需要把握所有的异常情况,保证在出现错误时,Global.RequestQueue.next方法也能够被及时地调用。否则这个队列就无法正常工作了。还有,ASP.NET AJAX中有UpdatePanel,该怎么建立Request Delegate?该如何访问Global.RequestQueue.next方法?
我们该怎么办?

可怜的JavaScript,太容易受骗了
我们需要找出一种方式,能够轻易的用在已有的应用中,解决已有应用中的问题。怎么样才能让已有应用修改尽可能的少呢?我们来想一个最极端的情况:一行代码都不用改,这可能么?
似乎是可能的,我们只需要骗过JavaScript就可以。可怜的JavaScript,太容易骗了。话不多说,直接来看代码,一目了然:
window._progIDs = [ 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP' ];

if (!window.XMLHttpRequest)
{
window.XMLHttpRequest = function()
{
for (var i = 0; i < window._progIDs.length; i++)
{
try
{
var xmlHttp = new _originalActiveXObject(window._progIDs[i]);
return xmlHttp;
}
catch (ex) {}
}

return null;
}
}

if (window.ActiveXObject)
{
window._originalActiveXObject = window.ActiveXObject;

window.ActiveXObject = function(id)
{
id = id.toUpperCase();

for (var i = 0; i < window._progIDs.length; i++)
{
if (id === window._progIDs[i].toUpperCase())
{
return new XMLHttpRequest();
}
}

return new _originaActiveXObject(id);
}
}

window._originalXMLHttpRequest = window.XMLHttpRequest;

window.XMLHttpRequest = function()
{
this._xmlHttpRequest = new _originalXMLHttpRequest();
this.readyState = this._xmlHttpRequest.readyState;
this._xmlHttpRequest.onreadystatechange =
this._createDelegate(this, this._internalOnReadyStateChange);
}

window.XMLHttpRequest.prototype =
{
open : function(method, url, async)
{
this._xmlHttpRequest.open(method, url, async);
this.readyState = this._xmlHttpRequest.readyState;
},

send : function(body)
{
var requestDelegate = this._createDelegate(
this,
function()
{
this._xmlHttpRequest.send(body);
this.readyState = this._xmlHttpRequest.readyState;
});

Global.RequestQueue.enqueueRequestDelegate(requestDelegate);
},

setRequestHeader : function(header, value)
{
this._xmlHttpRequest.setRequestHeader(header, value);
},

getResponseHeader : function(header)
{
return this._xmlHttpRequest.getResponseHeader(header);
},

getAllResponseHeaders : function()
{
return this._xmlHttpRequest.getAllResponseHeaders();
},

abort : function()
{
this._xmlHttpRequest.abort();
},

_internalOnReadyStateChange : function()
{
var xmlHttpRequest = this._xmlHttpRequest;

try
{
this.readyState = xmlHttpRequest.readyState;
this.responseText = xmlHttpRequest.responseText;
this.responseXML = xmlHttpRequest.responseXML;
this.statusText = xmlHttpRequest.statusText;
this.status = xmlHttpRequest.status;
}
catch(e){}

if (4 === this.readyState)
{
Global.RequestQueue.next();
}

if (this.onreadystatechange)
{
this.onreadystatechange.call(null);
}
},

_createDelegate : function(instance, method)
{
return function()
{
return method.apply(instance, arguments);
}
}
}


本来在想出这个解决方案时,我心中还比较忐忑,担心这个方法的可行性。当真正完成时,可真是欣喜不已。这个解决方案的的关键就在于“伪造JavaScript对象”。JavaScript只会直接根据代码来使用对象,我们如果将一些原生对象保留起来,并且提供一个同名的对象。这样,JavaScript就会使用你提供的伪造的JavaScript对象了。在上面的代码中,主要伪造了两个对象:

window.XMLHttpRequest对象:我们将XMLHttpRequest原生对象保留为window._originalXMLHttpRequest,并且提供一个新的(或者说是伪造的)window.XMLHttpRequest类型。在新的XMLHttpRequest对象中,我们封装了一个原生的XMLHttpRequest对象,同时也会定义了XMLHttpRequest原生对象存在的所有方法和属性,大多数的方法都会委托给原生XMLHttpRequest对象(例如abort方法)。需要注意的是,我们在新的XMLHttpRequest类型的send方法中,创造了一个delegate放入了队列中,并且_internalOnReadyStateChange方法在合适的情况下(readyState为4,表示completed)调用Global.RequestQueue.next方法,然后再触发onreadystatechange的handler。

ActiveXObject对象:由于类库在创建XMLHttpRequest对象的实现不同,有的类库会首先使用ActiveX进行尝试(例如prototype),有些则会首先尝试window.XMLHttpRequest对象(例如Yahoo! UI Library),因此我们必须保证在通过ActiveX创建XMLHttpRequest对象时也能够使用我们伪造的window.XMLHttpRequest类。实现相当的简单:保留原有的window.ActiveXObject对象,在通过新的window.ActiveXObject创建对象时判断传入的id是否为XMLHttpRequest所需的id,如果是,则返回伪造的window.XMLHttpRequest对象,否则则使用原来的ActiveXObject(保存在window._originaActiveXObject变量里)创建所需的ActiveX控件。

其实“骗取”JavaScript的“信任”非常简单,这也就是JavaScript灵活的体现,我们在扩展一个JS类库时,我们完全可以想一下,是否能够使用一些“巧妙”的办法来改变原有的逻辑呢?

“伪造”XMLHttpRequest对象的优点与缺点
现在,要在已有的应用中修改浏览器僵死的状况则太容易了,只需在IE浏览器中引入RequestQueue.js和FakeXMLHttpRequest.js即可。而且我们只需要把“判断”浏览器类型的任务交给浏览器本身就行了,如下:
<!--[if IE]> <script type="text/javascript" src="js/RequestQueue.js"></script> <script type="text/javascript" src="js/FakeXMLHttpRequest.js"></script> <![endif]-->


这样,只有在IE浏览器中,这两个文件才会被下载,何其容易!
那么,这么做会有什么缺点呢?可能最大的缺点,就是伪造的对象无法完全模拟XMLHttpRequest的“行为”。如果在服务器完全无法响应时,访问XMLHttpRequest的status则会抛出异常。请注意,这里说的“完全无法响应”不是指Service Unavailable(很明显,它的status是503),而是彻底的访问不到,比如机器的网络连接断了。而在伪造的XMLHttpRequest中,status无法模拟一个方法调用(IE没有FireFox里的__setter__),因此无法抛出异常。
这个问题很严重吗?个人认为没有什么问题。看看常见的类库封装,都是直接访问status,而不会判断它到底会不会出错。这也说明,这个状况本身已经被那些类库所忽略了。
那么我们也忽略一下吧,这个解决方案还是比较让人满意的。至少目前看来,在使用过程中没有出现问题。我们的“欺骗”行为没有被揭穿,异常成功。:)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: