让插入到 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 相关文章推荐
Javascript入门学习资料收集整理篇
Jul 06 Javascript
JQuery 确定css方框模型(盒模型Box Model)
Jan 22 Javascript
分析Node.js connect ECONNREFUSED错误
Apr 09 Javascript
js控制web打印(局部打印)方法整理
May 29 Javascript
jQuery Validate插件实现表单验证
Aug 19 Javascript
微信小程序 wx.uploadFile无法上传解决办法
Dec 14 Javascript
javascript实现的图片预览功能
Mar 25 Javascript
jquery应用实例分享_实现手风琴特效
Feb 01 jQuery
JS扩展String.prototype.format字符串拼接的功能
Mar 09 Javascript
微信小程序保存图片到相册权限设置
Apr 09 Javascript
JavaScript实现班级抽签小程序
May 19 Javascript
vue/cli 配置动态代理无需重启服务的方法
May 20 Vue.js
在网页中控制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
PHP4实际应用经验篇(9)
2006/10/09 PHP
php无限分类且支持输出树状图的详细介绍
2013/06/19 PHP
php实现文件下载实例分享
2014/06/02 PHP
CodeIgniter生成静态页的方法
2016/05/17 PHP
PHP中的密码加密的解决方案总结
2016/10/26 PHP
js DOM模型操作
2009/12/28 Javascript
Jquery 常用方法经典总结
2010/01/28 Javascript
Js获取下拉框选定项的值和文本的实现代码
2014/02/26 Javascript
NodeJS学习笔记之(Url,QueryString,Path)模块
2015/01/13 NodeJs
JavaScript+CSS实现仿Mootools竖排弹性动画菜单效果
2015/10/14 Javascript
js改变style样式和css样式的简单实例
2016/06/28 Javascript
浅析Ajax语法
2016/12/05 Javascript
JS实现的数字格式化功能示例
2017/02/10 Javascript
ES6教程之for循环和Map,Set用法分析
2017/04/10 Javascript
webpack开发跨域问题解决办法
2017/08/03 Javascript
ES6新增的math,Number方法
2017/08/06 Javascript
JS实现图片居中悬浮效果
2017/12/25 Javascript
微信小程序出现wx.getLocation再次授权问题的解决方法分析
2019/01/16 Javascript
node实现mock-plugin中间件的方法
2019/12/25 Javascript
基于jQuery实现挂号平台首页源码
2020/01/06 jQuery
Vue3不支持Filters过滤器的问题
2020/09/24 Javascript
[03:02]2014DOTA2西雅图邀请赛 让队员自己告诉你DK NAVI备战情况
2014/07/08 DOTA
[09:43]DOTA2每周TOP10 精彩击杀集锦vol.5
2014/06/25 DOTA
Python生成数字图片代码分享
2017/10/31 Python
基于Python List的赋值方法
2018/06/23 Python
Python编程flask使用页面模版的方法
2018/12/28 Python
Python数据类型之Set集合实例详解
2019/05/07 Python
解决win7操作系统Python3.7.1安装后启动提示缺少.dll文件问题
2019/07/15 Python
python爬虫可以爬什么
2020/06/16 Python
html5中localStorage本地存储的简单使用
2017/06/16 HTML / CSS
酒店服务实习自我鉴定
2013/09/22 职场文书
实用的简历自我评价
2014/03/06 职场文书
师德师风自我评价范文
2014/09/11 职场文书
仓管员岗位职责
2015/02/03 职场文书
pyqt5打包成exe可执行文件的方法
2021/05/14 Python
如何用RabbitMQ和Swoole实现一个异步任务系统
2021/05/29 PHP