Internet Explorer 请求过多时会卡住?伪造 XHR!






2.57/5 (3投票s)
当页面同时建立过多连接时,Internet Explorer 会卡死。让我们构建一个伪造的 XHR 对象来解决 Internet Explorer 浏览器的这个 bug。
引言
几个月前,我的一位朋友,他也是一名顾问和培训师,告诉我他的一个客户遇到了一个问题。当页面同时建立过多连接时,Internet Explorer 会卡死。由于 AJAX 技术如今已广泛使用,这个问题变得越来越普遍。当一个 AJAX 应用程序由较小的应用程序组成时——我们称之为“mash up”——这个问题很可能会发生。
这是 Internet Explorer 的一个 bug。当您发出大量 AJAX 调用时,浏览器会将所有请求保存在一个队列中,一次执行两个。所以,如果您点击某个东西尝试导航到另一个页面,浏览器必须等待正在运行的调用完成,然后才能处理下一个。这个 bug 在 Internet Explorer 6 中非常严重,不幸的是,它在 Internet Explorer 7 中仍然存在。
以编程方式管理请求
解决方案很简单。我们应该自己维护队列,并逐个将请求发送到浏览器的队列。因此,我编写了一个队列来管理请求。这真的很简单
if (!window.Global)
{
window.Global = new Object();
}
Global._ConnectionManager = function()
{
this._requestDelegateQueue = new Array();
this._requestInProgress = 0;
this._maxConcurrentRequest = 2;
}
Global._ConnectionManager.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.ConnectionManager = new Global._ConnectionManager();
我使用纯 JavaScript 代码构建了一个名为 ConnectionManager
的组件,没有任何 AJAX/JavaScript 框架/库的依赖。如果用户想使用此组件来管理请求,他们应该使用 enqueueRequestDelegate
方法将一个代理放入队列。当浏览器中没有正在进行的请求或只有一个正在进行的请求时,将执行该代理。在从服务器接收到响应后,用户必须调用 next
方法来通知 ConnectionManager
,然后 ConnectionManager
将执行队列中的下一个待处理请求代理(如果队列不为空)。
例如,如果我们使用 Prototype 框架连续进行十次 AJAX 调用
function requestWithoutQueue()
{
for (var i = 0; i < 10; i++)
{
new Ajax.Request(
url,
{
method: 'post',
onComplete: callback
});
}
}
function callback(xmlHttpRequest)
{
// do something
}
我们将使用 ConnectionManager
来排队请求,如下所示:
function requestWithQueue()
{
for (var i = 0; i < 10; i++)
{
var requestDelegate = function()
{
new Ajax.Request(
url,
{
method: 'post',
onComplete: callback,
onFailure: Global.ConnectionManager.next,
onException: Global.ConnectionManager.next
});
}
Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
}
}
function callback(xmlHttpRequest)
{
// do sth.
Global.ConnectionManager.next();
}
请注意,我们将 next
方法分配给 onFailure
和 onException
回调处理程序,以确保在从服务器接收到响应后会调用它,因为队列中的其余代理将无法执行,并且如果 next
方法尚未执行,系统将无法发出新的调用。
我把文件发给了我的朋友,几天后他告诉我他的客户说这个组件很难用。我同意。它确实冗长且容易出错。显然,ConnectionManager
不太方便集成到现有代码中。开发人员必须确保所有请求都在 ConnectionManager
中排队,并且无论请求何时完成,都必须在任何情况下执行 next
方法。但这还远远不够。越来越多的 AJAX 应用程序将执行服务器创建的脚本。如果客户端的互联网连接不够稳定,动态创建的文件可能无法成功加载。届时,脚本执行会抛出异常,而按设计本应执行的 next
方法很可能会被错过。
构建一个伪造的 XMLHttpRequest 类型
经过几天思考,我得到一个主意。如果我们能使用另一个组件来替换原生的 XMLHttpRequest
对象并提供内置的请求队列,那将是完美的。如果这样,开发人员就可以通过在页面中放置脚本文件来解决问题,而无需更改一行代码。
解决方案比我之前想象的要容易得多,现在我将向您展示如何构建它。
我们首先要做的就是保留原生的 XHR
类型。请注意,以下代码已经解决了不同浏览器中 XHR
的兼容性问题。
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 _originaActiveXObject(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;
然后,我们应该创建一个新类来替换原生的 XHR
类型。大多数方法只是委托给原生对象中的相应方法。
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;
},
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();
},
_createDelegate : function(instance, method)
{
return function()
{
return method.apply(instance, arguments);
}
},
_internalOnReadyStateChange : function()
{
// ...
},
send : function(body)
{
// ...
}
}
关键点在于 send
方法和 _internalOnReadyStateChange
方法的实现。send
方法会将一个原生 XHR
类型方法的代理放入队列。该代理将在适当的时间由 ConnectionManager
执行。
send : function(body)
{
var requestDelegate = this._createDelegate(
this,
function()
{
this._xmlHttpRequest.send(body);
this.readyState = this._xmlHttpRequest.readyState;
});
Global.ConnectionManager.enqueueRequestDelegate(requestDelegate);
},
我们在构造函数中将 _internalOnReadyStateChange
方法作为原生 XHR
对象的 onreadystatechange
回调处理程序。当回调函数触发时,我们将保留原生对象的所有属性,并执行我们的 onreadystatechange
处理程序。请注意,我们的新组件负责在 readyState
等于 4
(表示当前请求“已完成”)时执行 ConnectionManager
的 next
方法,以便从开发者的角度来看,next
方法可以自动执行。
_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.ConnectionManager.next();
}
if (this.onreadystatechange)
{
this.onreadystatechange.call(null);
}
}
我们已尽最大努力使新组件的行为与原生 XHR
类型相同。但它仍然存在。这是一个小问题,但我们无法做到。当我们在原生 XHR
对象中访问 status
属性时,如果对象无法从服务器端接收到标头,就会抛出错误。但在 Internet Explorer 中,我们无法使用 FireFox 中的 __setter__
关键字将对象的属性定义为方法。这是使用这两个组件时原生 XHR
类型与我们的新类型之间的唯一区别。
如何使用
现在,我们可以轻松地在页面中引用 JS 文件来解决用户使用 Internet Explorer 浏览页面时遇到的问题。
<!--[if IE]>
<script type="text/javascript" src="ConnectionManager.js"></script>
<script type="text/javascript" src="MyXMLHttpRequest.js"></script>
<![endif]-->
我把脚本文件发给了我的朋友。他的客户似乎对这个解决方案很满意。
历史
- 2007 年 6 月 21 日:初次发布