跨域的几种办法原理详细

1、什么是跨域

获取一个页面的域:

 
 
  1. document.domain;
  2. // qianduanblog.com

为了页面和服务器的安全(?),脚本是不能访问非本域的动态网络资源,但可以访问如脚本、样式、图片、视频、音频等这些静态资源。

那什么是跨域呢?存在以下情况中之一,即发生跨域:

  • 网络协议不同,如http协议访问https协议。
  • 端口不同,如80端口访问8080端口。
  • 域名不同,如qianduanblog.com访问baidu.com
  • 子域名不同,如abc.qianduanblog.com访问def.qianduanblog.com

跨域请求,如获取百度首页的内容:

 
 
  1. // 本域:qianduanblog.com
  2. // 他域:www.baidu.com
  3. $.ajax("http://www.baidu.com");
  4. // 报错:XMLHttpRequest cannot load http://www.baidu.com/. Origin http://qianduanblog.com is not allowed by Access-Control-Allow-Origin.

2、前端跨域原理

既然无法使用传统的XMLHttpRequest(即AJAX)实现跨域获取内容,那么是否还有其他办法呢?已经知道,静态资源是没有跨域限制的,那么是否可以通过请求静态资源的方法来实现跨域呢?

答案是肯定的。

通常,在前端开发中,实现跨域都是把动态资源伪装成脚本来实现跨域。如:

 
 
  1. // 请求 http://qianduanblog.duapp.com/test/index.php?jsonp=testjsonp
  2. // 返回 testjsonp({"time":"2013-11-20 13:46:30"});

可以在页面中写上:

 
 
  1. <script>
  2. function testjsonp() {
  3. console.log(arguments[0]);
  4. }
  5. </script>
  6. <script src="http://qianduanblog.duapp.com/test/index.php?jsonp=testjsonp"></script>

以上就是最原始的利用JSONP实现前端跨域。会在控制台输出:

 
 
  1. Object {time: "2013-11-20 13:49:21"}

什么是JSONP,接下来会说到。

既然通过脚本可以读取他域上的动态资源,那么我们就可以动态创建script来读取他域的动态资源。

 
 
  1. <script>
  2. var url = "http://qianduanblog.duapp.com/test/index.php?jsonp=testjsonp";
  3. var script = document.createElement("script");
  4. var head = document.getElementsByTagName("head")[0];
  5.  
  6. script.src = url;
  7.  
  8. function testjsonp() {
  9. console.log(arguments[0]);
  10. }
  11.  
  12. script.onload = function() {
  13.  
  14. }
  15.  
  16. // 页面上插入该脚本
  17. head.appendChild(script);
  18. </script>

同样的道理,同样的结果,也会在页面输出:

 
 
  1. Object {time: "2013-11-20 13:55:45"}

3、前端跨域方法

3.1、JSONP

如上,已经简单的说明了,如何使用JSONP实现前端跨域,现在来仔细说说。

先了解,什么是JSON。

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition – December 1999的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 这些特性使JSON成为理想的数据交换语言。

在做前后端交互的时候,JSON是一个利器,在AJAX运用中愈加流行,学习掌握JSON是前端开发的必经之路。

再了解下,什么是JSONP。

简易说明,JSONP就是包装的JSON,把P理解为package更为合适。例JSON和JSONP:

 
 
  1. // JSON
  2. {
  3. "s": "b"
  4. };
  5.  
  6. // JSONP
  7. jsonp({
  8. "s": "b"
  9. });

如例,JSONP中JSON是jsonp方法的实参,这样写的目的是,打开这个JSONP就运行了jsonp方法。如我们单独请求该脚本地址:

 
 
  1. <script src="http://qianduanblog.duapp.com/test/index.php?jsonp=jsonp"></script>
  2. <!-- 报错:Uncaught ReferenceError: jsonp is not defined -->

出现预想的错误,jsonp方法未定义,证明了以上说的是正确的。所以在处理JSONP的时候,我们需要预先定义一个全局函数jsonp,然后在该函数中返回实参即可。即:

 
 
  1. <script>
  2. // 预先顶一个全局函数 jsonp
  3. function jsonp() {
  4.  
  5. // 输出该函数的实参
  6. console.log(arguments[0]);
  7. }
  8. </script>
  9. <script src="http://qianduanblog.duapp.com/test/index.php?jsonp=jsonp"></script>

