原生JS实现跨域

本文著作权归饥人谷_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
    });
});

在这种情境下,是能够进行正常请求的,因为请求页面请求的是同域服务端的数据。

原生JS实现跨域_第1张图片

但是当我稍对客户端页面的代码做更改,就会出现不一样的结果。

xhr.open("get", "http://b.lyndon.com:8080/change", true);
原生JS实现跨域_第2张图片

因为http://a.com:8080http://b.lyndon.com:8080不同域,浏览器限制了我的跨域请求。

这时候使用jsonp的思路来做一些调整,这时候我就不再使用AJAX方法,而是加入一个script标签,点击“change”按钮时,scriptsrc属性将直接从服务端返回一个方法(回调函数),数据将作为其中的参数。客户端页面和服务端页面代码如下:

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
原生JS实现跨域_第3张图片
  • 单独执行html
原生JS实现跨域_第4张图片

>>> 跨域方法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,虽然存在数据交换但是无法更新页面。

原生JS实现跨域_第5张图片

将访问页的域名改为http://b.com:8080即可正常访问。

原生JS实现跨域_第6张图片

如果为了方便,希望来自所有域的请求都可以自由获取服务端的数据,那么只需要改为:res.header("Access-Control-Allow-Origin", "*");即可。


>>> 跨域方法3:降域

降域使得处于不同域的两个HTML文件实现相互访问或相互操作成为可能。一个非常典型的使用场景:在一个页面中存在一个iframe,但是iframe中的网页与包含网页不同域,使用降域的方法可以实现两个页面内容的同步更改,因为只有处于同域条件才能使用JS操作其中的元素。

需要注意的一点是:降域的使用是存在限制的,域名中需要有一致的父级域名才可以使用降域

比如:a.lyndon.comb.lyndon.com,它们拥有一致的父级域名:lyndon.com,因此可以进行降域从而实现跨域,而a.comb.com无法进行降域,同理,类似于a.jrg.comb.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,这样的做法在两个代码文件中建立起了相互的连接。

实际效果如下:

原生JS实现跨域_第7张图片

>>> 跨域方法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就是一个信息交叉的过程。实际执行效果是:

原生JS实现跨域_第8张图片

>>> 附加一个自己的实践:使用jsonp获取百度联想词

  • 首先在Console中Network查看百度搜索词的联想词获取地址
原生JS实现跨域_第9张图片

联想词的数据地址为: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);
    }
}
  • 最后的结果
原生JS实现跨域_第10张图片

>>> 总结

在今后的使用过程中,只需要辨清场景,然后按照因地制宜的原则选择一种跨域方法就好,没有必要完全依赖于一种特定的方法。一言以蔽之:没有最正确的,只有最适合的。

你可能感兴趣的:(原生JS实现跨域)