什么是跨域?为什么要实现跨域呢?
这是因为JavaScript出于安全方面的考虑,不允许跨域调用其他页面的对象。也就是说只能访问同一个域中的资源。我觉得这就有必要了解下javascript中的同源策略是怎么回事了:javascript的同源策略
。这里更加细致详细的总结了为什么要跨域:javascript跨域之 什么是跨域?为什么跨域?
于是当我们想某些特定的功能的时候,实现合理的跨域请求就显得比较重要了。我努力通过自己动手,自己模拟环境来切实的尝试跨域是怎么回事。
javascript中的原生Ajax对象XMLHTTPRequest
先看原生ajax的实现代码:
var xhr = createXHR();//for IE封装一下浏览器不同的ajax对象 xhr.onload = function(event){ //确保接收到适当的响应 if((xhr.status >=200&&xhr.status<300)||xhr.status == 304){ alert(xhr.responseText); }else{ alert('error:'+xhr.status); } } xhr.open('get','**.php',true);//open方法的参数open(get/post,url,是否发送异步请求) xhr.send(null);//open只是启动一个请求,并未发送,调用send()才会发送请求 |
我们知道想上面这样就可以发送ajax请求啦,得到的响应数据会自动填充xhr对象的属性,我们就可以去访问啦。
但是,就是有上面说到的限制,只能想同一个域中使用相同端口和协议的URL发送请求。然后就是解决跨域的问题了。
好在有个东西叫CORS(跨源资源共享),它背后的思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。
实现方法:
1.发送get/post请求时,给它添加一个额外的Origin头部,其中包含请求页面的源信息(协议,域名和端口),以便服务器根据这个头部信息来决定是否响应,像这样的:
Origin:http://www.nczonline.net |
2.如果服务器认为这个请求可以接受,就在头部发回相同的源信息:
Access-Control-Allow-Origin:http://www.nczonline.net |
如果没有这个头部或者源信息不匹配,那么服务器就会驳回请求,正常的情况下浏览器会处理请求。
深入研究CORS:HTTP access control (CORS)
另外,说到HTTP头部相关,我觉得也有必要了解下通常的Request Headers和Response
Headers通常都有哪些参数,它们会告诉我们很多信息的:
之所以想到了解,是因为记得看过这篇文章,小胡子利用了响应头部的Date来实现了个倒计时:利用XMLHttpRequest响应头的Date实现倒计时
JSONP(双向)
1、什么是JSONP:
jsonp(json with padding),jsonp与json看起来差不多,只不过jsonp是被包含在函数调用中的json。jsonp由两部分组成,一部分是回调函数,一部分是数据。回调函数就是当响应到来时应该在页面中调用的函数,数据就是传入回调函数中的json数据。他的优点是:第一,他能直接访问响应文本;第二,jsonp支持在浏览器与服务器之间的双向通信。但同时也存在缺点:它无法保证加载的来自其他域的代码是安全的,还有就是无法判断jsonp的请求是否失败。
2、使用方法:
动态创建<script>节点:
function handleResponse(response){
alert("你的ip地址:"+response.ip+",ip地址所在的城市:"+response.city+",所在省份:"+response.region_name);
}
var script = document.createElement('script');
script.src = 'http://freegeoip.net/json/?callback=handleResponse';/*http://freegeoip.net是一个jsonp地理定位服务*/
document.body.insertBefore(script,document.body.firstChild);
|
但是如何判断script节点加载完毕是个问题:ie只能通过script的readystatechange属性,其他浏览器支持script的load事件。
如果用了jquery的话,就不用写上面那些代码了,jquery做了很多工作,调用$.ajax()方法的时候,如果想使用jsonp这种方法,封装参数即可了。
图像ping(单向)
1、什么是图像ping:
图像ping是与服务器进行简单、单向的跨域通信的一种方式,请求的数据是通过查询字符串的形式发送的,而相应可以是任意内容,但通常是像素图或204相应(No
Content)。 图像ping有两个主要缺点:首先就是只能发送get请求,其次就是无法访问服务器的响应文本。
2、使用方法:
var img = new Image(); img.onload = img.onerror = function(){ alert("done!"); }; img.src = "https://raw.githubusercontent.com/zhangmengxue/Todo-List/master/me.jpg"; document.body.insertBefore(img,document.body.firstChild); |
然后页面上就可以显示我放在我的github上某个地方的照片啦。
与<img>类似的可以跨域内嵌资源的还有:
(1)<script src=""></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。上面jsonp也用到了呢。
(2) <link src="">标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type消息头。不同浏览器有不同的限制:
IE, Firefox, Chrome, Safari (跳至CVE-2010-0051)部分 和 Opera。
(3)<video> 和 <audio>嵌入多媒体资源。
(4)<object>, <embed> 和 <applet>的插件。
(5)@font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin
fonts)。
(6) <frame> 和 <iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。
document.domain+iframe实现跨域
首先要知道,只是在页面上通过<iframe>载入了资源,是不能与他交互的,像我的博客右边的那个About中的那个Github的Fllow一样的,只能去访问。那我们想要实现交互的话,对于主域相同而子域不同的情景,就可以借助于document.domain来实现。
www.one.com上的a.html:
document.domain = 'a.com'; var ifr = document.createElement('iframe'); ifr.src = 'http://script.a.com/b.html'; ifr.style.display = 'none'; document.body.appendChild(ifr); ifr.onload = function(){ var doc = ifr.contentDocument || ifr.contentWindow.document; // 在这里操纵b.html alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue); }; |
w3w.one.com上的b.html:
document.domain = 'a.com'; |
这种方式就可以实现a页面和b页面交互了。适用于{www.kuqin.com, kuqin.com, script.kuqin.com,
css.kuqin.com}这样的域名下的页面间,这里默认他们使用相同的协议和端口。
备注:某一页面的domain默认等于window.location.hostname。主域名是不带www的域名,例如a.com,主域名前面带前缀的通常都为二级域名或多级域名,例如www.a.com其实是二级域名。
domain只能设置为主域名,不可以在b.a.com中将domain设置为c.a.com。
同时在修改document.domain的时候也需要注意一些问题,和可能产生的影响:修改document.domain可能产生的影响
这部分参考了这里:js中跨域问题总结document.domain+iframe部分
这里说到了document.domain,那么我也就想到了document的其他属性,放这一起看下:
(1)document.referrer 简单来说,一般情况下浏览器请求A时发送的Header中Referer是什么,那么拿到A页面后document.referre的值就是什么.我们可以通过访问document的这个属性来知道我们的页面是从哪里来的。通常还能看到一些参数。
(2)document.links 它能得到页面中所有<a>和<area>标签中的href属性值。
(3)document.compatMode 如果得到的值是CSS1Compat那么是标准模式,BackCompat则是混杂模式
(4)document.readyState 文档加载属性 值为complete即加载完全 loading则正在加载。
上面只举例列了一些,其实还有很多很好用的,可以自行查阅手册:document的属性和方法
利用iframe和location.hash实现跨域
想必有很多人像我之前一样,或许只知道上面文中所说的那几种方法。所以,我刚了解到可以用iframe和location.hash来实现跨域的时候,我会想,为什么他们可以实现。iframe是什么,有什么特性,location.hash是什么又有什么特性。
准备:
>>1.MDN上说的<iframe>:HTML
的<iframe>(HTML inline框架元素)是一个嵌套的浏览上下文,他可以有效地嵌入另一个HTML页面到当前页面。在HTML
4.01中,一个文件可能包含一个头和一个身体或头部和一个框架集,而不是一个身体和一个框架集。然而,一个<
iframe >可以被应用在正常的文件体。每个浏览上下文有它自己的会话历史和活动文档。浏览上下文包含嵌入的内容叫做父浏览上下文。顶级的浏览上下文(没有父)是典型的浏览器窗口。
了解到了<iframe>,我觉得另外两个相似的标签<frame>和<frameset>可以一起拿过来看下啦,这有篇文章放在一起总结了下:Frameset,Frame和Iframe
>>2.window.location:打开chrome的console,一起来试一下,很快就懂啦。我们从最基本的开始:
2.1 可以跳转到另一个页面:打开百度搜索的主页,然后在console中输入下面代码:
window.location = "http://www.cnblogs.com/skylar/"; |
看,是不是跳转到我的博客主页啦。
2.2 强制从服务器重新加载页面:既然来到了我的博客主页,就先别走,继续输入下面代码:
window.location.reload(true); |
可以看到页面被重新加载了。
2.3 然后可以点击进去比如js中各种跨域问题实战小结(一)这篇文章,console输入:
location.hash = '#comments'; |
看看页面是不是跳到了最下面评论的位置。
其实还有两个特性:比如说alert当前url属性,通过修改window.location.search属性向服务器发送一个字符串,这你可以移步这里自行脑补:MDN
window.location 。
这里有用的是下面这个例子:location.hash理解demo
这里可以查看代码:https://github.com/zhangmengxue,因为我们就是要利用location.hash的特性来加上iframe来实现跨域,看完了这个demo,我们就可以开始实现跨域了。(请用chrome打开,还有些需要总结的兼容性问题)
实现:
我为了模拟两个不同的域,在SAE上面创建了两个wordpress应用,如果你之前也不懂SAE是什么的话,这个小简介:新浪SAE
会让你大致了解到SAE是做什么的,如何开始使用它。我的两个应用:
看到url了吧,是不同域喔,环境模拟好了,我可以开始尝试跨域了。
在[http://1.daeskylar.sinaapp.com/]下有两个文件 a.html和c.html;
在[http://1.skylarisdae.sinaapp.com/]下有文件 b.html
我想要在a.html中访问我在b.html中模拟的数据Helloworld!
好呀,那现在点击 有a.html和c.html的这个域 访问我在这个域上的a.html文件,它应该会告诉你他是a.html页面,然后他会访问到了我放在b.html中的HelloWorld!
跨域这样就实现了喔,那具体是怎样实现的呢?
原理:
a.html想和b.html通信(在a.html中动态创建一个b.html的iframe来发送请求);
但是由于“同源策略”的限制他们无法进行交流(b.html无法返回数据),于是就找个中间人:与a.html同域的c.html;
b.html将数据传给c.html(b.html中创建c.html的iframe),由于c.html和a.html同源,于是可通过c.html将返回的数据传回给a.html,从而达到跨域的效果。
三个页面之间传递参数用的是location.hash,改变hash并不会导致页面刷新(这点很重要)。
a.html---①--->b.html----②--->c.html---③--->a.html——>a.html便获得到了b.html中的数据
①通过iframe的location.hash传参数给b.html ;
②通过iframe的location.hash传参数给c.html ;
③同域传给a.html。
代码:
a.html:
<script type="text/javascript">
alert('我是a页面');
function sendRequest(){
//动态创建个iframe
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
//跨域发送请求给b.html,参数是#sayHello
ifr.src = 'http://1.skylarisdae.sinaapp.com/b.html#sayHello';
document.body.appendChild(ifr);
}
function checkHash(){
var data = location.hash?location.hash.substring(1):'';
if(data){
//这里处理返回值
alert(data);
location.hash = '';
}
}
setInterval(checkHash,1000);
window.onload = sendRequest;
</script>
|
c.html:
1 <!doctype html> 2 <html> 3 <head> 4 <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> 5 <title>localhost:c.html</title> 6 7 <style type="text/css"> 8 9 </style> 10 </head> 11 12 <body> 13 <script type="text/javascript"> 14 //因为c.html和a.html属于同一个域,所以可以通过改变其location.hash的值,
可以通过parent.parent获取a.html的window对象 15 parent.parent.location.hash = self.location.hash.substring(1); 16 </script> 17 </body> 18 </html> |
b.html:
<!doctype html> <html> <head> <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> <title>另某个域的:b.html</title>
<style type="text/css">
</style>
</head>
<body>
<script type="text/javascript">
function checkHash(){
var data = '';
//模拟一个简单的参数处理操作
switch(location.hash){
case '#sayHello':
data = 'HelloWorld';
break;
case '#sayHi':
data = 'HiWorld';
break;
default : break;
}
data && callBack('#'+data);
}
function callBack(hash){
var proxy = document.createElement('iframe');
proxy.style.display = 'none';
proxy.src = 'http://1.daeskylar.sinaapp.com/c.html'+hash;
document.body.appendChild(proxy);
}
window.onload = checkHash;
</script>
</body>
</html> |
利用window.name实现跨域
实现:
我还是利用上面我在sae上面创建的应用:
在[http://1.daeskylar.sinaapp.com/]下有两个文件 wantdata.html和proxy.html;
在[http://1.skylarisdae.sinaapp.com/]下有文件 data.html。
我想要在wantdata.html中访问到我在data.html中存在window.name中的数据,是个字符串,就叫‘我拿到数据啦!’。
访问这里:http://1.daeskylar.sinaapp.com/wantdata.html
应该可以看到alert出的数据喔。
原理:
MDN上的window.name是这样说的:
窗口的名称,主要用于设置超链接和表格对象。Windows不需要有名字。
它也被用于提供跨域通信的一些框架(例如,sessionvars和Dojo的DojoX。IO。windowname)为JSONP更安全的替代方案。
window.name 的美妙之处在于:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的
name 值(2MB)。
这篇文章讲解的非常详细,很值得阅读:使用window.name解决跨域问题
实现起来基本步骤如下:
1.创建一个iframe,把其src指向目标页面(提供web service的页面,该目标页面会把数据附加到这个iframe的window.name上,大小一般为2M,IE和firefox下可以大至32M左右;数据格式可以自定义,如json字符串);
2.监听iframe的onload事件,在此事件中立即设置这个iframe的src指向本地域的某个页面,由本地域的这个页面读取iframe的window.name。
3.获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)。
总结起来即:iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
代码:
wantdata.html
<!doctype html> <html> <head> <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> <title>window.name跨域demo</title>
<style type="text/css">
</style>
</head>
<body>
<script type="text/javascript">
var state = 0,
iframe = document.createElement('iframe'),
loadfn = function() {
if (state === 1) {
var data = iframe.contentWindow.name; // 读取数据
alert(data); //弹出'拿到数据啦!'
//下面立即销毁iframe,释放内存,也保证安全
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if (state === 0) {
state = 1;
iframe.contentWindow.location = "http://1.daeskylar.sinaapp.com/proxy.html";
// 设置的代理文件
}
};
iframe.src = 'http://1.skylarisdae.sinaapp.com/data.html';
if (iframe.attachEvent) {
iframe.attachEvent('onload', loadfn);
} else {
iframe.onload = loadfn;
}
document.body.appendChild(iframe);
</script>
</body>
</html> |
data.html
<!doctype html> <html> <head> <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> <title>data页</title>
<style type="text/css">
</style>
</head>
<body>
<script type="text/javascript">
window.name = '拿到跨域的数据啦!'; // 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右
// 数据格式可以自定义,如json、字符串
</script>
</body>
</html> |
proxy.html
<!doctype html> <html> <head> <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> <title>window.name跨域demo</title>
<style type="text/css">
</style>
</head>
<body>
<span>这里是代理空文件</span>
<script type="text/javascript">
</script>
</body>
</html> |
|