所以结合动态创建script标签,可以这样写:

 
 
  1. function getTime(callback) {
  2. var url = "http://qianduanblog.duapp.com/test/index.php?jsonp=testjsonp";
  3. var script = document.createElement("script");
  4. var head = document.getElementsByTagName("head")[0];
  5.  
  6. script.src = url;
  7.  
  8. window.testjsonp = function() {
  9. callback(arguments[0]);
  10. }
  11.  
  12. script.onload = function() {
  13. // 移除该script
  14. script.parentNode.removeChild(script);
  15.  
  16. // 删除该script
  17. script = null;
  18.  
  19. // 删除方法
  20. window.testjsonp = null;
  21. }
  22.  
  23. // 页面上插入该脚本
  24. head.appendChild(script);
  25. }
  26.  
  27. // 跨域获取时间
  28. getTime(function(json) {
  29. alert(json.time);
  30. });

3.2、VAR

JSONP是利用全局方法来实现跨域读取,当然也可以利用全局变量来实现跨域读取。例:

 
 
  1. var window.testvar = "123";
  2. alert(window.testvar);

并且这个方法比JSONP要来的更加简单一点,具体实现方法例:

 
 
  1. function getTime(callback) {
  2. var url = "http://qianduanblog.duapp.com/test/index.php?var=testvar";
  3. var script = document.createElement("script");
  4. var head = document.getElementsByTagName("head")[0];
  5.  
  6. script.src = url;
  7.  
  8. script.onload = function() {
  9. // 回调
  10. callback(testvar);
  11.  
  12. // 移除该script
  13. script.parentNode.removeChild(script);
  14.  
  15. // 删除该script
  16. script = null;
  17.  
  18. // 删除变量
  19. window.testvar = null;
  20. }
  21.  
  22. // 页面上插入该脚本
  23. head.appendChild(script);
  24. }
  25.  
  26. getTime(function(json) {
  27. alert(json.time);
  28. });

3.3、修缮与扩展

在跨域读取动态内容,无论是利用JSONP还是VAR方法,都需要面对覆盖全局方法、全局变量的危险,解决这样的情况,我们可以生成一个唯一的函数名或者变量名来尽可能的防止出现这样的情况,例:

 
 
  1. functionName = "yundanran" + new Date().getTime();
  2. varName = "yundanran" + new Date().getTime();

这样的重复的概率就大大降低了。

