本文著作权归饥人谷_Lyndon和饥人谷所有,转载请注明出处。
这是一篇对于跨域的总结,将涵盖跨域的四种方法:
- jsonp
- cors
- 降域
- postMessage
在回顾每种方法时都会结合自己的实践。
>>> 什么是跨域?
在介绍跨域之前首先要了解何为“同源策略”(Same Origin Policy),浏览器(注意:主体是浏览器)出于安全方面的考虑,只允许与本域(同协议、同域名、同端口)下的数据接口进行交互,不同源的客户端脚本在没有授权的情况下,是不能读写对方资源的。
可以设想一下:如果没有同源策略,如果我自己建了一个网站,然后在没有支付宝客户端脚本授权的情况下轻松操控支付宝的脚本,随意传入我的个人信息,或者获得其他用户支付宝的数据,那将是非常危险的。同源策略有效地阻止了诸如此类的危险行为。
但是请设想这样一种场景:我自己建设了一个网站,这时候需要在网站上建设一个天气控件,背后的数据我必须从一些天气网站或者数据接口中进行获取,但是由于同源策略的限制,我无法实现这一目标。因此跨域就应运而生了。JS在不同域之间进行数据传输或者通信,譬如AJAX向一个不同源的服务端去请求数据,或者利用JS获取页面中不同域的iframe数据,从而实现不同域数据的相互访问,这些情境归根结底都是跨域。
>>> 跨域方法1:jsonp
jsonp全称:json with padding,这个名称非常地形象。意思就是异步请求跨域服务端时,不直接返回数据,而是返回一个JS方法,数据是其中的参数。其实就相当于数据变成了馅料,填充(padding)在一个方法里面,然后返回并运行。
为什么会用这么巧妙的一种方法呢?实际上,在书写HTML时如果需要引用JQuery,只需要在页面中加上就可以了,之后在HTML中就能调用JQuery中已经封装好的各种方法,但是
code.jquery.com
与请求页面的域名肯定不一样,jsonp正是借鉴了这一点来实现跨域的数据访问。
我的电脑是Windows系统,首先我在我的host文件中添加以下新域名:
# New Hosts
127.0.0.1 a.com
127.0.0.1 b.com
127.0.0.1 a.lyndon.com
127.0.0.1 b.lyndon.com
为何要在host文件中添加这些?因为在浏览器地址栏中输入域名后,需要根据域名去寻找对应的IP地址,这就是所谓的DNS解析,首先是在浏览器的缓存中寻找,如果没有找到,就去系统的host文件中寻找,再没有找到,就去路由器缓存中找,再往深处就是ISP DNS,根域名服务器。
我在本地启动server-mock,最原始的客户端页面和服务端页面代码如下:
0000
app.get('/change', function(req, res){
array = [
"1111",
"2222",
"3333",
"4444",
"5555"
];
var data = [];
data.push(array[parseInt(Math.random() * array.length)]);
res.send({
data: data
});
});
在这种情境下,是能够进行正常请求的,因为请求页面请求的是同域服务端的数据。
但是当我稍对客户端页面的代码做更改,就会出现不一样的结果。
xhr.open("get", "http://b.lyndon.com:8080/change", true);
因为http://a.com:8080
和http://b.lyndon.com:8080
不同域,浏览器限制了我的跨域请求。
这时候使用jsonp的思路来做一些调整,这时候我就不再使用AJAX方法,而是加入一个script
标签,点击“change”按钮时,script
的src
属性将直接从服务端返回一个方法(回调函数),数据将作为其中的参数。客户端页面和服务端页面代码如下:
function $(id){
return document.querySelector(id);
}
// jsonp
$(".btn").addEventListener("click", function(){
var script = document.createElement("script");
script.src = "http://b.lyndon.com:8080/change?callback=process";
document.head.appendChild(script);
// 及时删除,防止加载过多的JS
document.head.removeChild(script);
});
function process(data){
$(".show").innerText = data[0];
}
app.get('/change', function(req, res){
array = [
"1111",
"2222",
"3333",
"4444",
"5555"
];
var data = [];
data.push(array[parseInt(Math.random() * array.length)]);
res.send(req.query.callback + "(" + JSON.stringify(data) + ")");
});
因为在客户端加入了回调函数,因此在服务端稍作更改即可,返回的是一个function_name(data)
,这样一来,即使脱离了server-mock
,也可以愉快地执行了。
- 客户端域名为:
a.com:8080
- 单独执行html
>>> 跨域方法2:CORS
使用CORS方法和AJAX原代码几近类似,主要工作是在服务端加上响应头res.header("Access-Control-Allow-Origin", "xxx"),只要响应头中包含了请求头(Origin),就可以实现跨域,相当于数据请求的决定权在于服务端是否同意,因此CORS对于代码的修改也只需修改服务端代码即可。
客户端和服务端的代码如下:
- 111
- 222
- 333
app.get('/getNums', function(req, res) {
var array = [
"444",
"555",
"666",
"777",
"888",
"999",
"000"
]
var data = [];
for(var i = 0; i < 3; i++){
data.push(array[parseInt(Math.random() * array.length)]);
array.splice(parseInt(Math.random() * array.length), 1);
}
res.header("Access-Control-Allow-Origin", "http://b.com:8080");
res.send(data);
});
在以上的服务端代码中,设定的允许域为http://b.com:8080
,在进行访问时,如果打开localhost:8080
,虽然存在数据交换但是无法更新页面。
将访问页的域名改为http://b.com:8080
即可正常访问。
如果为了方便,希望来自所有域的请求都可以自由获取服务端的数据,那么只需要改为:res.header("Access-Control-Allow-Origin", "*");
即可。
>>> 跨域方法3:降域
降域使得处于不同域的两个HTML文件实现相互访问或相互操作成为可能。一个非常典型的使用场景:在一个页面中存在一个iframe
,但是iframe
中的网页与包含网页不同域,使用降域的方法可以实现两个页面内容的同步更改,因为只有处于同域条件才能使用JS操作其中的元素。
需要注意的一点是:降域的使用是存在限制的,域名中需要有一致的父级域名才可以使用降域。
比如:a.lyndon.com
和b.lyndon.com
,它们拥有一致的父级域名:lyndon.com
,因此可以进行降域从而实现跨域,而a.com
和b.com
无法进行降域,同理,类似于a.jrg.com
和b.lik.com
也不行。
降域的实现很简单,以刚才提及的使用场景为例:只需要在两个html文件的script
中加入共同的代码document.domain="lyndon.com";
即可。
以下展现a.html和b.html的代码:
这里的window.frames
返回的是一个类数组对象,成员为页面内所有的框架,包括frame元素和iframe元素,window.frames
内的每个成员是框架内的窗口(框架的window对象),如果需要获取每个框架的DOM树,就需要像以上代码一样写成window.frames[0].document
的形式。
在第二段(b.html)的代码中,iframe内部使用的window.parent
指向的是父页面。因此第二段代码中的window.parent.document.querySelector("input")
对应的是第一段代码中的input
,这样的做法在两个代码文件中建立起了相互的连接。
实际效果如下:
>>> 跨域方法4:postMessage(window对象才有postMessage方法)
介绍postMessage之前,需要明确一点:iframe元素遵守同源政策,只有当父页面与框架页面来自同一个域名,两者之间才可以用脚本通信,否则只有使用window.postMessage方法。
因此可以明确得知:postMessage的使用范围是更加广阔的,且当降域不可行时(如:a.com和b.com无法降域)时,使用postMessage会是一个不错的选择。
这里依然以页面与嵌套的iframe消息传递这一场景为例。postMessage(data, origin)方法接受两个参数:
-
data
:要传递的数据,为了让所有浏览器都能正常解析,建议使用:JSON.stringify()
方法将对象参数序列化 -
origin
:目标窗口的源,postMessage()方法会将message传递给指定窗口,同CORS中一样,如果将origin
设置为*
,就可以将message传递给任意窗口
与postMessage(发送消息)对应的是接收消息,因此与postMessage相互搭配的是监听window的message事件。
以下给出两份添加注释的html代码:
所以归根结底,postMessage就是一个信息交叉的过程。实际执行效果是:
>>> 附加一个自己的实践:使用jsonp获取百度联想词
- 首先在Console中Network查看百度搜索词的联想词获取地址
联想词的数据地址为:https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=%E6%BC%82%E4%BA%AE%E7%9A%84&json=1&p=3&sid=1452_21099_18559_21673&req=2&csor=3&pwd=%20&cb=jQuery110208414170774720962_1486043984005&_=1486043984013
精简URL,可以发现:"https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=" + string
即可返回联想词。后面需要加上callback"cb=" + function name
来进行返回结果的处理。
- 动态获取跨域数据
function $(id){
if(document.querySelectorAll(id).length > 1){
return document.querySelectorAll(id);
}else{
return document.querySelector(id);
}
}
var txt = $("#txt"),
ul = $("#baidusug"),
script = null;
txt.onkeyup = function (){
ul.innerHTML = "";
if (script) {
document.body.removeChild(script);
}
script = document.createElement("script");
script.src = "https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=" + txt.value + "&cb=process";
document.body.appendChild(script);
};
function process(json){
for(var i = 0; i < json["s"].length; i++){
var li = document.createElement("li");
li.innerHTML = json.s[i];
ul.appendChild(li);
}
}
- 最后的结果
>>> 总结
在今后的使用过程中,只需要辨清场景,然后按照因地制宜的原则选择一种跨域方法就好,没有必要完全依赖于一种特定的方法。一言以蔽之:没有最正确的,只有最适合的。