让插入到 innerHTML 中的 script 跑起来的实现代码


Posted in Javascript onJuly 01, 2006

这个问题在某些时候微不足道,甚至可以忽略,但有些时候,这个问题就非常严重,它很可能让我们的程序得不到预期的结果。因此我们需要解决这个问题。

如果你读过 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 中会出错。

上面的分析看上去很完美了,但是实际上还是有问题,一个是 document.write 和 document.writeln 的问题,这个问题在 Blueidea 上,bound0 给出了一个思路,就是替换掉默认的 document.write 和 document.writeln 方法,不过他用的是字符串替换,因此只对内部脚本有效,对外部脚本就没办法了,因此我想了个更通用的办法,就是直接把 document.write 和 document.writeln 重新定义,这样不管内部脚本还是外部脚本执行的就都是我们我们自己定义的 document.write 和 document.writeln 了。不过也有副作用,就是这两个函数在当前页面中就不能再像原来一样使用了,不过这两个函数在页面加载完之后一般是不会再用到了,因此这里重新定义它们所带来的副作用影响很小。但是还有个问题是,尽管这样,我们仍然无法保证 document.write 或 document.writeln 输出的内容会显示在最合适的位置,它只是把内容附加到了我们放置内容的容器中。

另一个问题是 eval 引起的问题,一个是 Blueidea 上的 hutia 说的作用域的问题,另一个问题是如果用 eval 执行的内部脚本的话,内部脚本会在外部脚本加载完之前就开始执行了。要解决这个两个问题可以采用 window.setTimeout 这个函数,让每个脚本都延时一段后再执行,外部脚本延时时间可以设的较长,以保证其能够完全加载,而内部脚本则可以设置为很短,因为一个脚本执行的时间通常是很短的,这样既可以保证不会改变作用域,又可以基本保证脚本执行顺序不会改变了(这种方法对于保证执行顺序上也不一定会 100% 有效,如果网络非常繁忙,外部脚本可能在设置的时间内加载不完,但至少比直接用 eval 的时候好多了)。

如果按照前面的方式实现,对于大多数脚本来说可以正常执行了。但是如果 script 中带有 defer 属性,IE 会自己运行那段代码(前面提过了),因此它会打乱执行的顺序。另外 document.write 和 document.writeln 写入的代码都回被添加到最后面,而不是脚本所在的位置上,因此这也是个问题。

为了解决这两个问题,我们需要对前面的解决方法作一些改变。首先我们不能先把内容赋值给 innerHTML,然后再通过它取脚本了,我们需要直接对内容分析来取出脚本。另外,脚本以外的 HTML 部分也不能直接赋值给 innerHTML,需要在脚本执行以后,将原有的 HTML 内容和 document.write\writeln 写的内容按照顺序合并到一起再赋值给 innerHTML,这里要注意,我们不能一部分一部分的将这些内容连接到 innerHTML 后面,因为其中可能有半个标签的内容,这种情况下,浏览器很容易发生错误。而且你会看到页面反复刷新的情况出现。而如果先放入缓冲区,最后一次赋给 innerHTML,就不会出现这种问题了。

另外放入缓冲区的好处是,当脚本执行完后,可以检查缓冲区中是否还有新的脚本,如果有,再递归执行,这样就可以解决 document.write 和 document.writeln 写的脚本也可以执行的问题了。

2006-6-4 更新:

修正了插入到 innerHTML 中的脚本无法获取插入到 innerHTML 中对象的问题。(感谢网友 DE 的提醒)。

增加了对同一容器中内容设置的共享锁,使得连续设置同一个容器内的时,不会再发生冲突。(感谢新加坡网友 Jason Li 的提醒)。

2006-5-29 更新:

增加了使用外部脚本缓存功能,提高了第二次加载相同外部脚本的速度。

2006-5-23 更新:

在热心的使用者 johnZEN 的提醒下,增加了共享锁,使得同时设置多个容器内的内容时,不会再发生冲突。

在网友 udbjatwfn 的提醒下,修正了 IE 中存在的内部脚本执行作用域错误的问题。

下面是最后本人的实现代码:

