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实现前端跨域。会在控制台输出:
- 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地址:http://demo.qianduanblog.com/2858/1.html
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
|
a.com/b.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 | |
通过动态标签注入的必须是可执行的JavaScript代码,因此无论是你的数据格式是啥(xml、json等),都必须封装在一个回调函数中。一个回调函数如下:
www.a.com/a.html中的script
1 2 3 4 | |
在这个例子中,www.b.com/b.js需要将数据封装在上面这个dynCallback函数中,如下:
1 |
|
我们看到了让人开心的结果,Hello World~
不过动态脚本注入还是存在不少问题的,下面我们拿它和XMLHttpRequest来对比一下:
| XmlHttpRequest | Dynamic Script Tag |
跨浏览器兼容 | No | Yes |
跨域限制 | Yes | No |
接收HTTP状态 | Yes | No (除了200) |
支持Get、Post | Yes | No ( |
发送、接收HTTP头 | Yes | No |
接收XML | Yes | Yes |
接收JSON | Yes | Yes |
支持同步、异步 | Yes | No (只能异步) |
可以看出,动态脚本注入还是有不少限制,只能使用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
|
www.b.com/b.html
|
www.a.com/c.html
|
可能有人会有疑问,既然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+ 。
使用方法如下:
|
说明:
- otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性,window.open的返回值等。
- message: 所要发送的数据,string类型。
- targetOrigin: 用于限制otherWindow,“*”表示不作限制。
www.a.com/a.html中的代码:
html:
|
script:
|
www.b.com/b.html的script
|
3.6、使用flash
由于本人对flash不怎么熟悉,此处暂时忽略之~
3.7、Cross Frame
行文至此,突然在口碑网UED博客上看到了一篇 《跨域资源共享的10种方式》,对跨域的多种方法都有介绍(虽然有源码,但多数都是直接调用YUI库的,比较难看出原理)。
里面提到了Cross Frame这种方法,似乎挺不错的,改日一定翻源码来研究。
4、总结
研究了几天,虽然对多种跨域方法都有所了解了,但是真要投入应用还是明显不够的(还是需要借助一些js库)。
每种方法都有其优缺点,使用的时候其实应该将多种跨域方法进一步封装一下,统一调用的接口,利用js来自动判断哪种方法更为适用 。