让插入到 innerHTML 中的 script 跑起来

innerhtml.htm.rar

在做 ajax 编程时,我们常常需要将 xmlhttp 获取到的页面内容通过 innerHTML 来赋给某个容器(比如 div、span 或者 td 等),但是这里存在一个问题,就是我们将要赋给 innerHTML 的页面内容如果包含有脚本程序,这些脚本程序不管是外部脚本,还是内部脚本,可能(1)都不会被执行。这个问题在某些时候微不足道,甚至可以忽略,但有些时候,这个问题就非常严重,它很可能让我们的程序得不到预期的结果。因此我们需要解决这个问题。

如果你读过 MSDN,你会发现并非所有插入到 innerHTML 中的脚本都不能执行,如果这段脚本的 script 标签中包含了 defer 属性,IE 会正确的执行这些脚本程序。但不幸的是,Moziila/Firefox 和 Opera 可不吃这一套,不管 script 标签有没有设置 defer 属性,这些浏览器都不会向 IE 那样去执行插入到 innerHTML 中的脚本。

但不管脚本是否被执行了,有一点我们可以肯定,那就是这些脚本确实被插入到了 innerHTML 中,如果不相信,你可以 alert 一下看看。但如果你真的 alert 了,你也可能会发现有一种例外情况存在,那就是如果脚本在 innerHTML 内容开头的话,那么 IE 浏览器将会忽略掉这段脚本,而 Moziila/Firefox 和 Opera 却不会。

好了,问题分析的差不多了,我们来看看如何解决吧。

解决的思路其实很简单,那就是将插入到 innerHTML 中的所有脚本取出来,然后一一执行。不过我们先要解决上面的两个问题。

先来看第一个问题,如何避免在 IE 中重复执行 innerHTML 中带有 defer 属性的脚本。这个很容易,只需要先确定浏览器是否是 IE,然后再检测将要执行的脚本是否带有 defer 属性即可。需要注意的是,在判断 IE 浏览器时,我们需要避免被 opera 的浏览器识别欺骗。这一点我们在后面的代码中将会看到它是如何做的。

接下来,看 IE 忽略 innerHTML 开头脚本的问题,这个也很容易解决。只需要在要插入到 innerHTML 中的内容的开头附加一段不是脚本的内容,就可以了。但不要试图附加一个空内容的标签,或者空格、回车、换行等,这将不起作用,开头的脚本仍然会被忽略。也不要试图附加  ,虽然这可以让开头的脚本不再被忽略,但这个   仍然会影响原有内容的显示,虽然你可能觉得不明显,但是对于挑剔的用户来说,这可能是无法容忍的。因此,为了让附加的内容既可以起到避免开头脚本被忽略的功能,又不会造成不良影响,我们将附加这么一段内容:

<span style="display: none">hack ie</span>

虽然上面这段内容有一定的长度,但是它并不会显示,而且这个插入的标签没有 id 也没有 name,所以也不会跟原来内容中的某些标签的 id 或者 name 产生冲突。不过这里有一点要注意,这里也要判断是否是 IE,然后再决定加不加这段内容,因为其他某些浏览器可能不支持 display: none 这个 CSS 修饰(例如 Opera Mini),如果加上这段代码会影响最终的显示效果。

下面我们来看看如何取出脚本并执行。

取出脚本很容易,只需要用 innerHTML 所在对象的 getElementsByTagName 方法就可以了,这个方法对几乎所有的容器标签都管用。取出脚本以后,我们要一一判断它们是外部脚本还是内部脚本。

先来看外部脚本,如果是外部脚本,我们选择了这样一种方法,即先创建这个外部脚本的一个副本对象,并设置它的 defer 属性为 true(这一点是为了让 IE 浏览器能正确执行),然后用 appendChild 方法将这个副本对象插入到 head 中。这里你可能会问,为什么不是插入到 innerHTML 所在的对象中呢?插入到 innerHTML 所在的对象中不是更好吗?如果你试一下就会知道,如果插入到 innerHTML 所在的对象中,在 IE 浏览器中没有问题,但是在 Mozilla/Firefox 和 Opera 浏览器中会有一些问题。问题是如果在 Firefox 上这样做,浏览器会停止响应(这是在 Firefox 1.5 上的测试结果,其他版本是否有此问题,尚不得知),而在 Opera 上,脚本会莫名其妙的执行两次(这是在 Opera 8.5 上的测试结果,其它版本的 Opera 是否由此问题,也尚不得知)。为了避免这些问题,所以我选择了插入到 head 中。

再来看内部脚本,内部脚本的内容我们可以直接用脚本对象的 text 属性来获取,这里我们使用脚本对象的 text 属性而不是 innerHTML 属性,是因为在 Opera 浏览器中,脚本对象的 innerHTML 属性是空的,只有用 text 属性才能获取到脚本内容。执行内部脚本直接用 eval 即可。但是脚本可能会被包含在 HTML 的注释标签中,因此我们需要先将注释标签去掉,不然在 IE 中会出错。

——————————–
(1) 注:在这里,我们用了限定词“可能”,因为有一种情况,脚本会被执行,在下文中你将会看到这种情况。

好了,有了上面的分析,我们就可以很容易的写出一个让插入到 innerHTML 中的 script 跑起来的程序了,下面就是这个程序的代码:

下载: innerhtml.js
  • /* innerhtml.js
  •  * Copyright Ma Bingyao <andot@ujn.edu.cn>
  •  * Version: 1.1
  •  * LastModified: 2006-01-31
  •  * This library is free.  You can redistribute it and/or modify it.
  •  * http://www.coolcode.cn/?p=117
  •  */
  •  
  • function set_innerHTML(obj, html) {
  •     var ie = navigator.appVersion.match(/MSIE/);
  •     var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1);
  •     if (ie && !opera) {
  •         html = '<span style="display: none">hack ie</span>' + html;
  •     }
  •     obj.innerHTML = html;
  •     var scripts = obj.getElementsByTagName("script");
  •     if (scripts) {
  •         var script;
  •         for (var i = 0; i < scripts.length; i++) {
  •             script = scripts[i].text.replace(/(^\s*)|(\s*$)/g, "");
  •             if (!ie || opera || !scripts[i].defer) {
  •                 if (scripts[i].src) {
  •                     script = document.createElement("script");
  •                     script.src = scripts[i].src;
  •                     script.defer = true;
  •                     script.type =  scripts[i].type;
  •                     var head = document.getElementsByTagName("head").item(0);
  •                     head.appendChild(script);
  •                 }
  •                 else if (script.substr(0, 4) == "<!--") {
  •                     eval(script.substr(4));
  •                 }
  •                 else {
  •                     eval(script);
  •                 }
  •             }
  •         }
  •     }
  • }

演示程序地址:Demo

当然还有一点需要注意,如果这些脚本中包含有 document.write 或者 document.writeln 之类的输出语句,不要期望它们能够在正确的位置被执行。至少上面的这个函数还不行,如果你有好的思路或办法来解决这个问题,欢迎您提出来!

posted @ 2007-05-22 22:37  Winner.Net(2007)  阅读(2073)  评论(0编辑  收藏  举报