浅谈iframe及其跨域通信

Iframe元素在IE浏览器横行的时代曾大量的使用,应用场景包括引入公共的导航栏、公共登陆框等一些通用的组件,一些后台管理系统也经常能看到iframe的身影。iframe元素能够创建另外一个文档的内联框架,也就是说它有独立的上下文环境,具备沙箱隔离功能。HTML规范很明确的提到过:如果你需要独立的上下文环境,那么就用iframe,如果不是,那就不要用。

虽然建议上我们应该尽量避免使用iframe,但是某些特殊场景下,iframe还是最好的选择。所以了解清楚iframe对于技术的选型是有帮助的。下面简单介绍下iframe的特性和目前应用的场景,当然后面会介绍最主要的iframe跨域通信。

一、iframe的特性

  • iframe会阻塞主页面的onload事件

    这一条很好理解,也比较容易观察到,一个空页面引入一个iframe并监听onload事件,你会发现,只有iframe里的内容加载完成,onload事件才会响应。解决方法也比较简单,就是在主页面onload后去动态创建iframe,这样就避免了阻塞主页面的加载过程。

  • iframe页面和主页面是共享浏览器的并发请求的

    各浏览器的并发请求连接数是有限制的,iframe页面的资源请求是和主页面共享的,这就会导致主页面加载速度被拖累。

  • iframe新开辟的独立环境所带来的内存开销更大

    由于需要维护独立的上下文环境,iframe带来的内存开销会增加,并且iframe容易带来内存泄漏的问题,在变更地址的时候需要做一次清空DOM然后重置到空地址的操作。

  • iframe内的页面跳转无法被浏览器记录管理

    这点比较好理解,也是iframe的独立沙箱的特性导致的。

  • SEO不会读取iframe里的内容

    SEO无法扒取iframe里的内容,当然现在iframe很少应用在页面主体框架上,即使应用了,也有技术方案可以处理SEO的问题。

二、iframe的应用场景

  • 无刷新的文件上传

    在上传文件类型为multipart/form-data时较为常用。

  • 所见即所得编辑器

    目前绝大部分所见即所得编辑器为iframe框架,因为只有这样才能够防止页面上其他样式或者JS全局变量的干扰导致编辑器无法正常运行。

  • 长连接

    利用iframe保持长连接是实时通信的一个技术方案,具体实现大家可以百度,当然现在h5的websocket也相当成熟。不考虑兼容性的情况下,首选websocket。

  • 历史记录管理

    富客户端应用大量的使用了ajax技术来获取数据,这就导致这种应用对于历史记录难于管理。利用iframe可以实现历史记录的管理。

  • 引入第三方页面

  • 在移动端应用从网页调用客户端应用

    这个方案比较黑科技一点,但通常只应用于安卓系统,查看下面的代码,浏览器在接收到无法识别的协议时,就会交给系统处理,系统就会调用对应的APP应用。

1
<iframe src="alipayqr://platformapi/startapp?saId=10000007&clientVersion=3.7.0.0718&qrcode={支付二维码扫描的url}" frameborder="0"></iframe>

三、iframe的跨域通信

iframe跨域通信是这次要说明的重点。跨域是由浏览器的同源策略造成的,同源需要域名、协议、端口均相同,url地址中有任何一点不通,则会产生跨域的问题。跨域的问题会导致iframe和父页面无法通信,无法获取对方的对象。

跨域分为两种情况,一是相同主域名不同的子域名,这种可以通过设置相同的document.domain来达到跨域通信的目的。还有一种是主域名不同,主域名不同并不能简单的设置document.domain来解决,下面就介绍几种针对iframe的跨域通信的解决方法。

1、利用hash来通信

我们先来看一个需求:假设一个a.com嵌套了一个iframe,一个意见反馈的页面,意见反馈的页面主域名为b.com,根据同源策略,主页面和子页面已经跨域。这时候如果需要在加载完意见反馈的页面后动态改变iframe的高度,就变的无法实现了。

根据同源策略,要想主页面获取跨域iframe页面的高度,我们需要一个和主页面同源的中转页面内嵌在子页面里’a.com/proxy.html’。
这样,当子页面获取到文档高度的时候,我们把子页面内嵌的iframe地址改为’a.com/proxy.html#高度’,然后在中转页面中获取hash值,再通过parent.parent访问主页面的对象,以此来达到通信的目的。我们看下实现的代码。

