iframe诡异的内容消失问题

问题描述

不得不承认,这是一个非常诡异的问题,以下步骤可以重现问题:

  1. 用IE打开这个下面提供的页面,确认页面上有个iframe,里面显示着“abc”三个字符。
  2. 把这页面加进收藏夹。
  3. 重新打开IE。
  4. 从收藏夹再打开这页面。

页面源码:

<!DOCTYPE html>  
<html>  
<head>  
    <meta charset="utf-8">
    <title>test</title>
</head>  
<body>  
<script type="text/javascript">  
    var text = 'abc',
        script = 'var d = document; d.open(\'text/html\', \'replace\'); d.write(parent.text); d.close();',
        html = '<iframe id="abc" name="abc" src="javascript:void((function() {' + script + '})())"></iframe>';
    document.write(html);
</script>  
</body>  
</html>  

如果没出问题的话,你会发现页面上虽然还有iframe,但是“abc”消失了。更准确、详细地说,前后2次的页面主要有以下区别:

  • 从视觉上来说,页面中的abc字符消失了。
  • 从DOM结构上看,iframe中的<body>元素内没有任何内容。
  • 从iframe的右键-属性上看,第一次页面上iframe的地址是父页面的URL,第二次则变成了about:blank

以下是这个页面的源码,是从遇上问题的页面中不断分离、简化,最后形成的一个最简的重现方案,即上文提供的代码。

解决方案

首先,这个页面虽然简单,但其中用到了几个很恶心人的东西:

  • document.write
  • javascript:伪协议。

如果有办法避免使用这两者的话,就可以忽略这个问题。但是如果必须使用javascript:伪协议来向iframe中输出内容的话,将以上代码改为如下形式可以解决问题:

var iframe = document.createElement('iframe');  
document.body.appendChild(iframe); // 插入到需要的位置  
iframe.contentWindow.location = 'javascript:void((function() {' + script + '})())';  

具体的区别是,从直接使用document.write来输出<iframe>,变为了使用createElement创建<iframe>,随后使用<iframe>location来采用javascript:伪协议输出具体内容。

起因

由于原本这段代码不是我写的,所以在发现这个问题的时候,我也有过疑问,为啥要这么写呢?难道下面的方式不是更好吗:

var iframe = document.createElement('iframe');  
document.body.appendChild(iframe); // 插入到需要的位置  
iframe.contentWindow.document.write('abc');  

然而这一段代码的注释中写是“IE在修改了document.domain进行提权后,<iframe>会出现跨域问题”。所以以下代码,其中在IE中是会报错(拒绝访问)的:

// 假设当前域是www.tt.com
document.domain = 'tt.com'; // domain提权  
var iframe = document.createElement('iframe');  
document.body.appendChild(iframe); // 插入到需要的位置  
iframe.contentWindow.document.write('abc');  

至于解决的办法,就是在<iframe>src中,使用javascript:伪协议输出内容,当然输出的时候要注意,在<iframe>document执行open以后,加上一句代码,把<iframe>docuemnt.domain也进行提权,提升到和父页面相同,这样<iframe>和父页面就是同域的,可以进行交互了。

正是因为IE存在着这样的问题,为了解决这个问题,原有代码中使用了document.write输出带src属性的<iframe>元素,从而引发了另一个问题……

猜测

那么这个问题是因为什么原因引起的呢?首先对页面的执行过程进行分析,大致是这么几步:

  1. 解析<script>标签,执行内容。
  2. document.write向文档流中输出一个<iframe>元素。
  3. <iframe>中,使用document.write输出文字。

从页面的视觉效果而言,<iframe>是存在的,但<iframe>里面的内容消失了。这让人很自然地联想到,第2步已经执行了,但由于浏览器缓存<iframe>的内容等原因,第3步并没有执行。

为了测试这个情况,比较简单的方法就是在<iframe>src里的javascript代码中添加一个断点,我的选择是在d.close()这一句之前,加上了一行代码:alert(d.body.innerHTML);

经过以上的修改,执行的结果是,成功地出现了alert对话框,并且innerHTML确实存在abc字符,另外更奇怪的是,经过alert,abc出现在了iframe中。

也就是说,第3步是确实地执行了,但是在没有alert的情况下,却没有在界面上产生任何效果。综合以上的原因,联想到alert函数的一个作用是将浏览器的UI Update队列进行flush操作,因此对于完全黑盒的IE浏览器,现阶段只能猜测,在这种特殊的使用方式之下,IE丢失了UI Update队列中的部分更新,导致<iframe>document并没有得到更新,因此也保留了about:blank这种地址。

这个问题已经确认为IE丢失了UI Update Queue中的渲染任务,因此可以在d.close()之后加一句document.body.offsetHeight;来强制触发渲染即可。