原生JS實(shí)現(xiàn)跨域

本文著作權(quán)歸饑人谷_Lyndon和饑人谷所有膀估,轉(zhuǎn)載請(qǐng)注明出處参袱。


這是一篇對(duì)于跨域的總結(jié)答朋,將涵蓋跨域的四種方法:

  • jsonp
  • cors
  • 降域
  • postMessage

在回顧每種方法時(shí)都會(huì)結(jié)合自己的實(shí)踐续镇。


>>> 什么是跨域镀裤?

在介紹跨域之前首先要了解何為“同源策略”(Same Origin Policy)卿樱,瀏覽器(注意:主體是瀏覽器)出于安全方面的考慮僚害,只允許與本域(同協(xié)議、同域名繁调、同端口)下的數(shù)據(jù)接口進(jìn)行交互萨蚕,不同源的客戶端腳本在沒有授權(quán)的情況下,是不能讀寫對(duì)方資源的蹄胰。

可以設(shè)想一下:如果沒有同源策略岳遥,如果我自己建了一個(gè)網(wǎng)站,然后在沒有支付寶客戶端腳本授權(quán)的情況下輕松操控支付寶的腳本裕寨,隨意傳入我的個(gè)人信息浩蓉,或者獲得其他用戶支付寶的數(shù)據(jù)派继,那將是非常危險(xiǎn)的。同源策略有效地阻止了諸如此類的危險(xiǎn)行為捻艳。

但是請(qǐng)?jiān)O(shè)想這樣一種場(chǎng)景:我自己建設(shè)了一個(gè)網(wǎng)站驾窟,這時(shí)候需要在網(wǎng)站上建設(shè)一個(gè)天氣控件,背后的數(shù)據(jù)我必須從一些天氣網(wǎng)站或者數(shù)據(jù)接口中進(jìn)行獲取认轨,但是由于同源策略的限制绅络,我無法實(shí)現(xiàn)這一目標(biāo)。因此跨域就應(yīng)運(yùn)而生了嘁字。JS在不同域之間進(jìn)行數(shù)據(jù)傳輸或者通信恩急,譬如AJAX向一個(gè)不同源的服務(wù)端去請(qǐng)求數(shù)據(jù),或者利用JS獲取頁面中不同域的iframe數(shù)據(jù)拳锚,從而實(shí)現(xiàn)不同域數(shù)據(jù)的相互訪問假栓,這些情境歸根結(jié)底都是跨域。


>>> 跨域方法1:jsonp

jsonp全稱:json with padding霍掺,這個(gè)名稱非常地形象匾荆。意思就是異步請(qǐng)求跨域服務(wù)端時(shí),不直接返回?cái)?shù)據(jù)杆烁,而是返回一個(gè)JS方法牙丽,數(shù)據(jù)是其中的參數(shù)。其實(shí)就相當(dāng)于數(shù)據(jù)變成了餡料兔魂,填充(padding)在一個(gè)方法里面烤芦,然后返回并運(yùn)行。

為什么會(huì)用這么巧妙的一種方法呢析校?實(shí)際上构罗,在書寫HTML時(shí)如果需要引用JQuery,只需要在頁面中加上<script src = "http://code.jquery.com/xxx"></script>就可以了智玻,之后在HTML中就能調(diào)用JQuery中已經(jīng)封裝好的各種方法遂唧,但是code.jquery.com與請(qǐng)求頁面的域名肯定不一樣,jsonp正是借鑒了這一點(diǎn)來實(shí)現(xiàn)跨域的數(shù)據(jù)訪問吊奢。

我的電腦是Windows系統(tǒng)盖彭,首先我在我的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文件中添加這些?因?yàn)樵跒g覽器地址欄中輸入域名后页滚,需要根據(jù)域名去尋找對(duì)應(yīng)的IP地址召边,這就是所謂的DNS解析,首先是在瀏覽器的緩存中尋找裹驰,如果沒有找到隧熙,就去系統(tǒng)的host文件中尋找,再?zèng)]有找到邦马,就去路由器緩存中找贱鼻,再往深處就是ISP DNS宴卖,根域名服務(wù)器滋将。

我在本地啟動(dòng)server-mock邻悬,最原始的客戶端頁面和服務(wù)端頁面代碼如下:

<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
    });
});

在這種情境下,是能夠進(jìn)行正常請(qǐng)求的随闽,因?yàn)檎?qǐng)求頁面請(qǐng)求的是同域服務(wù)端的數(shù)據(jù)父丰。

但是當(dāng)我稍對(duì)客戶端頁面的代碼做更改,就會(huì)出現(xiàn)不一樣的結(jié)果掘宪。

xhr.open("get", "http://b.lyndon.com:8080/change", true);

因?yàn)?code>http://a.com:8080和http://b.lyndon.com:8080不同域蛾扇,瀏覽器限制了我的跨域請(qǐng)求。

