本文探讨如何解决浏览器对同域Ajax请求数量的限制导致的阻塞问题。通过引入隐藏的iframe并设置相同的document.domain,可以在不同子域名间建立通信通道,从而绕过主域名的请求限制,实现优雅且高效的跨子域名异步数据交互。

经过排查,问题根源已找到。虽然解决过程看似简单,但这反映出我在相关知识储备上仍有不足。以下是具体的解决方案。
被阻塞的Ajax请求
被阻塞的Ajax请求
首先,我们需要验证请求是否存在阻塞现象。使用以下代码进行测试:
连续发起三个请求
代码实现如下:
function simpleRequest()
{
var request = new XMLHttpRequest();
request.open('POST', 'Script.ashx');
request.send(null);
}
function threeRequests()
{
simpleRequest();
simpleRequest();
simpleRequest();
} 当调用threeRequests函数时,会连续向同一域名发起三个请求。通过性能监控图表可以清晰地观察到阻塞效果:

后续请求受前序请求阻塞
每个请求耗时约1.5秒。显然,第三个请求必须等待前两个请求全部结束后才能执行,导致总耗时超过3秒。我们的目标就是消除这种阻塞现象。
传统的跨域名异步请求方案
传统的跨域名异步请求方案
Ajax安全机制的核心限制之一,便是禁止跨域名(Cross-Domain)的Ajax请求。除非用户手动开启本地文件访问权限或在IE中放宽跨域限制,否则向其他域名发起Ajax请求均会被浏览器拦截。此外,跨域判断极为严格:不同的子域名或相同域名但不同端口的资源,均被视为独立域名,无法直接发起Ajax请求。
表面上看,这一限制难以突破,但iframe提供了另一种思路。
尽管iframe并非HTML标准的核心部分,但因其实用性,Firefox等浏览器也予以支持(类似情况还有innerHTML)。目前网上存在一些通过iframe实现跨域异步请求的方案,但实现质量参差不齐。其基本原理是:在目标域名下放置一个代理页面(Proxy),主页面通过Query String将请求参数传递给iframe中的代理页面;代理页面执行Ajax请求后,将结果写入自身location的hash中;主页面则轮询iframe src的hash值,一旦检测到变化,便从hash中提取所需信息。
该方案实现复杂且功能受限。在IE和Firefox中,URL长度通常限制在2000字符左右。对于普通需求或许足够,但若需传输大量数据,则显得力不从心。与下文提出的方案相比,其唯一优势在于支持任意域名的跨域异步请求,而我们的方案仅能突破子域名限制。
接下来介绍我们的解决方案。
优雅地突破子域名限制
优雅地突破子域名限制
突破子域名限制的关键依然在于iframe。
iframe具有独特优势,允许我们跨子域名访问iframe内的页面对象,包括window和DOM结构,甚至调用JavaScript(通过window对象)。只需将主页面与iframe内页面的document.domain设置为相同值即可。随后,在不同子域名的页面中发起请求,并通过JavaScript传递结果。唯一需要的额外资源是一个简单的静态代理页面。
下面开始编写原型代码,虽然简单,但足以说明原理。
首先,编写一个静态页面作为iframe内的代理,代码如下:
SubDomainProxy.html
代码实现如下:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>Untitled Page</title>
<script type="text/javascript" language="javascript">
document.domain = 'test.com'; function sendRequest(method, url)
{
var request = new XMLHttpRequest();
request.open(method, url);
request.send(null);
}
</script>
</head>
<body>
</body>
</html>接着编写主页面代码:
http://www.test.com/Default.html
代码实现如下:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<script type="text/javascript" language="javascript">
document.domain = 'test.com'; function simpleRequest()
{
var request = new XMLHttpRequest();
request.open('POST', 'Script.ashx');
request.send(null);
}
function crossSubDomainRequest()
{
var proxy = document.getElementById('iframeProxy').contentWindow;
proxy.sendRequest('POST', 'http://sub0.test.com/Script.ashx');
}
function threeRequests()
{
simpleRequest();
simpleRequest();
crossSubDomainRequest();
}
</script>
</head>
<body>
<input type="button" value="Request" onclick="threeRequests()" />
<iframe src="http://sub0.test.com/SubDomainProxy.html" style="display:none;"
id="iframeProxy"></iframe>
</body>
</html>当执行threeRequests方法时,将同时向http://www.test.com和http://sub0.test.com两个不同子域名下的资源发起请求。从性能图表可以看出,最后一个请求不再受前两个请求的阻塞:

不同子域名的请求互不阻塞
结果令人满意。
尽管该方案仅能突破子域名限制,但这已足够应对大多数场景。我们为何非要追求任意域名间的异步通信呢?何况我们的方案实现更为优雅。在后续文章中,我们将为ASP.NET AJAX客户端实现完整的CrossSubDomainRequestExecutor,自动判断是否为跨子域名请求并选择相应的Ajax请求方式。这样,客户端异步通信层将对开发人员完全透明。这难道不是最令人愉快的事情吗?:)
注意事项
注意事项
以下几点值得注意:
经过多次尝试,我发现创建XMLHttpRequest对象、调用open方法和send方法,都必须在iframe内的页面中执行,才能在IE和Firefox中成功发送Ajax请求。
在上述示例中,向子域名请求的路径为http://sub0.test.com/Script.ashx。请注意,完整的子域名不可省略,否则在Firefox下会出现权限不足的错误,调用open方法时会抛出异常——Firefox似乎将其视为父页面域名的资源。
受浏览器安全策略影响,浏览器禁止不同域(如phinest.org和lab.phinest.org)、不同协议(如http://phinest.org和https://phinest.org)、不同端口(如http://phinest.org和http://phinest.org:8080)下的页面通过XMLHttpRequest相互访问。此限制同样影响不同页面间JavaScript的相互调用与控制。然而,当主域、协议、端口相同时,通过设置页面的document.domain主域,JavaScript可以在不同子域名间进行访问控制。例如,设置document.domain='phinest.org'后,http://phinest.org和http://lab.phinest.org页面可互访。这一特性也为该情况下不同子域名下的XMLHttpRequest相互访问提供了解决方案。
对于主域、协议、端口相同的Ajax跨域问题,早期已有通过设置document.domain解决的说法,但鲜见具体成功案例。本次尝试的原理是:利用一个隐藏的iframe引入所跨另一子域的页面作为代理,通过JavaScript控制iframe引入的另一子域的XMLHttpRequest进行数据获取。对于不同主域、不同协议或不同端口下的Ajax访问,仍需通过后台代理实现。