由於同源策略的限制,Javascript跨域的問題,一直是一個比較棘手的問題,為了解決頁面之間的跨域通信,大家煞費苦心,研究了各種跨域方案。深入了解之后,這里給大家介紹一點我的具體做法。
先來看看哪些情況下才存在跨域的問題:
編號 |
URL |
說明 |
是否允許通信 |
1 |
同一域名下 |
允許 |
|
2 |
同一域名下不同文件夾 |
允許 |
|
3 |
同一域名,不同端口 |
不允許 |
|
4 |
同一域名,不同協議 |
不允許 |
|
5 |
域名和域名對應ip |
不允許 |
|
6 |
主域相同,子域不同 |
不允許 |
|
7 |
同一域名,不同二級域名(同上) |
不允許(cookie這種情況下也不允許訪問) |
|
8 |
不同域名 |
不允許 |
其中編號6、7兩種情況同屬於主域名相同的情況,可以設置domain來解決問題,今天就不討論這種情況了。
對於其他跨域通信的問題,我想又可以分成兩類,其一(第一種情況)是a.com下面的a.js試圖請求b.com下某個接口時產生的跨域問題。其二(第二種情況)是當a.com與b.com下面的頁面成父子頁面關系時試圖互相通信時產生的跨域問題,典型的應用場景如a.com/a.html使用iframe內嵌了b.com/b.html,大家都知道a.html內的js腳本試圖訪問b.html時是會被拒絕的,反之亦然。
第一種情況,目前主流的方案是JSONP,高版本瀏覽器支持html5的話,還可以使用XHR2支持跨域通信的新特性。
第二種情況,目前主要是通過代理頁面或者使用postMessageAPI來做,這也是今天要討論的話題。
第二種情況,有這樣一些類似的案例:a.com/a.html使用iframe內嵌了b.com/b.html,現在希望iframe的高度能自動適應b.html的高度,使iframe不要出現滾動條。我們都知道跨域了,a.html是沒辦法直接讀取到b.html的高度的,b.html也沒辦法把自己的高度告訴a.html。
直接說可以用代理頁面的方法搞定這個問題吧,但是怎么代理法,先來看下面這張圖:
圖1
b.html與a.html是不能直接通信的。我們可以在b.html下面再iframe內嵌一個proxy.html頁面,因為這個頁面是放在a.com下面的,與a.html同域,所以它其實是可以和a.html直接通信的,假如a.html里面有定義一個方法_callback,在proxy.html可以直接top._callback()調用它。但是b.html本身和proxy.html也是不能直接通信的,所謂代理頁面的橋梁作用怎么實現呢?
b.html內嵌proxy.html是通過一段類似下面這樣的代碼:
<iframe id="proxy" src="a.com/proxy.html" name="proxy" frameborder="0" width="0" height="0"></iframe>
這個iframe的src屬性b.html是有權限控制的。如果它把src設置成a.com/proxy.html?args=XXX,也就是給url加一個查詢字符串,proxy.html內的js是可以讀取到的。對的,這個url的查詢字符串就是b.html和proxy.html之間通信的橋梁,美中不足的是每次通信都要重寫一次url造成一次網絡請求,這有時會對服務器及頁面的運行效率產生很大的影響。同時由於參數是通過url來傳遞的,會有長度和數據類型的限制,搜集的資料顯示:
ØIE瀏覽器對URL的長度現限制為2048字節。
Ø360極速瀏覽器對URL的長度限制為2118字節。
ØFirefox(Browser)對URL的長度限制為65536字節。
ØSafari(Browser)對URL的長度限制為80000字節。
ØOpera(Browser)對URL的長度限制為190000字節。
ØGoogle(chrome)對URL的長度限制為8182字節。
上面的方法,通過迂回戰術實現了b.html跟a.html通信,但是倒過來,a.html怎么跟b.html通信呢?嵌入在b.html里面的proxy.html可以用top快速的聯系上a.html,但是要想讓a.html找到proxy.html就不容易了,夾在中間的b.html生生把它們分開了,a.html沒法讓b.html去找到proxy.html然后返回給它。只能采用更迂回的戰術了。
順着前面b.html到a.html的通信過程,逆向的想一下,雖然a.html沒有辦法主動找到proxy.html,但是proxy.html可以反過來告訴a.html它在哪里:
在proxy.html加這么一段腳本:
<scripttype="text/javascript">
var topWin = top;
function getMessage(data) {
alert("messageFormTopWin:" + data);
}
function sendMessage(data) {
topWin.proxyWin = window;
topWin.getMessage(data);
}
</script>
在a.html加這么一段腳本:
<scripttype="text/javascript">
var proxyWin = null;
function getMessage(data) {
alert("messageFormProxyWin:"+data);
sendMessage("top has receive data:"+data);
}
function sendMessage(data) {
if (null != proxyWin) {
proxyWin.getMessage(data);
}
}
</script>
也就是必須由proxy.html先主動發送一個消息給a.html,a.html得到proxy.html頁面window的引用,就可以反過來向它發送請求了。
現在a.html可以把消息發給proxy.html了,但是proxy.html怎么把消息轉送到b.html?似乎這才是難點,因為它們之間才真正有着“跨域”這一道鴻溝。
這回我們不再用前面那個iframe內嵌代理頁面的方法再在proxy.html內嵌一個b.com下面的代理頁面了,這樣實在會給人感覺嵌的太深了,四層。但是為了跨越這道鴻溝,b.com下面也加一個代理頁面是免不的。不過現在我們要利用一下window.name。window.name有一個特性,就是頁面在同一個瀏覽器窗口(標簽頁)中跳轉時,它一直存在而且值不會改變。比如我們在a.html中設置了window.name=”a”,然后location.href=”http://b.com/b.html”跳轉后,b.html可以讀取window.name的值為”a”;而且window.name的值長度一般可以到達2M,ie和firefox甚至可以達到32M,這樣的存儲容量,足夠利用起來做跨域的數據傳遞了。好吧,我們現在要做的就是當proxy.html拿到a.html發送過來的數據后把這個數據寫入window.name中,然后跳轉到b.com下面的代理頁面,我們這里假設是bproxy.html。bproxy.html讀取到window.name值后,通知給它父頁面b.html就簡單了。我們再來看這個過程可以用圖大概示意一下:
圖2
圖例中綠色的雙向箭頭表示可以通信,橙色的雙向箭頭表示不能直接通信。
最后我們簡單看一下雙向通信的實測效果:

