父子頁面之間跨域通訊的方法

發表於2014-08-08

由於同源策略的限制,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加這麼一段指令碼:

在a.html加這麼一段指令碼:

也就是必須由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:

程式碼片段二,a.com/proxy.html:

程式碼片段三,b.com/b.html:

碼片段四,b.com/bproxy.html:

好吧,現在我必須把話鋒調轉一下了。前面講的這麼多,也只是丟擲來一些之前我們可能會採用的跨域通訊方法,事實上代理頁面、url傳引數和 window.name、甚至還有一些利用url的hash值的跨域傳值方法,都能百度到不少相關資料。但它們都逃不開代理頁面,也就不可避免的要產生網 絡請求,而事實上這並不是我們的本意,我們原本希望它們能夠直接在客戶端通訊,避免不必要的網路請求開銷——這些開銷,在訪問量超大的站點可能會對伺服器 產生相當大的壓力。那麼,有沒有更完美一點的替代方案呢? 必須給大家推薦postMessage。postMessage 正是為了滿足一些合理的、不同站點之間的內容能在瀏覽器端進行互動的需求而設計的。利用postMessage API實現跨域通訊非常簡單,我們直接看一下例項的程式碼: 程式碼片段五,A.com/a.html

程式碼片段六,B.com/b.html:

程式碼的關鍵是message事件是一個擁有data(資料)和origin(來源)屬性的DOM事件。data屬性是傳送的實際資料,origin 屬性是傳送來源。Origin屬性很關鍵,有了這個屬性,接收方可以輕易的忽略掉來自不可信源的訊息,也就能有效避免跨域通訊這個開口給我們的源安全帶來 的隱患。介面很強大,所以程式碼很簡單。我們可以抓包看一下,這個通訊過程完全是在瀏覽器端的,沒有產生任何的網路請求。同時這個介面目前已經得到了絕大多 數瀏覽器的支援,包括IE8及以上版本,參見下面的圖表:

圖7

但是為了覆蓋ie6等低版本瀏覽器,我們完整的方案裡面還是要包含一下相容程式碼,就是最開始介紹的代理頁面的方法了,但必須是以postMessage為主,這樣即便最後會有某些瀏覽器因為這種通訊產生一些網路請求,比例也是非常低的了。

相關文章