本文详细解析JavaScript中iframe的通信机制与高度自适应方案。涵盖同域下利用contentWindow进行函数调用的加载时机处理,以及跨域场景下通过document.domain和location.hash实现不同主域及子域间的数据交互。此外,介绍了利用中间页嵌套结构解决跨域iframe高度自适应的具体实现逻辑。
iframe通信概述与同域通信
首先阐明,iframe通信包含:同域通信与跨域通信两种类型。
一、同域通信场景
同域通信指在 http://localhost/demo/iframe/iframeA.html 下的a.html页面中嵌套iframe的情况。
例如:<iframe src="http://localhost/demo/iframe/iframeB.html" id="iframeA" name="iframeA">嵌入的B.html页面,两者间进行数据交互。若需在父页面A.html调用子页面函数,可参考document.getElementById('iframeA').contentWindow.b(); 方式,其中b为子页面B.html中的函数。然而此调用在火狐浏览器中常报错,原因如下:

提示“b不是个函数”,尽管子页面已定义该函数。经分析与检索,发现当iframe未加载完成时执行JS会触发此错误。因此在火狐下使用iframe.onload 函数进行测试,确认无误,从而确定问题根源。为此编写兼容IE和火狐的函数以检测iframe加载状态,并提供回调函数调用上述方法。
基于上述思路,代码实现如下:
<iframe src="http://localhost/demo/iframe/iframeB.html" id="iframeA" name="iframeA"></iframe>
<div id="topName">topNddddddddddddddddame</div>
<script>
function A(){
alert("A");
}
var iframe = document.getElementById('iframeA');
iframeIsLoad(iframe,function(){
var obj = document.getElementById('iframeA').contentWindow;
obj.b();
});
function iframeIsLoad(iframe,callback){
if(iframe.attachEvent) {
iframe.attachEvent('onload',function(){
callback && callback();
});
}else {
iframe.onload = function(){
callback && callback();
}
}
}
</script>
B.html代码示例:
var b = function(){
alert("B");
}
子页面调用父页面函数十分简便,仅需如下操作:window.parent.A();
子页面获取父页面元素值的方法:window.parent.document.getElementById("topName").innerHTML等。
iframe跨域通信解决方案
二、iframe跨域通信
iframe跨域访问通常分为两种情形:第一种是同主域、不同子域的跨域;第二种是不同主域的跨域。
1、同主域不同子域间的跨域;可通过设置相同的document.domain来解决。
假设当前存在域 abc.example.com 下的页面abc.html,页面上嵌套iframe如下:<iframe src="http://def.example.com/demo/def.html" id="iframe2" style="display:none;"></iframe>。若需在abc域下的页面abc.html访问def域下的def.html,由于浏览器同源策略限制(不同域、协议或端口),js无法直接操作,故需解决跨域访问。若父页面abc.html包含js函数:function test(){console.log(1);}; ,子页面欲调用此函数,仍按同域方式调用parent.test();。在火狐环境下测试发现已跨域,解决方法是在各js函数顶部添加语句document.domain = 'example.com'即可解决。
abc.html代码示例:
<iframe src="http://def.example.com/demo/def.html" id="iframe2" style="display:none;"></iframe>
// 跨域 子页调用父页的 函数 (假设是下面test函数)
document.domain = 'example.com';
function test(){console.log(1);};
def.html代码示例:
/*
* 子页调用父页的方法
*/
document.domain = 'example.com';
//window.top.test();
window.parent.test();
针对这两个页面,若需父页面调用子页面,可采用如下方法:
a.html代码示例:
/*
* 跨域 父页想调用子页的的函数
*/
document.domain = 'example.com';
var iframe = document.getElementById('iframe2');
iframeIsLoad(iframe,function(){
var obj = iframe.contentWindow;
obj.child();
});
function iframeIsLoad(iframe,callback){
if(iframe.attachEvent) {
iframe.attachEvent('onload',function(){
callback && callback();
});
}else {
iframe.onload = function(){
callback && callback();
}
}
}
假设def.html页面存在child函数,代码如下:
document.domain = 'example.com';
function child(){console.log('我是子页');}
如此即可实现跨域调用,无论子页面调用父页面,还是父页面调用子页面,均能正常运行。
2、不同主域跨域情形;
尽管google提供了多种不同主域跨域方法,如通过location.hash方法或window.name方法或html5及flash等,但笔者认为以下iframe方法值得学习。
结构如图所示:域a.com的页面request.html(即http://a.com/demo/ajax/ajaxproxy/request.html)嵌套指向域b.com(http://b.com/demo/ajax/ajaxproxy/response.html)的response.html,而response.html又嵌套域a.com的proxy.html。

思路:要实现a.com域下的request.html请求域b.com下的process.php,可将请求参数通过url传递给response.html,由response.html向process.php发起真正的ajax请求(response.html与process.php同属域b.com),然后将返回结果通过url传递给proxy.html。最后,由于proxy.html和request.html处于同域,可在proxy.html利用window.top将结果返回至request.html,从而完成真正的跨域。

ok,先查看页面结构
a.com域下包含:
request.html
proxy.html
b.com域下包含:
response.html
process.php
先查看request.html页面如下:
<!DOCTYPE HTML>
<html>
<head>
<title> New Document </title>
</head>
<body>
<p id="result">这里将会填上响应的结果</p>
<a id="sendBtn" href="javascript:void(0)">点击,发送跨域请求</a>
<iframe id="serverIf" style="display:none"></iframe>
<script>
document.getElementById('sendBtn').onclick = function() {
var url = 'http://b.com/demo/ajax/ajaxproxy/reponse.html',
fn = 'GetPerson', //这是定义在response.html的方法
reqdata = '{"id" : 24}', //这是请求的参数
callback = "CallBack"; //这是请求全过程完成后执行的回调函数,执行最后的动作
CrossRequest(url, fn, reqdata, callback); //发送请求
}
function CrossRequest(url,fn,reqdata,callback) {
var server = document.getElementById('serverIf');
server.src = url + '?fn=' +encodeURIComponent(fn) + "&data=" +encodeURIComponent(reqdata) + "&callback="+encodeURIComponent(callback);
}
//回调函数
function CallBack(data) {
var str = "My name is " + data.name + ". I am a " + data.sex + ". I am " + data.age + " years old.";
document.getElementById("result").innerHTML = str;
}
</script>
</body>
</html>
该页面旨在告知response.html:需执行其定义好的方法GetPerson,并使用提供的参数'{"id" : 24}'。response.html仅负责将CallBack方法名传递给下一环节proxy.html,而proxy.html获取CallBack方法名后即可执行,因为proxy.html和request.html同域。
response.html代码示例:
<!DOCTYPE HTML>
<html>
<head>
<title> New Document </title>
</head>
<body>
<iframe id="proxy"></iframe>
<script>
// 通用方法 ajax请求
function _request (reqdata,url,callback) {
var xmlhttp;
if(window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
}else {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function(){
if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var data = xmlhttp.responseText;
callback(data);
}
}
xmlhttp.open('POST',url);
xmlhttp.setRequestHeader("Content-Type", "application/json; charset=utf-8");
xmlhttp.send(reqdata);
}
// 通用方法 获取url参数
function _getQuery(key) {
var query = location.href.split('?')[1],
value = decodeURIComponent(query.split(key + "=")[1].split("&")[0]);
return value;
}
//向process.php发送ajax请求
function GetPerson(reqdata,callback) {
var url = 'http://b.com/demo/ajax/ajaxproxy/process.php';
var fn = function(data) {
var proxy = document.getElementById('proxy');
proxy.src = "http://a.com/demo/ajax/ajaxproxy/Proxy.html?data=" + encodeURIComponent(data) + "&callback=" + encodeURIComponent(callback);
};
_request(reqdata, url, fn);
}
(function(){
var fn = _getQuery('fn'),
reqdata = _getQuery("data"),
callback = _getQuery("callback");
eval(fn + "('" + reqdata +"', '" + callback + "')");
})();
</script>
</body>
</html>
此处即接收来自request.html的请求,获取请求参数和方法后向服务器process.php发出真正的ajax请求,然后将服务器返回的数据及从request.html传来的回调函数名传递给proxy.html。
接下来查看php代码,旨在返回json数据:
<?php
$data = json_decode(file_get_contents("php://input"));
header("Content-Type: application/json; charset=utf-8");
echo ('{"id" : ' . $data->id . ', "age" : 24, "sex" : "boy", "name" : "huangxueming"}');
?>
最后是proxy.html代码:
<!DOCTYPE HTML>
<html>
<head>
<title> New Document </title>
</head>
<body>
<script>
function _getUrl(key) {//通用方法,获取URL参数
var query = location.href.split("?")[1],
value = decodeURIComponent(query.split(key + "=")[1].split("&")[0]);
return value;
}
(function() {
var callback = _getUrl("callback"),
data = _getUrl("data");
eval("window.top." + decodeURIComponent(callback) + "(" + decodeURIComponent(data) + ")");
})();
</script>
</body>
</html>
此为最后一步,proxy终于获取了request.html透过response.html传来的回调函数名以及从response.html直接传来的响应数据,利用window.top执行request.html中定义的回调函数。
iframe高度自适应实现
三、iframe高度自适应问题
iframe高度自适应分为两种:一种是同域下自适应,另一种是跨域下自适应。下面探讨同域下iframe高度自适应问题。
1.同域下iframe高度自适应问题:
思路:获取被嵌套iframe元素,通过JavaScript取得被嵌套页面最终高度,然后在主页面进行设置来实现。
假设demo包含iframe1.html和iframe2.html
以下贴上iframe1.html代码:
<!DOCTYPE HTML>
<html>
<head>
<title> New Document </title>
<style>
*{margin:0;padding:0;}
</style>
</head>
<body>
<iframe src="http://a.com/demo/ajax/iframeheight/iframe2.html" style="width:100%;border:1px solid #333;" frameborder="0" id="iframe"></iframe>
<script>
window.onload = function() {
var iframeid = document.getElementById('iframe');
if(iframeid && !window.opera) {
if(iframeid.contentDocument && iframeid.contentDocument.body.offsetHeight) {
iframeid.height = iframeid.contentDocument.body.offsetHeight;
}else if(iframeid.Document && iframeid.Document.body.scrollHeight){
iframeid.height = iframeid.Document.body.scrollHeight;
}
}
}
</script>
</body>
</html>
iframe2.html
<!DOCTYPE HTML>
<html>
<head>
<title> New Document </title>
<style>
*{margin:0;padding:0;}
</style>
</head>
<body>
<div style="height:500px;"></div>
</body>
</html>
即可动态设置iframe1页面的高度为iframe2的高度。
2.跨域下iframe高度自适应问题。
已知iframe跨域时无法使用上述js方式控制,故需引入中间键。可在a.com域下iframe1.html页面嵌套b.com域下的iframe2.html页面,然后在iframe2.html页面嵌套与iframe1.html同域的iframe3.html页面。这样,iframe1.html和iframe3.html即可无障碍通信,因为页面iframe2.html嵌套iframe3.html,所以iframe2.html可改写iframe3.html的href值。
iframe1中的内容:
iframe1.html内容主要接收iframe3.html页面传来的内容并完成相应操作。iframe1.html代码:
<iframe src="http://b.com/demo/ajax/iframeheight/iframe2.html" style="width:400px;height:200px;" id="iframe"></iframe>
<script>
var ifr_el = document.getElementById("iframe");
function getIfrData(data){
ifr_el.style.height = data+"px";
}
</script>
iframe2.html中的内容:
iframe2.html内容如何将值传给iframe3.html页面?前文提及将值传递至iframe3.html页面的href中,故只需修改iframe的src即可。因无需刷新C页面,故可通过hash方式传递给iframe3.html页面。iframe2.html代码:
<!DOCTYPE HTML>
<html>
<head>
<title> New Document </title>
<style>
*{margin:0;padding:0;}
</style>
</head>
<body>
<iframe id="iframe" src="http://a.com/demo/ajax/iframeheight/iframe3.html" width="0" height="230px"></iframe>
<script>
var oldHeight = 0,
ifr_el = document.getElementById("iframe");
t && clearInterval(t);
var t = setInterval(function(){
var height = document.body.scrollHeight;
if(oldHeight != height) {
oldHeight = height;
ifr_el.src += '#' +oldHeight;
}
},200);
</script>
</body>
</html>
可见,默认情况下iframe1.html页面给iframe2.html的高度为200像素,但在iframe2.html给iframe3.html高度为230像素,正常情况下会有滚动条。现需在iframe2.html获取滚动条高度,将高度通过iframe3.html的src传入,然后在iframe3.html页面获取此高度值并传给iframe1.html(因iframe1.html和iframe3.html同域),故iframe1.html能获取该高度值,再设置自身高度为该值即可。
iframe3.html页面的唯一功能是接收iframe2.html页面通过href传进来的值并传递给iframe1.html页面。可从iframe2.html传来的值通过定时器不停查看location.href是否被改变,但此法效率较低。另一种方式是在新浏览器中通过onhashchange事件(IE8+,Chrome5.0+,Firefox3.6+,Safari5.0+,Opera10.6+)来监听href的改变。
iframe3.html代码:
<script>
var oldHeight = 0;
t && clearInterval(t);
var t = setInterval(function(){
var height = location.href.split('#')[1];
if(height && height != oldHeight) {
oldHeight = height;
if(window.parent.parent.getIfrData) {
window.parent.parent.getIfrData(oldHeight);
}
}
},200);
</script>
如此即可解决通过跨域实现iframe自适应高度的问题。
四、总结
以上即本文全部内容,希望对大家的学习工作有所帮助。若有疑问欢迎留言讨论。