执行 innerHTML 里的 <script>
背景
有时候我们会有把一整段 HTML 动态塞进页面的需求,例如渲染了一个模板,从服务器端获取了一段广告代码等。一般情况下我们使用 container.innerHTML 即可。但是当 HTML 中出现 script 标签时,直接使用 innerHTML 并不会执行它。
一个例子
<div id="test">Hello HTML</div>
<script>
document.getElementById('test').innerHTML = 'Hello JS';
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.min.js"></script>
<script>
ReactDOM.render(React.createElement('div', null, 'Hello React'), document.getElementById('test'));
</script>
一个常见的例子里包含普通的 HTML 内容,<script> 里的 inline script,通过 src 引用的外部 script。如果我们尝试直接用 innerHTML 赋值只会得到一个 Hello HTML。而后面的 <script> 标签无一例外没有执行。
appendChild
我们知道通过 appendChild 把 <script> 标签直接塞进页面是可以执行和加载里面的 js 的(JSONP就是通过这种方法实现的,参见之前的文章:JSONP 的实现 - 知乎专栏。
所以其实我们需要做的就只是把所有的 <script>找出来,然后通过 appendChild 塞到页面里即可。
function runScript(script){
// 直接 document.head.appendChild(script) 是不会生效的,需要重新创建一个
const newScript = document.createElement('script');
// 获取 inline script
newScript.innerHTML = script.innerHTML;
// 存在 src 属性的话
const src = script.getAttribute('src');
if (src) newScript.setAttribute('src', src);
document.head.appendChild(newScript);
document.head.removeChild(newScript);
}
function setHTMLWithScript(container, rawHTML){
container.innerHTML = rawHTML;
const scripts = container.querySelectorAll('script');
for (let script of scripts) {
runScript(script);
}
}
执行顺序
当我们尝试用上面的 setHTMLWithScript(document.body, html) 时有一个问题,就是 script 的加载和执行并非同步的,我们会得到一个 Hello, JS。
而下面的 <script> 依赖前面的 <script> 执行加载完成是一个非常常见的需求,因为在正常的静态网页里就是这样的,虽然所有的远程脚本都是异步加载的,但后面的 <script> 会等待前面的加载执行后才开始执行。
为了让异常处理和异步流程的控制更方便,我们让 runScript 返回一个 Promise,然后只需要一个简单的 reduce 就可以把异步逻辑串联起来:
function runScript(script){
return new Promise((reslove, rejected) => {
// 直接 document.head.appendChild(script) 是不会生效的,需要重新创建一个
const newScript = document.createElement('script');
// 获取 inline script
newScript.innerHTML = script.innerHTML;
// 存在 src 属性的话
const src = script.getAttribute('src');
if (src) newScript.setAttribute('src', src);
// script 加载完成和错误处理
newScript.onload = () => reslove();
newScript.onerror = err => rejected();
document.head.appendChild(newScript);
document.head.removeChild(newScript);
if (!src) {
// 如果是 inline script 执行是同步的
reslove();
}
})
}
function setHTMLWithScript(container, rawHTML){
container.innerHTML = rawHTML;
const scripts = container.querySelectorAll('script');
return Array.prototype.slice.apply(scripts).reduce((chain, script) => {
return chain.then(() => runScript(script));
}, Promise.resolve());
}
得到预期的 Hello React。
其实这里有一点和直接渲染不一致的地方,就是脚本的加载也是同步的,后面的脚本会等待之前的脚本执行完才会加载,不过从 js 层面似乎没有办法解决这个问题。
JQuery.html
熟悉 JQuery 的同学可能知道 $.html 其实会直接执行里面的 <script> 标签,不过是同步的,在 $.html 的代码中,可以看到 jQuery 判断满足一定条件下直接使用 innerHTML,随便执行一个 $('body').html(test<script></script>) 然后打个断点,


可以看到这里做了一个简单的正则判断,如果碰到 <script><style><link> 标签就用 jQuery 自己实现的 append,继续追踪下去,

显然 jQuery 在这里完全没有考虑 <script> 前后的依赖。对于 inline script 的标签也是直接通过 eval 实现的而不是新建一个插入到文档里。
JQuery 也有几个 issue 讨论是否要按照顺序执行,但最后决定保持现状:Scripts in inner html are not exectuted sequentially in order · Issue #2538 · jquery/jquery。
其他
createContextualFragment
除了写进去再用 querySelectorAll 把 script 全都拿出来复制一遍外,IE11 以上的浏览器也可以通过 createContextualFragment 直接把 html 转换成 DOM 节点然后 append 到页面上:
var tagString = "<div>I am a div node</div><script>console.log('test')</script>";
var range = document.createRange();
// make the parent of the first div in the document becomes the context node
range.selectNode(document.body);
var documentFragment = range.createContextualFragment(tagString);
document.body.appendChild(documentFragment)
也可以用这种方法来实现上面的功能。
兼容性
上面的代码都只是顺手的探索,没有考虑兼容性方面的问题,例如 IE 不支持 script 的 onload 事件等,可能需要 onreadystatechange 来实现。
DOMContentLoaded
DOMContentLoaded 早已经完成,如果有需要,我们可能要在脚本加载完成后,重新触发一下
setHTMLWithScript(document.body, rawHTML)
.then(() => {
var DOMContentLoadedEvent = document.createEvent('Event');
DOMContentLoadedEvent.initEvent('DOMContentLoaded', true, true);
document.dispatchEvent(DOMContentLoadedEvent);
})
document.write
在静态页面中,<script> 标签里如果出现 document.write,会直接在 <script> 插入的位置写入,这种方法常被用于广告投放脚本来定位自己的位置。
而当我们在动态插入时文档已经关闭,会直接 write 到整个页面上,如果有必要可以暂时替换 document.write 来实现。
getCurrentScript
getCurrentScript 是另一个定位 <script> 标签所在位置的方法,之所以不太常用是因为 IE 不兼容它,如果我们要考虑兼容这个方法新产生的 <script> 标签就不应该往 <head> 里 append,而是插入到原来所在的位置。
总结
以上方法都只是模拟静态 <script> 解析的过程,一般来说我们不要求行为完全一致(毕竟跨域异步加载同步执行这点 JS 就无法模拟),但是可以按照我们的需求去实现它的行为。
这种方法也只适用于一部分场景,如果有更复杂的 JS 动态加载需求应该考虑使用 requirejs 等 AMD Loader。
拓展阅读
- javascript - load and execute order of scripts - Stack Overflow
- Scripts in inner html are not exectuted sequentially in order · Issue #2538 · jquery/jquery
- javascript - Load and execution sequence of a web page? - Stack Overflow
执行 innerHTML 里的 <script>的更多相关文章
- 使用innerHTML生成的script节点不会发出请求与执行text属性
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- 让 innerHTML 进来的 script 代码跑起来
今天来简单聊聊如何让 innerHTML 进来的 scrip 代码跑起来的问题. 前台请求一个接口,接口返回一些 HTML 标签拼接成的字符串,以供前端直接 innerHTML 生成 DOM 元素,这 ...
- 出现“不能执行已释放的Script代码”错误的原因及解决办法
很多web开发者或许都遇到过这样的问题,程序莫名奇怪出现“不能执行已释放Script的代码”,错误行1,列1.对于这种消息描述不着边,行列描述更是让人迷茫的js错误,相信是所有调试js程序的朋友们最郁 ...
- 在package.json里面的script设置环境变量,区分开发及生产环境。注意mac与windows的设置方式不一样
在package.json里面的script设置环境变量,区分开发及生产环境. 注意mac与windows的设置方式不一样. "scripts": { "publish- ...
- Linux 搜某个文件里关键字的上下500行到执行文件里
Linux 搜某个文件里关键字的上下500行到执行文件里grep '300000111110' -C 500 ./saastom7061_APP3/logs/sass.log >/app/saa ...
- IE10弹窗showModalDialog关闭之后提示SCRIPT5011:不能执行已释放的Script代码
在Web开发中,经常使用showModalDialog弹窗 今天遇到一个小问题,IE10中弹窗关闭之后提示SCRIPT5011:不能执行已释放的Script代码 网上搜罗了一些资料,发现大多都提到对象 ...
- PL/SQL 报错:动态执行表不可访问,本会话的自动统计被禁止。 在执行菜单里你可以禁止统计,或在v$session,v$sesstat 和vSstatname表里获得选择权限。
现象: 第一次用PL/SQL Developer连接数据库,若用sys用户登录并操作则正常,若用普通用户比如haishu登录并创建一个表则报错“动态执行表不可访问,本会话的自动统计被禁止.在执行菜单里 ...
- ApiPost如何在预执行脚本里添加请求参数?
ApiPost V3引入了预执行脚本和后执行脚本的概念,详细可以通过链接:<ApiPost的预执行脚本和后执行脚本>了解学习更多.本文主要介绍如何在预执行脚本里增加请求参数. 使用场景 我 ...
- MySQL 执行计划里的rows
<pre name="code" class="html">SQL> alter session set statistics_level=a ...
随机推荐
- 测试环境docker化(一)—基于ndp部署模式的docker基础镜像制作
本文来自网易云社区 作者:孙婷婷 背景 我所在测试项目组目前的测试环境只有一套,在项目版本迭代过程中,开发或产品偶尔会在测试环境进行数据校验,QA人数在不断增加,各个人员在负责不同模块工作时也会产生脏 ...
- 69、Android 布局中轻松实现图片的全屏、居中、平铺
public void paint() { if (item.laying_mode != 1)//平铺或者充满 { new AsyncTask<Void, Void, Void>() { ...
- 玲珑杯”ACM比赛 Round #15
手速狗从西安回来一只浑浑噩噩,好不容易迎来一场送饭比赛体验一把河南的优势,结果被高中生狂虐,无缘奖金..我的奖品梦就这样一次次被打破.... A -- Reverse the lights 最后半小时 ...
- maven国内镜像
<?xml version="1.0" encoding="UTF-8"?> <!--Licensed to the Apache Softw ...
- 【bzoj2836】魔法树 树链剖分+线段树
题目描述 输入 输出 样例输入 4 0 1 1 2 2 3 4 Add 1 3 1 Query 0 Query 1 Query 2 样例输出 3 3 2 题解 树剖+线段树模板题,不过为什么写的人这么 ...
- 通过Url网络编程实现下载
import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputS ...
- 普通平衡树(指针splay)
最早的板子,学自Ez大佬: #include<cstdio> #include<cstdlib> using namespace std; class Splay{ publi ...
- openssl-1.0.1c交叉编译动态库(转)
linux编译相关(13) 版权声明:本文为博主原创文章,未经博主允许不得转载. #交叉编译openssl ------直接修改Makefile新加这一行:CROSS_COMPILE= arm-un ...
- 密码 (pasuwado)
密码 (pasuwado) 题目描述 哪里有压迫,哪里就有反抗. moreD的宠物在法庭的帮助下终于反抗了.作为一只聪明的宠物,他打算把魔法使moreD的魔法书盗去,夺取moreD的魔法能力.但mor ...
- Fabric 和 Sawtooth 技术分析(下)
http://blog.talkingdata.com/?p=6172 在前一篇文章(Fabric和Sawtooth技术分析(上))中,我们着重跟大家分享了 Fabric 相关的内容,在本篇文章中,我 ...