圖3
b.html每次加載的時候都先給a.html發一個”連接請求”,讓a.html可以找到proxy.html。所以頁面第一次加載的時候會產生三個請求:
圖4
每次b.html向a.html發送消息的時候會產生一個請求:
圖5
每次a.html向b.html發送消息的時候會產生兩個請求,其中一個是a.com/proxy.html向b.com/bproxy.html跳轉產生的,另一個是b.html重新向a.html發起“連接請求”時產生的:
圖6
最后簡單看一下實測的幾個測試頁面代碼:
代碼片段一,a.com/a.html:
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>a.com</title>
</head>
<body>
<divid="Div1">
A.com/a.html</div>
<inputid="txt_msg"type="text"/>
<inputid="Button1"type="button"value="向b.com/b.html發送一條消息"onclick="sendMessage(document.getElementById('txt_msg').value)"/>
<divid="div_msg">
</div>
<iframewidth="800"height="400"id="mainFrame"src="http://localhost:8091/b.com/b.htm">
</iframe>
<scripttype="text/javascript">
var proxyWin = null;
function showMsg(msg) {
document.getElementById("div_msg").innerHTML = msg;
}
function getMessage(data) {
showMsg("messageForm b.html to ProxyWin:" + data);
}
function sendMessage(data) {
if (null != proxyWin) {
proxyWin.getMessage(data);
}
}
</script>
</body>
</html>
代碼片段二,a.com/proxy.html:
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>a.com</title>
</head>
<body>
<divid="Div1">A.com/proxy.html</div>
<divid="div_msg"></div>
<scripttype="text/javascript">
var topWin = top;
function showMsg(msg) {
document.getElementById("div_msg").innerHTML = msg;
}
function getMessage(data) {
showMsg("messageForm A.com/a.html:" + data + "<br/>兩秒后將跳轉到B.com/bproxy.html");
window.name = data;
setTimeout(function () { location.href = "http://localhost:8091/b.com/bproxy.htm" }, 2000);//為了能讓大家看到跳轉的過程,所以加了個延時
}
function sendMessage(data) {
topWin.proxyWin = window;
topWin.getMessage(data);
}
var search = location.search.substring(1);
showMsg("messageForm B.com/b.html:" + search);
sendMessage(search);
</script>
</body>
</html>
代碼片段三,b.com/b.html
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>b.com</title>
</head>
<body>
<divid="Div1">
B.com/b.html</div>
<inputid="txt_msg"type="text"/>
<inputid="Button1"type="button"value="向A.com/a.html發送一條消息"onclick="sendMessage(document.getElementById('txt_msg').value)"/>
<divid="div_msg">
</div>
<iframeid="proxy"name="proxy"style="width: 600px; height: 300px"></iframe>
<scripttype="text/javascript">
function showMsg(msg) {
document.getElementById("div_msg").innerHTML = msg;
}
function sendMessage(data) {
var proxy = document.getElementById("proxy");
proxy.src = "about:blank";
proxy.src="http://localhost:8090/a.com/proxy.htm?data=" + data;
}
function connect() {
sendMessage("connect");
}
function getMessage(data) {
showMsg("messageForm a.html to ProxyWin:" + data);
connect();
}
connect(); //頁面一加載,就執行一次連接
</script>
</body>
</html>
代碼片段四,b.com/bproxy.html
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>b.com</title>
</head>
<body>
<divid="Div1">
B.com/bproxy.html</div>
<divid="div_msg">
</div>
<scripttype="text/javascript">
var parentWin = parent;
var data = null;
function getMessage() {
if (window.name) {
data = window.name;
parentWin.getMessage(data);
}
document.getElementById("div_msg").innerHTML = "messageForm a.com/proxy.html:" + data;
}
getMessage();
</script>
</body>
</html>
好吧,現在我必須把話鋒調轉一下了。前面講的這么多,也只是拋出來一些之前我們可能會采用的跨域通信方法,事實上代理頁面、url傳參數和window.name、甚至還有一些利用url的hash值的跨域傳值方法,都能百度到不少相關資料。但它們都逃不開代理頁面,也就不可避免的要產生網絡請求,而事實上這並不是我們的本意,我們原本希望它們能夠直接在客戶端通信,避免不必要的網絡請求開銷——這些開銷,在訪問量超大的站點可能會對服務器產生相當大的壓力。那么,有沒有更完美一點的替代方案呢?
必須給大家推薦postMessage。postMessage 正是為了滿足一些合理的、不同站點之間的內容能在瀏覽器端進行交互的需求而設計的。利用postMessage API實現跨域通信非常簡單,我們直接看一下實例的代碼:
代碼片段一,A.com/a.html:
<htmlxmlns="http://www.w3.org/1999/xhtml">
<headrunat="server">
<title>A.com/a.html</title>
<scripttype="text/javascript">
var trustedOrigin = "http://localhost:8091";
function messageHandler(e) {
if (e.origin == trustedOrigin) {//接收消息的時候,判斷消息是否來自可信的源,這個源是否可信則完全看自己的定義了。
showMsg(e.data);//e.data才是真實要傳遞的數據
} else {
// ignore messages from other origins
}
}
function sendString(s) {//發送消息
document.getElementById("widget").contentWindow.postMessage(s, trustedOrigin);
}
function showMsg(message) {
document.getElementById("status").innerHTML = message;
}
function sendStatus() {
var statusText = document.getElementById("statusText").value;
sendString(statusText);
}
function loadDemo() {
addEvent(document.getElementById("sendButton"), "click", sendStatus);
sendStatus();
}
function addEvent(obj, trigger, fun) {
if (obj.addEventListener) obj.addEventListener(trigger, fun, false);
elseif (obj.attachEvent) obj.attachEvent('on' + trigger, fun);
else obj['on' + trigger] = fun;
}
addEvent(window, "load", loadDemo);
addEvent(window, "message", messageHandler);
</script>
</head>
<body>
<h1>A.com/a.html</h1>
<p><b>源</b>: http://localhost:8090</p>
<inputtype="text"id="statusText"value="msg from a.com/a.html">
<buttonid="sendButton">向b.com/b.html發送消息</button>
<p>接收到來自a.com/a.html的消息: <strongid="status"></strong>.<p>
<iframeid="widget"width="800"height="400"src="http://localhost:8091/PostMessage/Default.aspx"></< span> iframe>
</body>
</html>
代碼片段二,B.com/b.html:
<htmlxmlns="http://www.w3.org/1999/xhtml">
<headrunat="server">
<title>B.com/b.html</title>
<scripttype="text/javascript">
//檢查postMessage 是否可以用:window.postMessage===undefined
//定義信任的消息源
var trustedOrigin = "http://localhost:8090";
function messageHandler(e) {
if (e.origin === "http://localhost:8090") {
showMsg(e.data);
} else {
// ignore messages from other origins
}
}
function sendString(s) {
window.top.postMessage(s, trustedOrigin); //第二個參數是消息傳送的目的地
}
function loadDemo() {
addEvent(document.getElementById("actionButton"), "click", function () {
var messageText = document.getElementById("messageText").value;
sendString(messageText);
});
}
function showMsg(message) {
document.getElementById("status").innerHTML = message;
}
function addEvent(obj, trigger, fun) {
if (obj.addEventListener) obj.addEventListener(trigger, fun, false);
elseif (obj.attachEvent) obj.attachEvent('on' + trigger, fun);
else obj['on' + trigger] = fun;
}
addEvent(window, "load", loadDemo);
addEvent(window, "message", messageHandler);
</script>
</head>
<body>
<h1>B.com/b.html</h1>
<p><b>源</b>: http://localhost:8091</p>
<p>接收到來自a.com/a.html的消息: <strongid="status"></strong>.<p>
<div>
<inputtype="text"id="messageText"value="msg from b.com/b.html">
<buttonid="actionButton">向a.com/a.html發送一個消息</button>
</div>
</body>
</html>
代碼的關鍵是message事件是一個擁有data(數據)和origin(來源)屬性的DOM事件。data屬性是發送的實際數據,origin屬性是發送來源。Origin屬性很關鍵,有了這個屬性,接收方可以輕易的忽略掉來自不可信源的消息,也就能有效避免跨域通信這個開口給我們的源安全帶來的隱患。接口很強大,所以代碼很簡單。我們可以抓包看一下,這個通信過程完全是在瀏覽器端的,沒有產生任何的網絡請求。同時這個接口目前已經得到了絕大多數瀏覽器的支持,包括IE8及以上版本,參見下面的圖表:
圖7
但是為了覆蓋ie6等低版本瀏覽器,我們完整的方案里面還是要包含一下兼容代碼,就是最開始介紹的代理頁面的方法了,但必須是以postMessage為主,這樣即便最后會有某些瀏覽器因為這種通信產生一些網絡請求,比例也是非常低的了。
本人的小站“微聚合”也許有更多你感興趣的內容,歡迎交流。