挣脱浏览器的束缚(7) - CrossSubDomainExecutor

简介:
 在上次的文章中,我们已经提到了一种能够跨子域名进行AJAX请求的方法。我们现在就来实现一个对开发人员透明的实现,它会自动判断这个请求是否是跨子域名,如果不是,则使用传统的方法发出AJAX请求,反之则使用我们的方式。
   我在如何实现这个Executor的问题上,我想了很久。按照ASP.NET AJAX的“标准”来说,应该开发一个WebRequestExecutor的子类,然后将其设为默认的Executor或者某个特定 WebRequest的Executor。但是最终我使用了直接修改XMLHttpExecutor的做法,因为考虑到以下原因:
  • 完整实现一个WebRequestExecutor需要实现太多接口,而CrossSubDomainExecutor和XMLHttpExecutor有太多相同的地方。
  • 如果继承WebRequestExecutor的话,还是需要了解XMLHttpExecutor的太多细节,JavaScript做不到太好的封装,无法实现真正的面向对象。
  • CrossSubDomainExecutor在普通情况下和XMLHttpExecutor的行为一模一样。
  直接修改XMLHttpExecutor的做法其实就是在XMLHttpExecutor的prototype上做文章,这也就是JavaScript扩展对象的一贯做法。
  首先,我们定义一个Sys.Net.WebRequest类的静态方法,用来从一个URL中获得域名。例如输入 [url]http://www.sample.com/Default.aspx[/url]这个URL,返回 [url]http://www.sample.com[/url],这个静态函数对于判断是否Cross Domain非常重要,如下: 
Sys.Net.WebRequest._getRawDomain静态方法
Sys.Net.WebRequest._getRawDomain = function(url)
{
url = Sys.Net.WebRequest._resolveUrl(url);

var index = url.indexOf('://');
var prefix = url.substring(0, index + 3);

var url = url.substring(index + 3);
index = url.indexOf('/');
if (index < 0)
{
index = url.indexOf('?');
}

if (index >= 0)
{
url = url.substring(0, index);
}

return prefix + url;
}
 
  其次,我们为Sys.Net.WebRequest类扩展一个,用于检测请求能否直接发出,而不需要使用我们的方法。在这里我们直接使用了一个XMLHttpRequest对象作为尝试:
Sys.Net.WebRequest._checkIfIsCrossDomainRequest方法
Sys.Net.WebRequest.prototype._checkIfIsCrossDomainRequest = function()
{
var request = new XMLHttpRequest();
try
{
request.open('get', this.get_url());
return false;
}
catch(e)
{
return true;
}
}
 
  我们还需要得到WebRequest的两个特性:它的Document Domain(设为document.domain的值),以及它本身的域名(Raw Domain),我们依旧为Sys.Net.Request扩展方法:
Sys.Net.WebRequest._getDocDomain方法
Sys.Net.WebRequest.prototype._getDocDomain = function()
{
if (!this._docDomain)
{
var pageDomain = Sys.Net.WebRequest._getRawDomain(window.location.href);
var requestDomain = this._getRawDomain();

var i = 0;
while (true)
{
i ++;
var c1 = pageDomain.charAt(pageDomain.length - i);
var c2 = requestDomain.charAt(requestDomain.length - i);

if (c1 !== c2)
{
break;
}
}

var url = pageDomain.substring(pageDomain.length - i + 1);
var index = url.indexOf('.');
this._docDomain = url.substring(index + 1);
}

return this._docDomain;
}
Sys.Net.WebRequest._getRawDomain方法
Sys.Net.WebRequest.prototype._getRawDomain = function()
{
if (!this._rawDomain)
{
this._rawDomain = Sys.Net.WebRequest._getRawDomain(this.get_url());
}

return this._rawDomain;
}
 
  我们要修改XMLHttpExecutor,最关键的一步就是要重新实现executeRequest方法。很显然,再这之前,我们需要保留原来的实现:
保留原来的executeRequest方法,并提供新的实现
Sys.Net.XMLHttpExecutor.prototype._normalExecuteRequest = 
Sys.Net.XMLHttpExecutor.prototype.executeRequest;

Sys.Net.XMLHttpExecutor.prototype.executeRequest = function()
{
if (this.get_webRequest()._checkIfIsCrossDomainRequest())
{
this._crossDomainExecuteRequest();
}
else
{
this._normalExecuteRequest();
}
}
 
  接下来,就需要实现一个跨子域名发出AJAX请求方法了。再这之前,需要对于XMLHttpExecute本身的实现有所了解。我们现在就对添加的方法进行简单的分析。
  首先,自然是 _crossDomainExecuteRequest方法。我们准备了两个字典:_iframeCache和_iframeLoaded,分别用于保存 作为Proxy的iframe对象,以及表明iframe对象是否已经加载成功了(iframe加载也是异步的,也需要一定时间)。我们先模仿 XMLHttpExecutor的实现,建立一个侦测是否超时的监听器;再使用该请求的Raw Domain作为key,经过下面判断的三个分支做出不同逻辑。它们是:
  1. 如果iframeCache中没有相应的iframe对象,则调用_createIFrame方法创建一个作为Proxy的iframe。
  2. 如果已经存在iframe对象,但是Proxy页面还没有加载完毕,则等待500毫秒后重新尝试着调用_crossDomainExecuteRequest方法。
  3. 如果Proxy已经加载成功了,并且用户没有调用abort方法,请求也没有过超时时间,则将document.domain设为正确值,并且调用Proxy页面里的方法发出AJAX请求。