a.com主页面

1
2
3
4
5
6
7
<iframe id="frame" src="b.com/b.html" />
<script>
function setHeight(h){
var iframe = document.getElementById("frame");
iframe.style.height = h + 'px' ;
}
</script>

b.com页面

1
2
3
4
5
6
7
<iframe id="biframe" src="about:blank" />
<script>
window.onload = function(){
var bodyheight = document.documentElement.scrollHeight;
document.getElementById("biframe").src = 'http://a.com/proxy.html'+ "#" + bodyheight;
}
</script>

a.com/proxy.html页面

1
2
3
4
5
<script>
var hash = document.location.hash;
var height = hash.replace("#","")+"px";
parent.parent.setHeight(height);
</sciript>

原理比较简单,通过中转页面的url的hash值通信。但是HASH值有一些缺点,它能容纳的长度并不长,传递的字符串有限,而且对于一些敏感数据的传输,并没有办法隐藏。同时,中转页面需要部署到主域底下,在多方合作上,也会存在一些问题。那是否有更好的方法来更新iframe的跨域通信呢?

2、window.name

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个同域页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。window.name属性的神奇之处在于name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。

那对于升级方案,我们的解决方案思路就清晰了。我们在加载完跨域页面后,把iframe的地址马上重定向到同域页面上,就能获取到子域名的值了。

a.com的a页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
iframe = document.createElement('iframe'),
iframe.src = 'http://127.0.0.1/b.html';
document.body.appendChild(iframe);
var firstload = 0;
iframe.onload = function() {
//因为更改src地址会不断触发onload事件,所以需要一个标记来判断。
if(!firstload){
iframe.src = 'about:blank';
//在iframe载入过程中,迅速重置iframe.src的指向,使之与index.html同源,就能获取到name值了(注意空白页面和任何域名都表示同源。)
alert(iframe.contentWindow.name);
iframe.style.height = iframe.contentWindow.name+'px';
firstload = 1;
}

};
</script>

b.com的b页面

1
2
3
4
5
6
7
<iframe id="biframe" src="about:blank"  frameborder="0"></iframe>
<script>
window.onload = function(){
var bodyheight = document.documentElement.scrollHeight;
window.name = bodyheight;
}
</script>

这样iframe的跨域通信就升级了,window.name支持的数据量足够大。看似是完美的方案。但是,由于window.name并没有锁的概念,在多个iframe通信的情况下,可能会出问题。况且,window.name的变化需要页面保持轮询来监听,还是麻烦了点。那是不是有更好的解决方案呢?当然。。。

3、跨文档消息传送(cross-document messaging)

HTML5新的API,可在不同域之间传送消息,支持IE8+。使用方式也非常简单。

1
otherWindow.postMessage(message, targetOrigin);

otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性;window.open的返回值;通过name或下标从window.frames取到的值。 message: 所要发送的数据,string类型。 targetOrigin: 用于限制otherWindow,“*”表示不作限制。然后要接受消息的页面监听一下message就可以了

我们来看下例子:

a.com的a页面

1
2
3
4
5
6
7
8
9
10
11
<input type="text" id="txt"><button id="btn">发送</button>
<script>
iframe = document.createElement('iframe'),
iframe.src = 'http://127.0.0.1/b.html';
document.body.appendChild(iframe);
var child = iframe.contentWindow;
document.getElementById("btn").onclick = function(){
child.postMessage(document.getElementById("txt").value, "http://127.0.0.1");
//targetOrigin: 用于限制接收的windows,“*”表示不作限制
}
</script>

b.com的b页面

1
2
3
4
5
6
7
8
9
10
11
<div id="info"></div>
<script>
var info = document.getElementById("info");
window.addEventListener('message', function(event){
var data = event.data;
var origin = event.origin;
var msg = document.createElement("p");
msg.innerHTML = data;
info.appendChild(msg);
}, false);
</script>

完成,是不是很简单。postMessage兼容IE8+,如果还要再兼容低版本浏览器,就只能推荐用封装好的类库了。隆重介绍iframe跨域通信插件—MessengerJS:
https://github.com/biqing/MessengerJS