JS 解決跨域彙總
什麼是跨域? 在瞭解跨域之前,首先要知道什麼是同源策略(same-origin policy)。簡單來講同源策略就是瀏覽器為了保證使用者資訊的安全,防止惡意的網站竊取資料,禁止不同域之間的JS進行互動。對於瀏覽器而言只要域名、協議、埠其中一個不同就會引發同源策略,從而限制他們之間如下的互動行為:
Cookie、LocalStorage 和 IndexDB 無法讀取。 DOM 無法獲得。 AJAX 請求不能傳送。 那麼有時候我們又不得不去解決不同域之間的js互動,這時候就要解決瀏覽器同源策略的問題,也就是需要跨域。 跨域的解決辦法 一、JSONP 在js中,我們直接用XMLHttpRequest請求不同域上的資料時,是不可以的。但是,在頁面上引入不同域上的js指令碼檔案卻是可以的,script標籤裡的src屬性來完成的,jsonp正是利用這個特性來實現的。 比如,在桌面新建一個crossDomain.html頁面,它裡面的程式碼需要利用ajax獲取一個不同域上的json資料,假設這個json資料地址是http://192.168.x.xxx/JSONP/jsonpTest.php那麼crossDomain.html中的程式碼就可以這樣:
<script type="text/javascript"> var text = document.querySelector('.text'); function dosomething(jsondata) { var str = ""; for (var i = 0; i < jsondata.length; i++) { str += jsondata[i]; } text.innerHTML = '我是JS通過JSONP跨域請求來的資料:'+'<span class="show">'+str+'</span>'; } </script> <script type="text/javascript" src="http://192.168.x.xxx/JSONP/jsonpTest.php?callback=dosomething"></script>
可以看到在獲取資料的地址後面還有一個callback引數,按慣例是用這個引數名,但是你用其他的也一樣。當然如果獲取資料的jsonp地址頁面不是你自己能控制的,就得按照提供資料的那一方的規定格式來操作了。
因為是當做一個js檔案來引入的,所以http://192.168.x.xxx/JSONP/jsonpTest.php返回的必須是一個能執行的js檔案,所以這個頁面的php程式碼可能是這樣的:
<?php $callback = $_GET['callback'];//得到回掉函式名 $data = array('a','b','c'); //要返回的資料 echo $callback.'('.json_encode($data).')'; //輸出 ?>
然後在crossDomain.html中打印出返回的jsondata如下:
["a", "b", "c"]
可以看到請求成功了,然後就可以在crossDomain.html這個頁面裡處理這個資料了。 這樣jsonp的原理就很清楚了,通過script標籤引入一個js檔案,這個js檔案載入成功後會執行我們在url引數中指定的函式,並且會把我們需要的json資料作為引數傳入。所以jsonp是需要伺服器端的頁面進行相應的配合的。 當然可以直接用一些已經封裝過的庫,這樣就不用每次去建立script標籤了。如下為JQ的跨域API:
$.getJSON('http://192.168.x.xxx/JSONP/jsonpTest.php?callback=?',function(jsondata){
console.log(jsondata);//["a", "b", "c"]
var str = "";
$.each(jsondata,function(i,index){
return str += index;
});
$(".text1").html('我是JQ通過JSONP跨域請求來的資料:'+'<span class="show">'+str+'</span>');
});
jquery的getJSON方法會自動生成一個全域性函式來替換callback=?中的問號,之後獲取到資料後又會自動銷燬,實際上就是起一個臨時代理函式的作用。$.getJSON方法會自動判斷是否跨域,不跨域的話,就呼叫普通的ajax方法;跨域的話,則會以非同步載入js檔案的形式來呼叫jsonp的回撥函式。
二、通過修改document.domain來跨子域 上面的jsonp是來解決ajax跨域請求的,那麼如果是需要處理 Cookie 和 iframe 該怎麼辦呢?這時候就可以通過修改document.domain來跨子域。兩個網頁一級域名相同,只是二級域名不同,瀏覽器允許通過設定document.domain共享 Cookie或者處理iframe。比如A網頁是http://w1.example.com/a.html,B網頁是http://w2.example.com/b.html,那麼只要設定相同的document.domain,兩個網頁就可以共享Cookie。
document.domain = 'example.com';
//現在,A網頁通過指令碼設定一個 Cookie。
document.cookie = "test1=hello";
//B網頁就可以讀到這個 Cookie。
var allCookie = document.cookie;
注意,這種方法只適用於 Cookie 和 iframe 視窗,LocalStorage 和 IndexDB 無法通過這種方法,規避同源政策,而要使用下文介紹的PostMessage API。 另外,伺服器也可以在設定Cookie的時候,指定Cookie的所屬域名為一級域名,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/
//這樣的話,二級域名和三級域名不用做任何設定,都可以讀取這個Cookie。
不同的iframe 之間(父子或同輩),是能夠獲取到彼此的window物件的,但是你卻不能使用獲取到的window物件的屬性和方法(html5中的postMessage方法是一個例外,還有些瀏覽器比如ie6也可以使用top、parent等少數幾個屬性),總之,你可以當做是隻能獲取到一個幾乎無用的window物件。 首先說明一下同域之間的iframe是可以操作的。比如http://127.0.0.1/JSONP/a.html裡面嵌入一個iframe指向http://127.0.0.1/myPHP/b.html。那麼在a.html裡面是可以操作iframe裡面的DOM的。
<iframe src="http://127.0.0.1/myPHP/b.html" frameborder="1"></iframe>
<body>
<script type="text/javascript">
var iframe = document.querySelector("iframe");
iframe.onload = function(){
var win = iframe.contentWindow;
var doc = win.document;
var ele = doc.querySelector(".text1");
var text = ele.innerHTML="123456";
}
</script>
如果兩個網頁不同源,就無法拿到對方的DOM。典型的例子是iframe視窗和window.open方法開啟的視窗,它們與父視窗無法通訊。如果兩個視窗一級域名相同,只是二級域名不同,那麼document.domain屬性,就可以規避同源政策,拿到DOM。 對於完全不同源的網站,目前有三種方法,可以解決跨域視窗的通訊問題。
片段識別符(fragment identifier) window.name 跨文件通訊API(Cross-document messaging)
三、使用片段識別符來進行跨域 片段識別符號(fragment identifier)指的是,URL的#號後面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改變片段識別符號,頁面不會重新重新整理。 父視窗可以把資訊,寫入子視窗的片段識別符號。在父視窗寫入:
document.getElementById('frame').onload = function(){
var src = "http://127.0.0.1/JSONP/b.html" + '#' + "data";
this.src = src;
}
子視窗通過監聽hashchange事件得到通知。
window.onload = function(){
console.log("b.html載入完成")
window.onhashchange = function(){
var message = window.location.hash;
console.log(message)//#data
};
}
同樣的,子視窗也可以改變父視窗的片段識別符號。
parent.location.href= target + "#" + hash;
四、使用window.name來進行跨域 window物件有個name屬性,該屬性有個特徵:即在一個視窗(window)的生命週期內,視窗載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的許可權,window.name是持久存在一個視窗載入過的所有頁面中的,並不會因新頁面的載入而進行重置。這個屬性的最大特點是,無論是否同源,只要在同一個窗口裡,前一個網頁設定了這個屬性,後一個網頁可以讀取它。 比如:有一個頁面a.html,它裡面有這樣的程式碼:
window.name = "我是a頁面設定的";
setTimeout(function(){
window.location = "http://127.0.0.1/JSONP/b.html";
},1000)
b.html頁面的程式碼:
console.log(window.name);
a.html頁面載入後1秒,跳轉到了b.html頁面,結果b頁面打印出了:
我是a頁面設定的
可以看到在b.html頁面上成功獲取到了它的上一個頁面a.html給window.name設定的值。如果在之後所有載入的頁面都沒對window.name進行修改的話,那麼所有這些頁面獲取到的window.name的值都是a.html頁面設定的那個值。當然,如果有需要,其中的任何一個頁面都可以對window.name的值進行修改。注意,window.name的值只能是字串的形式,這個字串的大小最大能允許2M左右甚至更大的一個容量,具體取決於不同的瀏覽器,但一般是夠用了。 利用window.name可以對同域或者不同域的之間的js進行互動。 那麼在a.html頁面中,我們怎麼把b.html頁面載入進來呢?顯然我們不能直接在a.html頁面中通過改變window.location來載入b.html頁面,因為我們想要即使a.html頁面不跳轉也能得到b.html裡的資料。答案就是在a.html頁面中使用一個隱藏的iframe來充當一箇中間人角色,由iframe去獲取b.html的資料,然後a.html再去得到iframe獲取到的資料。
五、window.postMessage 上面兩種方法都屬於破解,HTML5為了解決這個問題,引入了一個全新的API:跨文件通訊 API(Cross-document messaging)。 這個API為window物件新增了一個window.postMessage方法,允許跨視窗通訊,不論這兩個視窗是否同源。目前IE8+、FireFox、Chrome、Opera等瀏覽器都已經支援window.postMessage方法。 舉例來說,父視窗http://a.com向子視窗http://b.com發訊息,呼叫postMessage方法就可以了。 a頁面:
<iframe id="frame1" src="http://127.0.0.1/JSONP/b.html" frameborder="1"></iframe>
document.getElementById('frame1').onload = function(){
var win = document.getElementById('frame1').contentWindow;
win.postMessage("我是來自a頁面的","http://127.0.0.1/JSONP/b.html")
}
b頁面通過監聽message事件可以接受到來自a頁面的訊息。
window.onmessage = function(e){
e = e || event;
console.log(e.data);//我是來自a頁面的
}
子視窗向父視窗傳送訊息的寫法類似。
window.opener.postMessage('我是來自b頁面的', 'http://a.com');
//父視窗和子視窗都可以通過message事件,監聽對方的訊息。
通過window.postMessage,讀寫其他視窗的 LocalStorage 也成為了可能。 下面是一個例子,主視窗寫入iframe子視窗的localStorage。 父視窗傳送訊息程式碼
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入物件
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://b.com');
// 讀取物件
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = function(e) {
if (e.origin != 'http://a.com') return;
// "Jack"
console.log(JSON.parse(e.data).name);
};
子視窗接收訊息的程式碼
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') return;
var payload = JSON.parse(e.data);
switch (payload.method) {
case 'set':
localStorage.setItem(payload.key, JSON.stringify(payload.data));
break;
case 'get':
var parent = window.parent;
var data = localStorage.getItem(payload.key);
parent.postMessage(data, 'http://aaa.com');
break;
case 'remove':
localStorage.removeItem(payload.key);
break;
}
};
六、通過WebSocket進行跨域 WebSocket是一種通訊協議,使用ws://(非加密)和wss://(加密)作為協議字首。該協議不實行同源政策,只要伺服器支援,就可以通過它進行跨源通訊。 下面是一個例子,瀏覽器發出的WebSocket請求的頭資訊(摘自維基百科)。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
上面程式碼中,有一個欄位是Origin,表示該請求的請求源(origin),即發自哪個域名。 正是因為有了Origin這個欄位,所以WebSocket才沒有實行同源政策。因為伺服器可以根據這個欄位,判斷是否許可本次通訊。如果該域名在白名單內,伺服器就會做出如下回應。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
七、 CORS CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標準,是跨源AJAX請求的根本解決方法。相比JSONP只能發GET請求,CORS允許任何型別的請求。CORS需要瀏覽器和伺服器同時支援。目前,所有瀏覽器都支援該功能,IE瀏覽器不能低於IE10。 整個CORS通訊過程,都是瀏覽器自動完成,不需要使用者參與。對於開發者來說,CORS通訊與同源的AJAX通訊沒有差別,程式碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動新增一些附加的頭資訊,有時還會多出一次附加的請求,但使用者不會有感覺。 因此,實現CORS通訊的關鍵是伺服器。只要伺服器實現了CORS介面,就可以跨源通訊。由於CORS涉及內容較多,以後會寫一篇 專門介紹CORS的文章。
八、服務端設定代理頁面專門處理前端跨域請求 總結:以上整理了各種常見的跨域解決辦法,在開發過程中我們可以根據不同的場景選擇最佳的解決辦法。處理ajax的跨域可以選擇JSONP、CORS,服務端設定代理、WebSocket。如果主域相同,處理多級子域之間的通訊可以選擇document.domain,處理不同域之間的iframe,子視窗可以選擇window.name、window.postMessage、location.hash來解決。