這時(shí)候使用jsonp的思路來做一些調(diào)整魏滚,這時(shí)候我就不再使用AJAX方法镀首,而是加入一個(gè)script標(biāo)簽,點(diǎn)擊“change”按鈕時(shí)鼠次,scriptsrc屬性將直接從服務(wù)端返回一個(gè)方法(回調(diào)函數(shù))更哄,數(shù)據(jù)將作為其中的參數(shù)⌒瓤埽客戶端頁面和服務(wù)端頁面代碼如下:

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);
    // 及時(shí)刪除成翩,防止加載過多的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) + ")");
});

因?yàn)樵诳蛻舳思尤肓嘶卣{(diào)函數(shù),因此在服務(wù)端稍作更改即可赦役,返回的是一個(gè)function_name(data)麻敌,這樣一來,即使脫離了server-mock掂摔,也可以愉快地執(zhí)行了术羔。

  • 客戶端域名為:a.com:8080
  • 單獨(dú)執(zhí)行html

>>> 跨域方法2:CORS

使用CORS方法和AJAX原代碼幾近類似,主要工作是在服務(wù)端加上響應(yīng)頭res.header("Access-Control-Allow-Origin", "xxx")乙漓,只要響應(yīng)頭中包含了請(qǐng)求頭(Origin)级历,就可以實(shí)現(xiàn)跨域,相當(dāng)于數(shù)據(jù)請(qǐng)求的決定權(quán)在于服務(wù)端是否同意簇秒,因此CORS對(duì)于代碼的修改也只需修改服務(wù)端代碼即可鱼喉。

客戶端和服務(wù)端的代碼如下:

<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);
});

在以上的服務(wù)端代碼中,設(shè)定的允許域?yàn)?code>http://b.com:8080趋观,在進(jìn)行訪問時(shí)扛禽,如果打開localhost:8080,雖然存在數(shù)據(jù)交換但是無法更新頁面皱坛。

將訪問頁的域名改為http://b.com:8080即可正常訪問编曼。

如果為了方便,希望來自所有域的請(qǐng)求都可以自由獲取服務(wù)端的數(shù)據(jù)剩辟,那么只需要改為:res.header("Access-Control-Allow-Origin", "*");即可掐场。


>>> 跨域方法3:降域

降域使得處于不同域的兩個(gè)HTML文件實(shí)現(xiàn)相互訪問或相互操作成為可能往扔。一個(gè)非常典型的使用場(chǎng)景:在一個(gè)頁面中存在一個(gè)iframe,但是iframe中的網(wǎng)頁與包含網(wǎng)頁不同域熊户,使用降域的方法可以實(shí)現(xiàn)兩個(gè)頁面內(nèi)容的同步更改萍膛,因?yàn)橹挥刑幱谕驐l件才能使用JS操作其中的元素。

需要注意的一點(diǎn)是:降域的使用是存在限制的嚷堡,域名中需要有一致的父級(jí)域名才可以使用降域蝗罗。

比如:a.lyndon.comb.lyndon.com,它們擁有一致的父級(jí)域名:lyndon.com蝌戒,因此可以進(jìn)行降域從而實(shí)現(xiàn)跨域串塑,而a.comb.com無法進(jìn)行降域,同理北苟,類似于a.jrg.comb.lik.com也不行桩匪。

降域的實(shí)現(xiàn)很簡(jiǎn)單,以剛才提及的使用場(chǎng)景為例:只需要在兩個(gè)html文件的script中加入共同的代碼document.domain="lyndon.com";即可友鼻。

以下展現(xiàn)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返回的是一個(gè)類數(shù)組對(duì)象傻昙,成員為頁面內(nèi)所有的框架,包括frame元素和iframe元素桃移,window.frames內(nèi)的每個(gè)成員是框架內(nèi)的窗口(框架的window對(duì)象)屋匕,如果需要獲取每個(gè)框架的DOM樹,就需要像以上代碼一樣寫成window.frames[0].document的形式借杰。

在第二段(b.html)的代碼中过吻,iframe內(nèi)部使用的window.parent指向的是父頁面。因此第二段代碼中的window.parent.document.querySelector("input")對(duì)應(yīng)的是第一段代碼中的input蔗衡,這樣的做法在兩個(gè)代碼文件中建立起了相互的連接纤虽。

實(shí)際效果如下:


>>> 跨域方法4:postMessage(window對(duì)象才有postMessage方法

介紹postMessage之前,需要明確一點(diǎn):iframe元素遵守同源政策绞惦,只有當(dāng)父頁面與框架頁面來自同一個(gè)域名逼纸,兩者之間才可以用腳本通信,否則只有使用window.postMessage方法济蝉。

因此可以明確得知:postMessage的使用范圍是更加廣闊的杰刽,且當(dāng)降域不可行時(shí)(如:a.com和b.com無法降域)時(shí),使用postMessage會(huì)是一個(gè)不錯(cuò)的選擇王滤。

這里依然以頁面與嵌套的iframe消息傳遞這一場(chǎng)景為例贺嫂。postMessage(data, origin)方法接受兩個(gè)參數(shù):

  • data:要傳遞的數(shù)據(jù),為了讓所有瀏覽器都能正常解析雁乡,建議使用:JSON.stringify()方法將對(duì)象參數(shù)序列化
  • origin:目標(biāo)窗口的源第喳,postMessage()方法會(huì)將message傳遞給指定窗口,同CORS中一樣踱稍,如果將origin設(shè)置為*曲饱,就可以將message傳遞給任意窗口

與postMessage(發(fā)送消息)對(duì)應(yīng)的是接收消息悠抹,因此與postMessage相互搭配的是監(jiān)聽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");
    });
    // 監(jiān)聽b.html(本頁面上的iframe)是否有message傳遞過來扩淀,如果有楔敌,將輸入框中的內(nèi)容換成iframe中input里的輸入內(nèi)容
    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>
    // 當(dāng)有輸入的文字時(shí),向父頁面(a.html)發(fā)出message
    document.querySelector("#input").addEventListener("input", function () {
        window.parent.postMessage(this.value, "http://a.lyndon.com:8080/a.html");
    });
    // 監(jiān)聽a.html是否有message傳遞過來引矩,如果有梁丘,將iframe輸入框中的內(nèi)容換成a.html中input里的輸入內(nèi)容
    window.addEventListener("message", function(e){
        document.querySelector("#input").value = e.data;
        console.log(e.data);
    });