Sys.Net.XMLHttpExecutor._crossDomainExecuteRequest方法
Sys.Net.XMLHttpExecutor._iframeCache = {};
Sys.Net.XMLHttpExecutor._iframeLoaded = {};

Sys.Net.XMLHttpExecutor.prototype._crossDomainExecuteRequest = function()
{
var webRequest = this.get_webRequest();
var rawDomain = webRequest._getRawDomain();
var iframeCache = Sys.Net.XMLHttpExecutor._iframeCache;

var timeout = webRequest.get_timeout();
if (timeout > 0)
{
this._timer = window.setTimeout(this._onTimeout, timeout);
}

if (!iframeCache[rawDomain])
{
this._createIFrame();
}
else if (!Sys.Net.XMLHttpExecutor._iframeLoaded[rawDomain])
{
setTimeout(
Function.createDelegate(this, this._crossDomainExecuteRequest),
500);
}
else if (!this.get_aborted() && !this.get_timedOut())
{
document.domain = webRequest._getDocDomain();
iframeCache[rawDomain].contentWindow.sendRequest(this);
}
}
 
  createIFrame方法的作用就是创 建一个新的作为Proxy的iframe对象,加载的内容即为我们准备的Proxy页面,请注意Proxy页面还带有QueryString,用于指定 Proxy页面的document.domain。我们也会响应iframe对象的onload事件。很幸运,这个事件被IE和FireFox浏览器都实 现了——毕竟FireFox原本就是向IE拿来的iframe,有何道理实现的不同呢?
Sys.Net.XMLHttpExecutor._createIFrame方法
Sys.Net.XMLHttpExecutor.prototype._createIFrame = function()
{
var webRequest = this.get_webRequest();
var rawDomain = webRequest._getRawDomain();
var proxyUrl = rawDomain + "/SubDomainProxy.htm?" +
webRequest._getDocDomain();

var iframe = document.createElement('iframe');
Sys.Net.XMLHttpExecutor._iframeCache[rawDomain] = iframe;
iframe.style.display = "none";
$addHandler(iframe, 'load',
Function.createDelegate(this, this._onIFrameLoadHandler));
iframe.src = proxyUrl;
document.body.appendChild(iframe);
return iframe;
}
 
  在iframe加载成功时, onIFrameLoadHandler方法会被调用。在这个方法里,我们将会通过查看Proxy方法有没有被正确加载来判断iframe里的Proxy 有没有被加载成功。如果没有加载成功,则清除iframeCache字典中相应的iframe对象,并直接结束WebRequest请求:清除检测超时的 Timer,调用WebRequest对象的completed方法等等。如果加载成功了,则在iframeLoaded字典中标记这个iframe已经 加载成功了,并且在该方法之后重新调用_crossDomainExecuteRequest方法——只要使用setTimeout再将时间设为0即可:
Sys.Net.XMLHttpExecutor._onIFrameLoadHandler方法
Sys.Net.XMLHttpExecutor.prototype._onIFrameLoadHandler = function()
{
var webRequest = this.get_webRequest();
var rawDomain = webRequest._getRawDomain();
var iframeCache = Sys.Net.XMLHttpExecutor._iframeCache;

try
{
document.domain = webRequest._getDocDomain();
if (iframeCache[rawDomain].contentWindow.sendRequest)
{
Sys.Net.XMLHttpExecutor._iframeLoaded[rawDomain] = true;
setTimeout(
Function.createDelegate(this, this._crossDomainExecuteRequest),
0);
}
else
{
throw new Error();
}
}
catch(e)
{
var iframe = iframeCache[rawDomain];
document.body.removeChild(iframe);
iframeCache[rawDomain] = null;
this._clearTimer();
webRequest.completed(Sys.EventArgs.Empty);
}
}
 
  最后我们只要再提供一个作为Proxy的页 面即可,我们必须把它放在每个子域名的根目录下——当然您也可以改进之前_createIFrame的代码,加载其他位置上的Proxy页面。Proxy 页面的实现非常简单,只要模仿XMLHttpExecutor原有的executeRequest方法实现即可:
SubDomainProxy.htm页面
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>Untitled Page</title>
<script type="text/javascript" language="javascript">
var search = window.location.search;
document.domain = search.substring(1);