/* innerhtml.js 
* Copyright Ma Bingyao <andot@ujn.edu.cn> 
* Version: 1.9 
* LastModified: 2006-06-04 
* This library is free. You can redistribute it and/or modify it. 
* http://www.coolcode.cn/?p=117 
*/ var global_html_pool = []; 
var global_script_pool = []; 
var global_script_src_pool = []; 
var global_lock_pool = []; 
var innerhtml_lock = null; 
var document_buffer = ""; 
function set_innerHTML(obj_id, html, time) { 
if (innerhtml_lock == null) { 
innerhtml_lock = obj_id; 
} 
else if (typeof(time) == "undefined") { 
global_lock_pool[obj_id + "_html"] = html; 
window.setTimeout("set_innerHTML('" + obj_id + "', global_lock_pool['" + obj_id + "_html']);", 10); 
return; 
} 
else if (innerhtml_lock != obj_id) { 
global_lock_pool[obj_id + "_html"] = html; 
window.setTimeout("set_innerHTML('" + obj_id + "', global_lock_pool['" + obj_id + "_html'], " + time + ");", 10); 
return; 
} 
function get_script_id() { 
return "script_" + (new Date()).getTime().toString(36) 
+ Math.floor(Math.random() * 100000000).toString(36); 
} 
document_buffer = ""; 
document.write = function (str) { 
document_buffer += str; 
} 
document.writeln = function (str) { 
document_buffer += str + "\n"; 
} 
global_html_pool = []; 
var scripts = []; 
html = html.split(/<\/script>/i); 
for (var i = 0; i < html.length; i++) { 
global_html_pool[i] = html[i].replace(/<script[\s\S]*$/ig, ""); 
scripts[i] = {text: '', src: '' }; 
scripts[i].text = html[i].substr(global_html_pool[i].length); 
scripts[i].src = scripts[i].text.substr(0, scripts[i].text.indexOf('>') + 1); 
scripts[i].src = scripts[i].src.match(/src\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|([^\s]*)[\s>])/i); 
if (scripts[i].src) { 
if (scripts[i].src[2]) { 
scripts[i].src = scripts[i].src[2]; 
} 
else if (scripts[i].src[3]) { 
scripts[i].src = scripts[i].src[3]; 
} 
else if (scripts[i].src[4]) { 
scripts[i].src = scripts[i].src[4]; 
} 
else { 
scripts[i].src = ""; 
} 
scripts[i].text = ""; 
} 
else { 
scripts[i].src = ""; 
scripts[i].text = scripts[i].text.substr(scripts[i].text.indexOf('>') + 1); 
scripts[i].text = scripts[i].text.replace(/^\s*<\!--\s*/g, ""); 
} 
} 
var s; 
if (typeof(time) == "undefined") { 
s = 0; 
} 
else { 
s = time; 
} 
var script, add_script, remove_script; 
for (var i = 0; i < scripts.length; i++) { 
var add_html = "document_buffer += global_html_pool[" + i + "];\n"; 
add_html += "document.getElementById('" + obj_id + "').innerHTML = document_buffer;\n"; 
script = document.createElement("script"); 
if (scripts[i].src) { 
script.src = scripts[i].src; 
if (typeof(global_script_src_pool[script.src]) == "undefined") { 
global_script_src_pool[script.src] = true; 
s += 2000; 
} 
else { 
s += 10; 
} 
} 
else { 
script.text = scripts[i].text; 
s += 10; 
} 
script.defer = true; 
script.type = "text/javascript"; 
script.id = get_script_id(); 
global_script_pool[script.id] = script; 
add_script = add_html; 
add_script += "document.getElementsByTagName('head').item(0)"; 
add_script += ".appendChild(global_script_pool['" + script.id + "']);\n"; 
window.setTimeout(add_script, s); 
remove_script = "document.getElementsByTagName('head').item(0)"; 
remove_script += ".removeChild(document.getElementById('" + script.id + "'));\n"; 
remove_script += "delete global_script_pool['" + script.id + "'];\n"; 
window.setTimeout(remove_script, s + 10000); 
} 
var end_script = "if (document_buffer.match(/<\\/script>/i)) {\n"; 
end_script += "set_innerHTML('" + obj_id + "', document_buffer, " + s + ");\n"; 
end_script += "}\n"; 
end_script += "else {\n"; 
end_script += "document.getElementById('" + obj_id + "').innerHTML = document_buffer;\n"; 
end_script += "innerhtml_lock = null;\n"; 
end_script += "}"; 
window.setTimeout(end_script, s); 
}

JS调用方法:
JavaScript代码
set_innerHTML('要插入innerhtml的ID名称', '要插入的代码');

方案2:来自ajaxwing的innerHTML简单版
不过这个实现有一点问题就是脚本中的 document.write 和 document.writeln 缩写的内容位置是不对的。

调用方法:
JavaScript代码
setInnerHTML('DOM 树中的节点', '要插入的代码');
JavaScript代码

/* 
* 描述:跨浏览器的设置 innerHTML 方法 
* 允许插入的 HTML 代码中包含 script 和 style 
* 作者:kenxu <ken@ajaxwing.com> 
* 日期:2006-03-23 
* 参数: 
* el: 合法的 DOM 树中的节点 
* htmlCode: 合法的 HTML 代码 
* 经测试的浏览器:ie5+, firefox1.5+, opera8.5+ 
*/ 
var setInnerHTML = function (el, htmlCode) { 
var ua = navigator.userAgent.toLowerCase(); 
if (ua.indexOf('msie') >= 0 && ua.indexOf('opera') < 0) { 
htmlCode = '<div style="display:none">for IE</div>' + htmlCode; 
htmlCode = htmlCode.replace(/<script([^>]*)>/gi, 
'<script$1 defer>'); 
el.innerHTML = htmlCode; 
el.removeChild(el.firstChild); 
} else { 
var el_next = el.nextSibling; 
var el_parent = el.parentNode; 
el_parent.removeChild(el); 
el.innerHTML = htmlCode; 
if (el_next) { 
el_parent.insertBefore(el, el_next) 
} else { 
el_parent.appendChild(el); 
} 
} 
}

