iframe父子頁面間通訊總結
隨著網頁結構的複雜化,頁面內嵌frame越來越常見,但不管是作為內容頁來顯示,還是作為元件模組嵌入,都有父子頁面間通訊的需求可能,因此為了更好的實現需求就必須瞭解父子頁面間的通訊。
iframe子頁面與父頁面通訊根據iframe中src屬性是同域連結還是跨域連結,通訊方式也不同。
同域下父子頁面的通訊
頁面間通訊常以頁面間呼叫實現,包括以下幾個方面:(呼叫包含html dom,js全域性變數,js方法)
- 父頁面呼叫子iframe頁面;
- 子iframe頁面呼叫父頁面;
- 主頁面內兄弟iframe頁面之間相互呼叫;
下面我們詳細分析
父頁面呼叫子iframe
<iframe name="iframeName" id="iframeId" src="child.html"></iframe>
/**
*1、通過iframe的ID獲取子頁面的dom,然後通過內建屬性contentWindow取得子視窗的window物件
* 此方法相容各個瀏覽器
*/
document.getElementById('iframeId').contentWindow.func();
document.getElementById('iframeId').contentWindow.document.getElementById('子頁面中的元素ID');
/**
*2、通過iframe的name(名字)直接獲取子視窗的window物件
*/
iframeName.window.func();
iframeName.window.document.getElementById('子頁面中的元素ID');
/**
*3、通過window物件的frames[]陣列物件直接獲取子frame物件
*/
window.frames[0].func();
window.frames[0].document.getElementById('子頁面中的元素ID');
//或
window.frames["iframeName"].func();
window.frames["iframeName"].document.getElementById('子頁面中的元素ID' );
子iframe頁面呼叫父頁面
/**
*通過parent或top物件獲取父頁面的window物件內元素及方法
*/
parent.window.func();
parent.window.document.getElementById('父頁面中的元素ID');
//同理
top.window.func();
top.window.document.getElementById('父頁面中的元素ID');
頁面內兄弟iframe頁面相互呼叫
其實任意頁面間通訊是類似的,根據上面兩個方法組合就可以實現頁面內兄弟iframe頁面相互呼叫
原理:在子頁面中獲取父頁面(parent)或頂層頁面(top)的window元素,然後再根據兄弟iframe的name或id獲取兄弟頁面的window物件,得到window物件後就可以隨意操作了。
/*以下為在child1.html頁面內訪問兄弟frame頁面*/
<iframe name="iframe1Name" id="iframe1Id" src="child1.html"></iframe>
<iframe name="iframe2Name" id="iframe2Id" src="child2.html"></iframe>
<iframe name="iframe3Name" id="iframe3Id" src="child3.html"></iframe>
/**
*1、通過兄弟iframe的ID獲取其dom,然後通過內建屬性contentWindow取得window物件
* 此方法相容各個瀏覽器
*/
parent.window.document.getElementById('iframe2Id').contentWindow.func();
parent.window.document.getElementById('iframe3Id').contentWindow.document.getElementById('兄弟頁面3中的元素ID');
/**
*2、通過iframe的name(名字)直接獲取子視窗的window物件
*/
parent.window.iframe2Name.window.func();
parent.window.iframe3Name.window.document.getElementById('兄弟頁面3中的元素ID');
/**
*3、通過window物件的frames[]陣列物件直接獲取子frame物件
*/
parent.window.frames[1].func();
top.window.frames[2].document.getElementById('兄弟頁面3中的元素ID');
//或
parent.window.frames["iframe2Name"].func();
parent.window.frames["iframe3Name"].document.getElementById('兄弟頁面3中的元素ID');
跨域下父子頁面的通訊
父頁面向子頁面傳遞資料
如果iframe所連結的是外部頁面,因為安全機制就不能使用同域名下的通訊方式了。
實現的技巧是利用location物件的hash值,通過它傳遞通訊資料。在父頁面設定iframe的src後面多加個data字串,然後在子頁面中通過某種方式能即時的獲取到這兒的data就可以了,例如:
在子頁面中通過setInterval方法設定定時器,監聽location.href的變化即可獲得上面的data資訊
然後子頁面根據這個data資訊進行相應的邏輯處理
子頁面向父頁面傳遞資料
實現技巧就是利用一個代理iframe,它嵌入到子頁面中,並且和父頁面必須保持是同域,然後通過它充分利用上面第一種通訊方式的實現原理就把子頁面的資料傳遞給代理iframe,然後由於代理的iframe和主頁面是同域的,所以主頁面就可以利用同域的方式獲取到這些資料。使用 window.top或者window.parent.parent獲取瀏覽器最頂層window物件的引用。
詳解
先來看看哪些情況下才存在跨域的問題
其中編號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。 直接說可以用代理頁面的方法搞定這個問題吧,但是怎麼代理法,先來看下面這張圖:
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加這麼一段指令碼:
var topWin = top;
function getMessage(data) {
alert("messageFormTopWin:" + data);
}
function sendMessage(data) {
topWin.proxyWin = window;
topWin.getMessage(data);
}
在a.html加這麼一段指令碼:
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就簡單了。我們再來看這個過程可以用圖大概示意一下:
圖例中綠色的雙向箭頭表示可以通訊,橙色的雙向箭頭表示不能直接通訊。 最後我們簡單看一下雙向通訊的實測效果:
b.html每次載入的時候都先給a.html發一個”連線請求”,讓a.html可以找到proxy.html。所以頁面第一次載入的時候會產生三個請求:
每次b.html向a.html傳送訊息的時候會產生一個請求:
每次a.html向b.html傳送訊息的時候會產生兩個請求,其中一個是a.com/proxy.html向b.com/bproxy.html跳轉產生的,另一個是b.html重新向a.html發起“連線請求”時產生的:
最後簡單看一下實測的幾個測試頁面程式碼:
程式碼片段一,a.com/a.html:
<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:
<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/>兩秒後將跳轉到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
<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
<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:
<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:
<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及以上版本,參見下面的圖表:
但是為了覆蓋ie6等低版本瀏覽器,我們完整的方案裡面還是要包含一下相容程式碼,就是最開始介紹的代理頁面的方法了,但必須是以postMessage為主,這樣即便最後會有某些瀏覽器因為這種通訊產生一些網路請求,比例也是非常低的了。