第二个问题是,如何扩展该方法,两种方法大都雷同,可以合二为一。综合例:

 
 
  1. /**
  2. * 跨域读取
  3. * @param {String} 跨域方法,jsonp或var
  4. * @param {String} 跨域地址
  5. * @param {Function} 跨域成功回调
  6. * @param {Function} 跨域失败回调
  7. * @return {Undefined} 无返回
  8. * @author 云淡然
  9. * @version 1.0
  10. * 2013年11月20日14:30:51
  11. */
  12.  
  13. function crossDomain(type, url, onsuccess, onerror) {
  14. // 设置回调为
  15. var callbackName = "prefix" + new Date().getTime() + "callback";
  16.  
  17. // 创建回调函数
  18. if (type == "jsonp") {
  19. window[callbackName] = function () {
  20. if (onsuccess) onsuccess(arguments[0]);
  21. }
  22. }
  23.  
  24. // 创建一个 script 的 DOM 对象
  25. script = document.createElement("script");
  26.  
  27. // 设置其同步属性
  28. script.async = true;
  29.  
  30. // 设置其地址
  31. script.src = url.replace(/#.*$/, "") + (/\?/.test(url) ? "&" : "?") + type + "=" + callbackName;
  32.  
  33. // 监听
  34. script.onload = script.onreadystatechange = function () {
  35. if (!script.readyState || /loaded|complete/.test(script.readyState)) {
  36. script.onload = script.onreadystatechange = null;
  37.  
  38. if (type == "var") {
  39. if (onsuccess) onsuccess(window[callbackName]);
  40. }
  41.  
  42. // 移除该 script 的 DOM 对象
  43. if (script.parentNode) {
  44. script.parentNode.removeChild(script);
  45. }
  46.  
  47. // 删除函数或变量
  48. window[callbackName] = null;
  49. }
  50. }
  51.  
  52. script.onerror = function () {
  53. if (onerror) onerror();
  54. }
  55.  
  56. // 插入head
  57. head.appendChild(script);
  58. }

4、demo

demo地址:JS学习35:js使用JSONP、VAR实现前端跨域http://demo.qianduanblog.com/2858/1.html

5、参考资料

JS学习35:js使用JSONP、VAR实现前端跨域http://www.whatwg.org/specs/web-apps/current-work/multipage/origin-0.html


3、肿么跨域

下面为了更好的讲解和测试,我们可以通过修改hosts文件来模拟跨域的效果,hosts文件在C:\Windows\System32\drivers\etc 文件夹下。在下面加3行:

127.0.0.1 www.a.com

127.0.0.1 a.com

127.0.0.1 www.b.com

3.1、跨域代理

一种简单的办法,就是把跨域的工作交给服务器,从后台获取其他站点的数据再返回给前台,也就是跨域代理(Cross Domain Proxy)

这种方法似乎蛮简单的,改动也不太大。不过就是http请求多了些,响应慢了些,服务器的负载重了些~

 

3.2、document.domain+iframe

对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。

举www.a.com/a.html和a.com/b.html为例,只需在a.html中添加一个b.html的iframe,并且设置两个页面的document.domain都为’a.com’(只能为主域名),两个页面之间即可互相访问了,代码如下:

www.a.com/a.html中的script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
document.domain= 'a.com' ;
var  ifr = document.createElement( 'iframe' );
ifr.src =  'http://a.com/b.html' ;
ifr.style.display =  'none' ;
document.body.appendChild(ifr);
ifr.onload =  function (){
   //获取iframe的document对象
   //W3C的标准方法是iframe.contentDocument,
   //IE6、7可以使用document.frames[ID].document
   //为了更好兼容,可先获取iframe的window对象iframe.contentWindow
   var  doc = ifr.contentDocument || ifr.contentWindow.document;
   // 在这里操纵b.html
   alert(doc.getElementById( "test" ).innerHTML);
};

 

a.com/b.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
< html >
< head >
< title ></ title >
< script  type = "text/javascript" >
   document.domain='a.com';
</ script >
</ head >
< body >
< h1  id = "test" >Hello World</ h1 >
</ body >
</ html >

如果b.html要访问a.html,可在子窗口(iframe)中通过window.parent来访问父窗口的window对象,然后就可以为所欲为了(window对象都有了,还有啥不行的),同理子窗口也可以和子窗口之间通信。

于是,我们可以通过b.html的XMLHttpRequest来获取数据,再传给a.html,从而解决跨子域获取数据的问题。

但是这种方法只支持同一根域名下的页面,如果不同根域名(例如baidu.com想访问google.com)那就无能为力了。

 

3.3、动态script标签(Dynamic Script Tag)

这种方法也叫“动态脚本注入”。详情

这种技术克服了XMLHttpRequest的最大限制,也就是跨域请求数据。直接用JavaScript创建一个新的脚本标签,然后设置它的src属性为不同域的URL。

www.a.com/a.html中的script

1
2
3
4
5
var  dynScript = document.createElement( 'script' );
dynScript.src =  'http://www.b.com/b.js' ;
dynScript.setAttribute( "type" "text/javascript" );
document.getElementsByTagName( 'head' )[0]
     .appendChild(dynScript);

通过动态标签注入的必须是可执行的JavaScript代码,因此无论是你的数据格式是啥(xml、json等),都必须封装在一个回调函数中。一个回调函数如下:

www.a.com/a.html中的script

1
2
3
4
function  dynCallback(data){
   //处理数据, 此处简单示意一下
   alert(data.content);
}

在这个例子中,www.b.com/b.js需要将数据封装在上面这个dynCallback函数中,如下:

1dynCallback({content:'Hello World'})

我们看到了让人开心的结果,Hello World~

 

不过动态脚本注入还是存在不少问题的,下面我们拿它和XMLHttpRequest来对比一下:

 XmlHttpRequestDynamic Script Tag
跨浏览器兼容NoYes
跨域限制YesNo
接收HTTP状态YesNo (除了200)
支持Get、PostYesNo (GET only)
发送、接收HTTP头YesNo
接收XMLYesYes
接收JSONYesYes
支持同步、异步YesNo (只能异步)

 

可以看出,动态脚本注入还是有不少限制,只能使用Get,不能像XHR一样判断Http状态等。

而且使用动态脚本注入的时候必须注意安全问题。因为JavaScript没有任何权限与访问控制的概念,通过动态脚本注入的代码可以完全控制整个页面,所以引入外部来源的代码必须多加小心。


3.4、iframe+location.hash

这种方法比上面两种稍微繁琐一点,原理如下:

www.a.com下的a.html想和www.b.com下的b.html通信(在a.html中动态创建一个b.html的iframe来发送请求);

但是由于“同源策略”的限制他们无法进行交流(b.html无法返回数据),于是就找个中间人:www.a.com下的c.html(注意是www.a.com下的);

b.html将数据传给c.html(b.html中创建c.html的iframe),由于c.html和a.html同源,于是可通过c.html将返回的数据传回给a.html,从而达到跨域的效果。

三个页面之间传递参数用的是location.hash(也就是www.a.html#sayHello后面的’#sayHello’),改变hash并不会导致页面刷新(这点很重要)。

具体代码如下:

www.a.com/a.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//通过动态创建iframe的hash发送请求
function  sendRequest(){
   var  ifr = document.createElement( 'iframe' );
   ifr.style.display =  'none' ;
   //跨域发送请求给b.html, 参数是sayHello
   ifr.src =  'http://www.b.com/b.html#sayHello' ;
   document.body.appendChild(ifr);
}
//获取返回值的方法
function  checkHash() {
   var  data = location.hash ?
      location.hash.substring(1) :  '' ;
   if  (data) {
     //处理返回值
     alert(data);
     location.hash= '' ;
   }
}
//定时检查自己的hash值
setInterval(checkHash, 2000);
window.onload = sendRequest;

 

www.b.com/b.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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){
   // ie、chrome的安全机制无法修改parent.location.hash,
   // 所以要利用一个中间的www.a.com域下的代理iframe
   var  proxy = document.createElement( 'iframe' );
   proxy.style.display =  'none' ;
   // 注意该文件在"www.a.com"域下
   proxy.src =  'http://www.a.com/c.html' +hash;
   document.body.appendChild(proxy);
}
window.onload = checkHash;

 

www.a.com/c.html

1
2
3
4
5
//因为c.html和a.html属于同一个域,
//所以可以改变其location.hash的值
//可通过parent.parent获取a.html的window对象
parent.parent.location.hash =
     self.location.hash.substring(1);

可能有人会有疑问,既然c.html已经获取了a.html的window对象了,为何不直接修改它的dom或者传递参数给某个变量呢?

原因是在c.html中修改 a.html的dom或者变量会导致页面的刷新,a.html会重新访问一次b.html,b.html又会访问c.html,造成死循环……囧呀~

所以只能通过location.hash了。这样做也有些不好的地方,诸如数据容量是有限的(受url长度的限制),而且数据暴露在url中(用户可以随意修改)……

 

3.5、postMessage(html5)

html5中有个很酷的功能,就是跨文档消息传输Cross Document Messaging)。新一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。

