原生JS实现跨域

本文著作权归饥人谷_Lyndon和饥人谷所有,转载请注明出处。


这是一篇对于跨域的总结,将涵盖跨域的四种方法:

  • jsonp
  • cors
  • 降域
  • postMessage

在回顾每种方法时都会结合自己的实践。


>>> 什么是跨域?

在介绍跨域之前首先要了解何为“同源策略”(Same Origin Policy),浏览器(注意:主体是浏览器)出于安全方面的考虑,只允许与本域(同协议、同域名、同端口)下的数据接口进行交互,不同源的客户端脚本在没有授权的情况下,是不能读写对方资源的。

可以设想一下:如果没有同源策略,如果我自己建了一个网站,然后在没有支付宝客户端脚本授权的情况下轻松操控支付宝的脚本,随意传入我的个人信息,或者获得其他用户支付宝的数据,那将是非常危险的。同源策略有效地阻止了诸如此类的危险行为。

但是请设想这样一种场景:我自己建设了一个网站,这时候需要在网站上建设一个天气控件,背后的数据我必须从一些天气网站或者数据接口中进行获取,但是由于同源策略的限制,我无法实现这一目标。因此跨域就应运而生了。JS在不同域之间进行数据传输或者通信,譬如AJAX向一个不同源的服务端去请求数据,或者利用JS获取页面中不同域的iframe数据,从而实现不同域数据的相互访问,这些情境归根结底都是跨域。


>>> 跨域方法1:jsonp

jsonp全称:json with padding,这个名称非常地形象。意思就是异步请求跨域服务端时,不直接返回数据,而是返回一个JS方法,数据是其中的参数。其实就相当于数据变成了馅料,填充(padding)在一个方法里面,然后返回并运行。

为什么会用这么巧妙的一种方法呢?实际上,在书写HTML时如果需要引用JQuery,只需要在页面中加上<script src = "http://code.jquery.com/xxx"></script>就可以了,之后在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,最原始的客户端页面和服务端页面代码如下:

<div class="container">
    <p class="show">0000</p>
    <button class="btn">change</button>
</div>
<script>
    function $(id){
        return document.querySelector(id);
    }
    $(".btn").addEventListener("click", function(){
        var xhr = new XMLHttpRequest();
        xhr.open("get", "/change", true);
        xhr.send();
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4 && xhr.status === 200){
                console.log(JSON.parse(xhr.responseText));
                append(JSON.parse(xhr.responseText));
            }
        }
    });
    function append(data){
        $(".show").innerText = data.data[0];
    }
</script>
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: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
  • 单独执行html

>>> 跨域方法2:CORS

使用CORS方法和AJAX原代码几近类似,主要工作是在服务端加上响应头res.header("Access-Control-Allow-Origin", "xxx"),只要响应头中包含了请求头(Origin),就可以实现跨域,相当于数据请求的决定权在于服务端是否同意,因此CORS对于代码的修改也只需修改服务端代码即可。

客户端和服务端的代码如下:

<div class="ct">
    <ul class="nums">
        <li>111</li>
        <li>222</li>
        <li>333</li>
    </ul>
    <button class="btn">换一组</button>
</div>
<script>
    function $(id){
        return document.querySelector(id);
    }

    $(".btn").addEventListener("click", function(){
        var xhr = new XMLHttpRequest();
        xhr.open("get", "http://b.com:8080/getNums", true);
        xhr.send();
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4 && xhr.status === 200){
                appendHtml(JSON.parse(xhr.responseText));
            }
        }
    })

    function appendHtml(nums){
        var html = "";
        for(var i = 0; i < nums.length; i++){
            html += "<li>" + nums[i] + "</li>";
        }
        console.log(html);
        $(".nums").innerHTML = html;
    }
</script>
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.comb.lyndon.com,它们拥有一致的父级域名:lyndon.com,因此可以进行降域从而实现跨域,而a.comb.com无法进行降域,同理,类似于a.jrg.comb.lik.com也不行。

降域的实现很简单,以刚才提及的使用场景为例:只需要在两个html文件的script中加入共同的代码document.domain="lyndon.com";即可。

以下展现a.html和b.html的代码:

<div class="main">
    <input type="text" placeholder="http://a.lyndon.com:8080/a.html">
</div>
<iframe src="http://b.lyndon.com:8080/b.html" frameborder="0"></iframe>
<script>
    document.querySelector(".main input").addEventListener("input", function(){
        console.log(this.value);
        window.frames[0].document.querySelector("#input").value = this.value;
    });
    document.domain = "lyndon.com";
</script>
<input type="text" id="input" placeholder="http://b.lyndon.com:8080/b.html">
<script>
    document.querySelector("#input").addEventListener("input", function(){
        console.log(this.value);
        window.parent.document.querySelector("input").value = this.value;
    });
    document.domain = "lyndon.com";
</script>

这里的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代码:

<div class="ct">
    <input type="text" placeholder="http://a.lyndon.com:8080/a.html">
</div>
<iframe src="http://localhost:8080/b.html" frameborder="0"></iframe>
<script>
    // 将输入的信息传递给页面上的不同域的iframe(b.html)
    document.querySelector(".ct input").addEventListener("input", function(){
        console.log(this.value);
        window.frames[0].postMessage(this.value, "http://localhost:8080/b.html");
    });
    // 监听b.html(本页面上的iframe)是否有message传递过来,如果有,将输入框中的内容换成iframe中input里的输入内容
    window.addEventListener("message", function(e){
        document.querySelector(".ct input").value = e.data;
        console.log(e.data);
    });
</script>
<input type="text" id="input" placeholder="http://b.lyndon.com:8080/b.html">
<script>
    // 当有输入的文字时,向父页面(a.html)发出message
    document.querySelector("#input").addEventListener("input", function () {
        window.parent.postMessage(this.value, "http://a.lyndon.com:8080/a.html");
    });
    // 监听a.html是否有message传递过来,如果有,将iframe输入框中的内容换成a.html中input里的输入内容
    window.addEventListener("message", function(e){
        document.querySelector("#input").value = e.data;
        console.log(e.data);
    });
</script>

所以归根结底,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);
    }
}
  • 最后的结果

>>> 总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容