if (!window.XMLHttpRequest)
{
window.XMLHttpRequest = function window$XMLHttpRequest()
{
var progIDs = [ 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP' ];

for (var i = 0; i < progIDs.length; i++)
{
try
{
var xmlHttp = new ActiveXObject(progIDs[i]);
return xmlHttp;
}
catch (ex) {}
}

return null;
}
}

function sendRequest(executor)
{
executor._webRequest = executor.get_webRequest();

if (executor._started)
{
throw window.parent.Error.invalidOperation(
window.parent.String.format(
window.parent.Sys.Res.cannotCallOnceStarted,
'executeRequest'));
}
if (executor._webRequest === null)
{
throw window.parent.Error.invalidOperation(
window.parent.Sys.Res.nullWebRequest);
}

var body = executor._webRequest.get_body();
var headers = executor._webRequest.get_headers();

executor._xmlHttpRequest = new XMLHttpRequest();
executor._xmlHttpRequest.onreadystatechange =
executor._onReadyStateChange;

var verb = executor._webRequest.get_httpVerb();
executor._xmlHttpRequest.open(verb,
executor._webRequest.getResolvedUrl(), true);

if (headers)
{
for (var header in headers)
{
var val = headers[header];
if (typeof(val) !== "function")
executor._xmlHttpRequest.setRequestHeader(header, val);
}
}

if (verb.toLowerCase() === "post")
{
if ((headers === null) || !headers['Content-Type'])
{
executor._xmlHttpRequest.setRequestHeader(
'Content-Type', 'application/x-www-form-urlencoded');
}

if (!body)
{
body = "";
}
}

executor._xmlHttpRequest.send(body);
executor._started = true;
}
</script>
</head>
<body>

</body>
</html>
 
  到目前为止,这个CrossDomainExecutor就实现好了,我们现在使用WebRequst时就可以把它的URL设为不同子域名下的资源。例如,我们在 [url]http://www.test.com[/url]里可以请求 [url]http://sub.test.com[/url][url]http://sub0.sub1.test.com[/url]里的资源。而且,如果不做跨子域名的请求,XMLHttpExecutor的行为和之前不会有任何不同。


本文转自 jeffz 51CTO博客,原文链接:http://blog.51cto.com/jeffz/60275,如需转载请自行联系原作者
相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
相关文章
|
3月前
|
存储 前端开发 JavaScript
揭秘浏览器的事件循环:让网页动起来的幕后英雄
揭秘浏览器的事件循环:让网页动起来的幕后英雄
揭秘浏览器的事件循环:让网页动起来的幕后英雄
|
7月前
|
数据采集 Web App开发 JavaScript
Puppeteer无头浏览器:开启自动化之门,掌握浏览器世界的无限可能
大概还是入门期,我曾用Puppeteer做爬虫工具以此来绕过某网站的防爬机制。近期有需求要做任意链接网页截图,像这种场景非常适合用Puppeteer完成。无头浏览器我已知的还有Selenium。
157 2
Puppeteer无头浏览器:开启自动化之门,掌握浏览器世界的无限可能
|
7月前
|
JavaScript 安全 前端开发
浏览器基础原理-安全: 同源策略
浏览器基础原理-安全: 同源策略
36 0
|
10月前
|
Web App开发 开发者
谈一谈|脚本—丰富你的浏览器
谈一谈|脚本—丰富你的浏览器
70 0
|
11月前
|
XML JavaScript 前端开发
我对浏览器的浅微认知
作为前端天天和浏览器打交道,怎么能不对其有所了解呢?这两天看了一些浏览器相关的文章,现在简单总结下自己的理解。
|
Web App开发 缓存 开发框架
浏览器专题系列 - 浏览器内核
浏览器专题系列 - 浏览器内核
|
Web App开发 移动开发 安全
6款浏览器大对比,这就是你不再使用国产浏览器的原因?
6款浏览器大对比,这就是你不再使用国产浏览器的原因?
6款浏览器大对比,这就是你不再使用国产浏览器的原因?
|
JSON 前端开发 JavaScript
程序员应对浏览器同源策略的姿势
浏览器最基本的安全规范——同源策略(Same-Origin Policy)。所谓同源是指域名、协议、端口相同。不同源的浏览器脚本(javascript、ActionScript、canvas)在没明确授权的情况下,不能读写对方的资源。
程序员应对浏览器同源策略的姿势
|
Web App开发 人工智能 搜索推荐
谁说浏览器没有创新?夸克浏览器3.0就给前辈上了一课
谁说浏览器没有创新?夸克浏览器3.0就给前辈上了一课
331 0
谁说浏览器没有创新?夸克浏览器3.0就给前辈上了一课
|
Web App开发 前端开发 JavaScript
浅聊常见浏览器的兼容性问题
浏览器的兼容性问题是个很庞大复杂的问题,很难找到四海皆准的办法,这里我们只是简单介绍下几种经典的处理兼容性问题的方法。
1593 0