由於同源策略的限制,JavaScript跨域的問題,一直是一個比較棘手的問題,為了解決頁面之間的跨域通訊,大家煞費苦心,研究了各種跨域方案。之前也有小網同學分享過一篇“跨域,不再糾結” 開始照著嘗試時還是有些不夠明白的地方,深入瞭解之後,這裡給大家補充一點更具體的做法。
先來看看哪些情況下才存在跨域的問題:
編號 | URL | 說明 | 是否允許通訊 |
1 |
http://www.a.com/a.js http://www.a.com/b.js |
同一域名下 |
允許 |
2 |
http://www.a.com/lab/a.js http://www.a.com/script/b.js |
同一域名下不同資料夾 |
允許 |
3 |
http://www.a.com:8000/a.js http://www.a.com/b.js |
同一域名,不同埠 |
不允許 |
4 |
http://www.a.com/a.js https://www.a.com/b.js |
同一域名,不同協議 |
不允許 |
5 |
http://www.a.com/a.js http://70.32.92.74/b.js |
域名和域名對應ip |
不允許 |
6 |
http://www.a.com/a.js http://script.a.com/b.js |
主域相同,子域不同 |
不允許 |
7 |
http://www.a.com/a.js http://a.com/b.js |
同一域名,不同二級域名(同上) |
不允許(cookie這種情況下也不允許訪問) |
8 |
http://www.a.com/a.js http://www.b.com/b.js |
不同域名 |
不允許 |
其中編號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加這麼一段指令碼:
1 2 3 4 5 6 7 8 |
var topWin = top; function getMessage(data) { alert("messageFormTopWin:" + data); } function sendMessage(data) { topWin.proxyWin = window; topWin.getMessage(data); } |
在a.html加這麼一段指令碼:
1 2 3 4 5 6 7 8 9 10 11 |
var proxyWin = null; function getMessage(data) { alert("messageFormProxyWin:"+data); sendMessage("top has receive data:"+data); } function sendMessage(data) { if (null != proxyWin) { proxyWin.getMessage(data); } } |
也就是必須由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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>a.com</title> </head> <body> <div id="Div1"> A.com/a.html</div> <input id="txt_msg" type="text" /> <input id="Button1" type="button" value="向b.com/b.html傳送一條訊息" onclick="sendMessage(document.getElementById('txt_msg').value)" /> <div id="div_msg"> </div> <iframe width="800" height="400" id="mainFrame" src="<a href="http://localhost:8091/b.com/b.htm">http://localhost:8091/b.com/b.htm</a>"> </iframe> <script type="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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>"> <head> <title>a.com</title> </head> <body> <div id="Div1">A.com/proxy.html</div> <div id="div_msg"></div> <script type="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/>兩¢?秒?後¨®將?跳¬?轉Áa到Ì?B.com/bproxy.html"); window.name = data; setTimeout(function () { location.href = "<a href="http://localhost:8091/b.com/bproxy.htm">http://localhost:8091/b.com/bproxy.htm</a>" }, 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>"> <head> <title>b.com</title> </head> <body> <div id="Div1"> B.com/b.html</div> <input id="txt_msg" type="text" /> <input id="Button1" type="button" value="向A.com/a.html傳送一條訊息" onclick="sendMessage(document.getElementById('txt_msg').value)" /> <div id="div_msg"> </div> <iframe id="proxy" name="proxy" style="width: 600px; height: 300px"></iframe> <script type="text/javascript"> function showMsg(msg) { document.getElementById("div_msg").innerHTML = msg; } function sendMessage(data) { var proxy = document.getElementById("proxy"); proxy.src="<a href="http://localhost:8090/a.com/proxy.htm?data">http://localhost:8090/a.com/proxy.htm?data</a>=" + data; } function connect() { sendMessage("connect"); } function getMessage(data) { showMsg("messageForm a.html to ProxyWin:" + data); connect(); } connect(); // 頁面一載入,就執行一次連線 </script> </body> </html> |
碼片段四,b.com/bproxy.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>"> <head> <title>b.com</title> </head> <body> <div id="Div1"> B.com/bproxy.html</div> <div id="div_msg"> </div> <script type="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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>"> <head runat="server"> <title>A.com/a.html</title> <script type="text/javascript"> var trustedOrigin = "<a href="http://localhost:8091/">http://localhost:8091</a>"; 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); else if (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>: <a href="http://localhost:8090</p">http://localhost:8090</p</a>> <input type="text" id="statusText" value="msg from a.com/a.html"> <button id="sendButton">向b.com/b.html傳送訊息</button> <p>接收到來自a.com/a.html的訊息: <strong id="status"></strong>.<p> <iframe id="widget" width="800" height="400" src="<a href="http://localhost:8091/PostMessage/Default.aspx%22%3E%3C/iframe">http://localhost:8091/PostMessage/Default.aspx"></iframe</a>> </body> </html> |
程式碼片段六,B.com/b.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<html xmlns="<a href="http://www.w3.org/1999/xhtml">http://www.w3.org/1999/xhtml</a>"> <head runat="server"> <title>B.com/b.html</title> <script type="text/javascript"> //檢查postMessage 是否可以用:window.postMessage===undefined //定義信任的訊息源 var trustedOrigin = "<a href="http://localhost:8090/">http://localhost:8090</a>"; function messageHandler(e) { if (e.origin === "<a href="http://localhost:8090/">http://localhost:8090</a>") { 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); else if (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>: <a href="http://localhost:8091</p">http://localhost:8091</p</a>> <p>接收到來自a.com/a.html的訊息: <strong id="status"></strong>.<p> <div> <input type="text" id="messageText" value="msg from b.com/b.html"> <button id="actionButton"> 向a.com/a.html傳送一個訊息</button> </div> </body> </html> |
程式碼的關鍵是message事件是一個擁有data(資料)和origin(來源)屬性的DOM事件。data屬性是傳送的實際資料,origin 屬性是傳送來源。Origin屬性很關鍵,有了這個屬性,接收方可以輕易的忽略掉來自不可信源的訊息,也就能有效避免跨域通訊這個開口給我們的源安全帶來 的隱患。介面很強大,所以程式碼很簡單。我們可以抓包看一下,這個通訊過程完全是在瀏覽器端的,沒有產生任何的網路請求。同時這個介面目前已經得到了絕大多 數瀏覽器的支援,包括IE8及以上版本,參見下面的圖表:
圖7
但是為了覆蓋ie6等低版本瀏覽器,我們完整的方案裡面還是要包含一下相容程式碼,就是最開始介紹的代理頁面的方法了,但必須是以postMessage為主,這樣即便最後會有某些瀏覽器因為這種通訊產生一些網路請求,比例也是非常低的了。