解決ajax跨域的方法原理詳解
1、神馬是跨域(Cross Domain)
對於埠和協議的不同,只能通過後臺來解決。一句話:同一個ip、同一個網路協議、同一個埠,三者都滿足就是同一個域,否則就是
跨域問題了。而為什麼開發者最初不直接定為一切可跨域的呢?預設的為什麼都是不可跨域呢?這就涉及到了同源策
略,為了系統的安全,由Netscape提出一個著名的安全策略。現在所有支援JavaScript的瀏覽器都會使用這個策略。
所謂同源是,域名,協議,埠相同。當我們在瀏覽器中開啟百度和谷歌兩個網站時,百度瀏覽器在執行一個指令碼的
時候會檢查這個指令碼屬於哪個頁面的,即檢查是否同源,只有和百度同源的指令碼才會被執行,如果沒有同源策略,那
隨便的向百度中注入一個js指令碼,彈個惡意廣告,通過js竊取資訊,這就很不安全了。
說白點就是post、get的url不是你當前的網站,域名不同。例如在aaa.com/a.html裡面,表單的提交action是bbb.com/b.html。
不僅如此,www.aaa.com和aaa.com之間也屬於跨域,因為www.aaa.com是二級域名,aaa.com是根域名。
JavaScript出於安全方面的考慮,是不允許跨域呼叫其他頁面的物件的(同源策略 Same-Origin Policy)。
關於JavaScript能否跨域通訊的詳細說明,見下表:
http://www.a.com/a.js
URL | 說明 | 是否允許通訊 |
---|---|---|
http://www.a.com/b.js | 同一域名下 | 允許 |
http://www.a.com/script/b.js | 同一域名下不同資料夾 | 允許 |
http://www.a.com:8000/b.js | 同一域名,不同埠 | 不允許 |
https://www.a.com/b.js | 同一域名,不同協議 | 不允許 |
http://70.32.92.74/b.js | 域名和域名對應ip | 不允許 |
http://script.a.com/b.js | 主域相同,子域不同 | 不允許 |
http://a.com/b.js | 同一域名,不同二級域名(同上) | 不允許 |
http://www.b.com/b.js | 不同域名 | 不允許 |
2、為嘛要跨域
跨域這東西其實很常見,例如我們可以把網站的一些指令碼、圖片或其他資源放到另外一個站點。例如我們可以使用Google提供的jQuery,載入時間少了,而且減少了伺服器的流量,如下:
1 | < script type = "text/java script" src = "https://aja x.googleapis.com/aj
ax/libs/jquery/1.4.2/jquery.min.js" ></ script > |
有時候不僅僅是一些指令碼、圖片這樣的資源,我們也會希望從另外的站點呼叫一些資料(有時候是不得不這樣),例如我希望獲取一些blog的RSS來生成一些內容,再或者說我在“人人開放平臺”上開發一個應用,需要呼叫人人的資料。
然而,很不幸的是,直接用XMLHttpRequest來Get或者Post是不行的,例如我用jQuery的$.get去訪問本小博的主域名 :
1 2 3 4 |
{}, function (data){
alert( '跨域不是越獄:' +data)
}, "html" );
|
結果如下(總之就是不行啦~FF不報錯,但是木有返回資料):
那咋麼辦捏?(弱弱的說,測試的時候我發現IE訪問本地檔案時,是可以跨域的,不過這也沒啥用~囧~)
3、腫麼跨域
下面為了更好的講解和測試,我們可以通過修改hosts檔案來模擬跨域的效果,hosts檔案在C:\Windows\System32\drivers\etc 資料夾下。在下面加3行:
127.0.0.1 www.a.com
127.0.0.1 a.com
127.0.0.1 www.b.com
3.1、跨域代理
一種簡單的辦法,就是把跨域的工作交給伺服器,從後臺獲取其他站點的資料再返回給前臺,也就是跨域代理(Cross Domain Proxy)。
這種方法似乎蠻簡單的,改動也不太大。不過就是http請求多了些,響應慢了些,伺服器的負載重了些~
3.2、document.domain+iframe
對於主域相同而子域不同的例子,可以通過設定document.domain的辦法來解決。
舉www.a.com/a.html和a.com/b.html為例,只需在a.html中新增一個b.html的iframe,並且設定兩個頁面的document.domain都為’a.com’(只能為主域名),兩個頁面之間即可互相訪問了,程式碼如下:
www.a.com/a.html中的script
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
document.domain= 'a.com' ;
var ifr = document.createElement( 'iframe' );
ifr.style.display = 'none' ;
document.body.appendChild(ifr);
ifr.onload = function (){
//獲取iframe的document物件
//W3C的標準方法是iframe.contentDocument,
//IE6、7可以使用document.frames[ID].document
//為了更好相容,可先獲取iframe的window物件iframe.contentWindow
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 在這裡操縱b.html
alert(doc.getElementById( "test" ).innerHTML);
};
|
a.com/b.html
1 2 3 4 5 6 7 8 9 10 11 12 |
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
< html >
< head >
< title ></ title >
< script type = "text/javascript" >
document.domain='a.com';
</ script >
</ head >
< body >
< h1 id = "test" >Hello World</ h1 >
</ body >
</ html >
|
如果b.html要訪問a.html,可在子視窗(iframe)中通過window.parent來訪問父視窗的window物件,然後就可以為所欲為了(window物件都有了,還有啥不行的),同理子視窗也可以和子視窗之間通訊。
於是,我們可以通過b.html的XMLHttpRequest來獲取資料,再傳給a.html,從而解決跨子域獲取資料的問題。
但是這種方法只支援同一根域名下的頁面,如果不同根域名(例如baidu.com想訪問google.com)那就無能為力了。
3.3、動態script標籤(Dynamic Script Tag)
2.因為script的src屬性是可以跨域的 <script></script>可以呼叫跨域的js檔案這裡我們需要用到JQuery.getScript(url, callback)方法,url是指令碼檔案的URL路勁,callback函式在指令碼資源已被載入和求值後呼叫的回撥函式。
首先在bb.com建立一個js檔案,test.js
1 var ojb = {msg:'js跨域請求成功'};
2 然後在aa.com的頁面上使用$.getScript載入test.js指令碼
1 $(function() {
2 $.getScript('http://www.bb.com/test.js', function() {
3 if (ojb) {
4 alert(obj.msg);
5 }
6 });
7 });
使用$.getScript函式的最大好處就是可以保證,指令碼載入完畢後呼叫回撥函式。
這種技術克服了XMLHttpRequest的最大限制,也就是跨域請求資料。直接用JavaScript建立一個新的指令碼標籤,然後設定它的src屬性為不同域的URL。
www.a.com/a.html中的script
1 2 3 4 5 |
var dynScript = document.createElement( 'script' );
dynScript.setAttribute( "type" , "text/javascript" );
document.getElementsByTagName( 'head' )[0]
.appendChild(dynScript);
|
通過動態標籤注入的必須是可執行的JavaScript程式碼,因此無論是你的資料格式是啥(xml、json等),都必須封裝在一個回撥函式中。一個回撥函式如下:
www.a.com/a.html中的script
1 2 3 4 |
function dynCallback(data){
//處理資料, 此處簡單示意一下
alert(data.content);
}
|
在這個例子中,www.b.com/b.js需要將資料封裝在上面這個dynCallback函式中,如下:
1 | dynCallback({content: 'Hello World' }) |
我們看到了讓人開心的結果,Hello World~
不過動態指令碼注入還是存在不少問題的,下面我們拿它和XMLHttpRequest來對比一下:
XmlHttpRequest | Dynamic Script Tag | |
---|---|---|
跨瀏覽器相容 | No | Yes |
跨域限制 | Yes | No |
接收HTTP狀態 | Yes | No (除了200) |
支援Get、Post | Yes | No (GET only) |
傳送、接收HTTP頭 | Yes | No |
接收XML | Yes | Yes |
接收JSON | Yes | Yes |
支援同步、非同步 | Yes | No (只能非同步) |
可以看出,動態指令碼注入還是有不少限制,只能使用Get,不能像XHR一樣判斷Http狀態等。
而且使用動態指令碼注入的時候必須注意安全問題。因為JavaScript沒有任何許可權與訪問控制的概念,通過動態指令碼注入的程式碼可以完全控制整個頁面,所以引入外部來源的程式碼必須多加小心。
3.4、iframe+location.hash
這種方法比上面兩種稍微繁瑣一點,原理如下:
www.a.com下的a.html想和www.b.com下的b.html通訊(在a.html中動態建立一個b.html的iframe來發送請求);
但是由於“同源策略”的限制他們無法進行交流(b.html無法返回資料),於是就找個中間人:www.a.com下的c.html(注意是www.a.com下的);
b.html將資料傳給c.html(b.html中建立c.html的iframe),由於c.html和a.html同源,於是可通過c.html將返回的資料傳回給a.html,從而達到跨域的效果。
三個頁面之間傳遞引數用的是location.hash(也就是www.a.html#sayHello後面的’#sayHello’),改變hash並不會導致頁面重新整理(這點很重要)。
具體程式碼如下:
www.a.com/a.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//通過動態建立iframe的hash傳送請求
function sendRequest(){
var ifr = document.createElement( 'iframe' );
ifr.style.display = 'none' ;
//跨域傳送請求給b.html, 引數是sayHello
document.body.appendChild(ifr);
}
//獲取返回值的方法
function checkHash() {
var data = location.hash ?
location.hash.substring(1) : '' ;
if (data) {
//處理返回值
alert(data);
location.hash= '' ;
}
}
//定時檢查自己的hash值
setInterval(checkHash, 2000);
window.onload = sendRequest;
|
www.b.com/b.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function checkHash(){
var data = '' ;
//模擬一個簡單的引數處理操作
switch (location.hash){
case '#sayHello' : data = 'HelloWorld' ; break ;
case '#sayHi' : data = 'HiWorld' ; break ;
default : break ;
}
data && callBack( '#' +data);
}
function callBack(hash){
// ie、chrome的安全機制無法修改parent.location.hash,
// 所以要利用一箇中間的www.a.com域下的代理iframe
var proxy = document.createElement( 'iframe' );
proxy.style.display = 'none' ;
// 注意該檔案在"www.a.com"域下
document.body.appendChild(proxy);
}
window.onload = checkHash;
|
www.a.com/c.html
1 2 3 4 5 |
//因為c.html和a.html屬於同一個域,
//所以可以改變其location.hash的值
//可通過parent.parent獲取a.html的window物件
parent.parent.location.hash =
self.location.hash.substring(1);
|
可能有人會有疑問,既然c.html已經獲取了a.html的window物件了,為何不直接修改它的dom或者傳遞引數給某個變數呢?
原因是在c.html中修改 a.html的dom或者變數會導致頁面的重新整理,a.html會重新訪問一次b.html,b.html又會訪問c.html,造成死迴圈……囧呀~
所以只能通過location.hash了。這樣做也有些不好的地方,諸如資料容量是有限的(受url長度的限制),而且資料暴露在url中(使用者可以隨意修改)……
3.5、postMessage(html5)
html5中有個很酷的功能,就是跨文件訊息傳輸(Cross Document Messaging)。新一代瀏覽器都將支援這個功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。
使用方法如下:
1 | otherWindow.postMessage(message, targetOrigin); |
說明:
- otherWindow: 對接收資訊頁面的window的引用。可以是頁面中iframe的contentWindow屬性,window.open的返回值等。
- message: 所要傳送的資料,string型別。
- targetOrigin: 用於限制otherWindow,“*”表示不作限制。
www.a.com/a.html中的程式碼:
html:
script:
1 2 3 4 5 6 |
window.onload = function () {
var ifr = document.getElementById( 'ifr' );
// 若寫成'http://www.c.com'就不會執行postMessage了
ifr.contentWindow.postMessage( 'sayHello' , targetOrigin);
};
|
www.b.com/b.html的script
1 2 3 4 5 6 7 8 |
//通過message事件來通訊,實在太爽了
window.addEventListener( 'message' , function (e){
// 通過origin屬性判斷訊息來源地址
e.data== 'sayHello' ) {
alert( 'Hello World' );
}
}, false );
|
3.6、使用flash
由於本人對flash不怎麼熟悉,此處暫時忽略之~
3.7、Cross Frame
行文至此,突然在口碑網UED部落格上看到了一篇 《跨域資源共享的10種方式》,對跨域的多種方法都有介紹(雖然有原始碼,但多數都是直接呼叫YUI庫的,比較難看出原理)。
裡面提到了Cross Frame這種方法,似乎挺不錯的,改日一定翻原始碼來研究。
跨域 :
1.jsonp
簡易說明,JSONP就是包裝的JSON,把P理解為package更為合適。例JSON和JSONP:
- // JSON
- {
- "s": "b"
- };
- // JSONP
- jsonp({
- "s": "b"
- });
如例,JSONP中JSON是jsonp方法的實參,這樣寫的目的是,開啟這個JSONP就運行了jsonp方法。如我們單獨請求該指令碼地址:
- <script src="http://qianduanblog.duapp.com/test/index.php?jsonp=jsonp"></script>
- <!-- 報錯:Uncaught ReferenceError: jsonp is not defined -->
出現預想的錯誤,jsonp方法未定義,證明了以上說的是正確的。所以在處理JSONP的時候,我們需要預先定義一個全域性函式jsonp,然後在該函式中返回實參即可。即:
$(document).ready(function(){
var url='http://localhost:8080/WorkGroupManagment/open/getGroupById"
+"?id=1&callback=?';
$.ajax({
url:url,
dataType:'jsonp',
processData: false,
type:'get',
success:function(data){
alert(data.name);
},
error:function(XMLHttpRequest, textStatus, errorThrown) {
alert(XMLHttpRequest.status);
alert(XMLHttpRequest.readyState);
alert(textStatus);
}});
});
String callback = request.getParameter("callback");
String json = JsonConverter.bean2Json(result);
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
out.print(callback + "(" + json + ")");
return null;
}
getJson和jsonp是一樣的
$.getJSON("http://api.taobao.com/apitools/ajax_props.do&jsoncallback=?",
function (data) {
alert(data);
}
);
3.2、VAR
JSONP是利用全域性方法來實現跨域讀取,當然也可以利用全域性變數來實現跨域讀取。例:
- var window.testvar ="123";
- alert(window.testvar);
並且這個方法比JSONP要來的更加簡單一點,具體實現方法例
- function getTime(callback){
- var url ="http://qianduanblog.duapp.com/test/index.php?var=testvar";
- var script = document.createElement("script");
- var head = document.getElementsByTagName("head")[0];
- script.src = url;
- script.onload =function(){
- // 回撥
- callback(testvar);
- // 移除該script
- script.parentNode.removeChild(script);
- // 刪除該script
- script =null;
- // 刪除變數
- window.testvar =null;
- }
- // 頁面上插入該指令碼
- head.appendChild(script);
- }
- getTime(function(json){
- alert(json.time);
- });
3.3、修繕與擴充套件
在跨域讀取動態內容,無論是利用JSONP還是VAR方法,都需要面對覆蓋全域性方法、全域性變數的危險,解決這樣的情況,我們可以生成一個唯一的函式名或者變數名來儘可能的防止出現這樣的情況,例:
- functionName = "yundanran" + new Date().getTime();
- varName = "yundanran" + new Date().getTime();
這樣的重複的概率就大大降低了。
第二個問題是,如何擴充套件該方法,兩種方法大都雷同,可以合二為一。綜合例:
- /**
- * 跨域讀取
- * @param {String} 跨域方法,jsonp或var
- * @param {String} 跨域地址
- * @param {Function} 跨域成功回撥
- * @param {Function} 跨域失敗回撥
- * @return {Undefined} 無返回
- * @author 雲淡然
- * @version 1.0
- * 2013年11月20日14:30:51
- */
- function crossDomain(type, url, onsuccess, onerror){
- // 設定回撥為
- var callbackName ="prefix"+newDate().getTime()+"callback";
- // 建立回撥函式
- if(type =="jsonp"){
- window[callbackName]=function(){
- if(onsuccess) onsuccess(arguments[0]);
- }
- }
- // 建立一個 script 的 DOM 物件
- script = document.createElement("script");
- // 設定其同步屬性
- script.async =true;
- // 設定其地址
- script.src = url.replace(/#.*$/,"")+(/\?/.test(url)?"&":"?")+ type +"="+ callbackName;
- // 監聽
- script.onload = script.onreadystatechange =function(){
- if(!script.readyState ||/loaded|complete/.test(script.readyState)){
- script.onload = script.onreadystatechange =null;
- if(type =="var"){
- if(onsuccess) onsuccess(window[callbackName]);
- }
- // 移除該 script 的 DOM 物件
- if(script.parentNode){
- script.parentNode.removeChild(script);
- }
- // 刪除函式或變數
- window[callbackName]=null;
- }
- }
- script.onerror =function(){
- if(onerror) onerror();
- }
- // 插入head
- head.appendChild(script);
- }
1.1
CROS(Cross-Origin Resource Sharing)跨域資源共享,定義了必須在訪問跨域資源時,瀏覽器與伺服器應該如何溝通。CROS背後的基本思想就是使用自定義的HTTP頭部讓瀏覽器與伺服器進行溝通,從而決定請求或響應是應該成功還是失敗。
複製程式碼程式碼如下:<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("GET", "/trigkit4",true);
xhr.send();
</script>
以上的trigkit4是相對路徑,如果我們要使用CORS,相關Ajax程式碼可能如下所示:
複製程式碼程式碼如下:<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://segmentfault.com/u/trigkit4/",true);
xhr.send();
</script>
程式碼與之前的區別就在於相對路徑換成了其他域的絕對路徑,也就是你要跨域訪問的介面地址。
伺服器端對於CORS的支援,主要就是通過設定Access-Control-Allow-Origin來進行的。如果瀏覽器檢測到相應的設定,就可以允許Ajax進行跨域的訪問。
jquery會自動生成一個全域性函式來替換callback=?中的問號,之後獲取到資料後又會自動銷燬,實際上就是起一個臨時代理函式的作用。$.getJSON方法會自動判斷是否跨域,不跨域的話,就呼叫普通的ajax方法;跨域的話,則會以非同步載入js檔案的形式來呼叫jsonp的回撥函式。
JSONP的優缺點
JSONP的優點是:它不像XMLHttpRequest物件實現的Ajax請求那樣受到同源策略的限制;它的相容性更好,在更加古老的瀏覽器中都可以執行,不需要XMLHttpRequest或ActiveX的支援;並且在請求完畢後可以通過呼叫callback的方式回傳結果。
JSONP的缺點則是:它只支援GET請求而不支援POST等其它型別的HTTP請求;它只支援跨域HTTP請求這種情況,不能解決不同域的兩個頁面之間如何進行JavaScript呼叫的問題。
CROS和JSONP對比
CORS與JSONP相比,無疑更為先進、方便和可靠。
1、 JSONP只能實現GET請求,而CORS支援所有型別的HTTP請求。
2、 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得資料,比起JSONP有更好的錯誤處理。
3、 JSONP主要被老的瀏覽器支援,它們往往不支援CORS,而絕大多數現代瀏覽器都已經支援了CORS)。
4、總結
研究了幾天,雖然對多種跨域方法都有所瞭解了,但是真要投入應用還是明顯不夠的(還是需要藉助一些js庫)。
每種方法都有其優缺點,使用的時候其實應該將多種跨域方法進一步封裝一下,統一呼叫的介面,利用js來自動判斷哪種方法更為適用 。