基于原作者的不让转载,导致所有的测试代码都没有了,本来应该留一份的。唉
不过三水点靠木特为大家制作了一个例子,以后大家可以全站的使用js控制广告,减少连接数。
https://3water.com/article/20068.htm
Javascript 相关文章推荐
不间断滚动JS打包类,基本可以实现所有的滚动效果,太强了
Dec 08 Javascript
JS将所有对象s的属性复制给对象r(原生js+jquery)
Jan 25 Javascript
JavaScript中number转换成string介绍
Dec 31 Javascript
js实现可兼容IE、FF、Chrome、Opera及Safari的音乐播放器
Feb 11 Javascript
xmlplus组件设计系列之分隔框(DividedBox)(8)
May 02 Javascript
全面解析vue中的数据双向绑定
May 10 Javascript
详解Vue用自定义指令完成一个下拉菜单(select组件)
Oct 31 Javascript
angular基于ng-alain定义自己的select组件示例
Feb 23 Javascript
AngularJs用户输入动态模板XSS攻击示例详解
Apr 21 Javascript
JS中判断字符串存在和非空的方法
Sep 12 Javascript
Vue实现简单的拖拽效果
Aug 25 Javascript
js+html+css实现手动轮播和自动轮播
Dec 30 Javascript
在网页中控制wmplayer播放器
Jul 01 #Javascript
Javascript的IE和Firefox兼容性汇编
Jul 01 #Javascript
提高代码性能技巧谈—以创建千行表格为例
Jul 01 #Javascript
对textarea框的代码调试,而且功能上使用非常方便,酷
Jun 30 #Javascript
列表内容的选择
Jun 30 #Javascript
会自动逐行上升的文本框
Jun 30 #Javascript
自动检查并替换文本框内的字符
Jun 30 #Javascript
You might like
支持oicq头像的留言簿(二)
2006/10/09 PHP
php实现ip白名单黑名单功能
2015/03/12 PHP
PHP实现通过URL提取根域名
2016/03/31 PHP
Yii2实现上下联动下拉框功能的方法
2016/08/10 PHP
PHP+redis实现微博的推模型案例分析
2019/07/10 PHP
用javascript实现自定义标签
2007/05/08 Javascript
Prototype1.6 JS 官方下载地址
2007/11/30 Javascript
Json对象与Json字符串互转(4种转换方式)
2013/03/27 Javascript
面向对象继承实例(a如何继承b问题)(自写)
2013/07/01 Javascript
JavaScript中按位“异或”运算符使用介绍
2014/03/14 Javascript
jQuery的end()方法使用详解
2015/07/15 Javascript
理解JavaScript中worker事件api
2015/12/25 Javascript
安装使用Mongoose配合Node.js操作MongoDB的基础教程
2016/03/01 Javascript
深入理解JavaScript中的for循环
2017/02/07 Javascript
js实现手机发送验证码功能
2017/03/13 Javascript
jQuery获取table下某一行某一列的值实现代码
2017/04/07 jQuery
快速理解 JavaScript 中的 LHS 和 RHS 查询的用法
2017/08/24 Javascript
JavaScript实现为事件句柄绑定监听函数的方法分析
2017/11/14 Javascript
详解node.js中的npm和webpack配置方法
2018/01/21 Javascript
layui默认选中table的CheckBox复选框方法
2019/09/19 Javascript
微信小程序连续签到7天积分获得功能的示例代码
2020/08/20 Javascript
python实现从web抓取文档的方法
2014/09/26 Python
python中正则表达式的使用详解
2014/10/17 Python
Python中字典和JSON互转操作实例
2015/01/19 Python
微信 用脚本查看是否被微信好友删除
2016/10/28 Python
使用XML库的方式,实现RPC通信的方法(推荐)
2017/06/14 Python
Python Tensor FLow简单使用方法实例详解
2020/01/14 Python
Python @property及getter setter原理详解
2020/03/31 Python
python自动生成sql语句的脚本
2021/02/24 Python
枚举和一组预处理的#define有什么不同
2016/09/21 面试题
编辑找工作求职信分享
2014/01/03 职场文书
世界读书日的活动方案
2014/08/20 职场文书
小学教育见习报告
2014/10/31 职场文书
教师年度考核个人总结
2015/02/12 职场文书
2015年乡镇卫生院妇幼保健工作总结
2015/05/19 职场文书
反腐倡廉观后感
2015/06/08 职场文书