</script>

所以歸根結(jié)底侵浸,postMessage就是一個(gè)信息交叉的過程旺韭。實(shí)際執(zhí)行效果是:


>>> 附加一個(gè)自己的實(shí)踐:使用jsonp獲取百度聯(lián)想詞

  • 首先在Console中Network查看百度搜索詞的聯(lián)想詞獲取地址

聯(lián)想詞的數(shù)據(jù)地址為: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
精簡(jiǎn)URL,可以發(fā)現(xiàn):"https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=" + string即可返回聯(lián)想詞掏觉。后面需要加上callback"cb=" + function name來進(jìn)行返回結(jié)果的處理区端。

  • 動(dòng)態(tài)獲取跨域數(shù)據(jù)
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);
    }
}
  • 最后的結(jié)果

>>> 總結(jié)

在今后的使用過程中,只需要辨清場(chǎng)景澳腹,然后按照因地制宜的原則選擇一種跨域方法就好织盼,沒有必要完全依賴于一種特定的方法。一言以蔽之:沒有最正確的酱塔,只有最適合的沥邻。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市羊娃,隨后出現(xiàn)的幾起案子唐全,更是在濱河造成了極大的恐慌,老刑警劉巖蕊玷,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邮利,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡垃帅,警方通過查閱死者的電腦和手機(jī)延届,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贸诚,“玉大人方庭,你說我怎么就攤上這事〗垂蹋” “怎么了械念?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)媒怯。 經(jīng)常有香客問我订讼,道長(zhǎng),這世上最難降的妖魔是什么扇苞? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任欺殿,我火速辦了婚禮寄纵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脖苏。我一直安慰自己程拭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布棍潘。 她就那樣靜靜地躺著恃鞋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪亦歉。 梳的紋絲不亂的頭發(fā)上恤浪,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音肴楷,去河邊找鬼水由。 笑死,一個(gè)胖子當(dāng)著我的面吹牛赛蔫,可吹牛的內(nèi)容都是我干的砂客。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼呵恢,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼鞠值!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起渗钉,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤彤恶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后晌姚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粤剧,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年挥唠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抵恋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡宝磨,死狀恐怖弧关,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情唤锉,我是刑警寧澤世囊,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站窿祥,受9級(jí)特大地震影響株憾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一嗤瞎、第九天 我趴在偏房一處隱蔽的房頂上張望墙歪。 院中可真熱鬧,春花似錦贝奇、人聲如沸虹菲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毕源。三九已至,卻和暖如春陕习,著一層夾襖步出監(jiān)牢的瞬間霎褐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工衡查, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瘩欺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓拌牲,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親歌粥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子塌忽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 人生就像一列開往墳?zāi)沟牧熊嚕吠旧蠒?huì)有很多站失驶,很難有人至始至終陪你走完全程土居,當(dāng)陪你的人要下車時(shí),即便不舍嬉探,也要心存...
    sunny519111閱讀 1,634評(píng)論 0 1
  • 什么是跨域擦耀? 2.) 資源嵌入:、涩堤、眷蜓、等dom標(biāo)簽,還有樣式中background:url()胎围、@font-fac...
    電影里的夢(mèng)i閱讀 2,377評(píng)論 0 5
  • 個(gè)人轉(zhuǎn)載做筆記用 http://web.jobbole.com/88525/ 這里說的js跨域是指通過js在不同的...
    平謙閱讀 413評(píng)論 0 7
  • 跨域問題產(chǎn)生的原理是指通過js在不同的域之間進(jìn)行數(shù)據(jù)傳輸或通信吁系,比如用ajax向一個(gè)不同的域請(qǐng)求數(shù)據(jù),或者通過js...
    往復(fù)隨安_cc75閱讀 519評(píng)論 0 1
  • 感恩天氣的炎熱白魂,跑操跑的大汗淋漓汽纤,特別的爽快。感恩菜農(nóng)們辛勤的付出福荸,不顧炎熱蕴坪,把新鮮的蔬菜運(yùn)送到菜市場(chǎng),感恩他們讓...
    rainlove2011閱讀 101評(píng)論 0 0