使用方法如下:

1otherWindow.postMessage(message, targetOrigin);

说明:

  • otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性,window.open的返回值等。
  • message: 所要发送的数据,string类型。
  • targetOrigin: 用于限制otherWindow,“*”表示不作限制。

www.a.com/a.html中的代码:

html:

1<iframe id="ifr" src="http://www.b.com/b.html"></iframe>

script:

1
2
3
4
5
6
window.onload =  function () {
     var  ifr = document.getElementById( 'ifr' );
     // 若写成'http://www.c.com'就不会执行postMessage了
     var  targetOrigin =  'http://www.b.com' ;
     ifr.contentWindow.postMessage( 'sayHello' , targetOrigin);
};

www.b.com/b.html的script

1
2
3
4
5
6
7
8
//通过message事件来通信,实在太爽了
window.addEventListener( 'message' function (e){
   // 通过origin属性判断消息来源地址
   if  (e.origin ==  'http://www.a.com'  &&
     e.data== 'sayHello' ) {
     alert( 'Hello World' );
   }
},  false );

 

3.6、使用flash

由于本人对flash不怎么熟悉,此处暂时忽略之~

 

3.7、Cross Frame

行文至此,突然在口碑网UED博客上看到了一篇 《跨域资源共享的10种方式》,对跨域的多种方法都有介绍(虽然有源码,但多数都是直接调用YUI库的,比较难看出原理)。

里面提到了Cross Frame这种方法,似乎挺不错的,改日一定翻源码来研究。

 

4、总结

研究了几天,虽然对多种跨域方法都有所了解了,但是真要投入应用还是明显不够的(还是需要借助一些js库)。

每种方法都有其优缺点,使用的时候其实应该将多种跨域方法进一步封装一下,统一调用的接口,利用js来自动判断哪种方法更为适用 。


  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值