在 DOM 中动态插入并执行脚本

DOM HTML JavaScript jQuery

在 HTML 中脚本以 <script> 来标记,通过设置其内容或src属性执行内联脚本或外部脚本。 本文讨论动态地插入脚本标签时浏览器对它的解析、下载和执行行为。 动态插入脚本的场景可能包括使用 AJAX 获取脚本并动态执行(多用于性能优化), 以及运行时决定执行页面模板中的某段脚本(多用于单页异步)。

动态执行脚本还有其他方式,比如evalnew Function,这些不在本文的讨论范围。

执行内联脚本

为了插入内联脚本,可以创建一个script元素并设置其内容,插入到 DOM 即可立即执行。 例如:

var script = document.createElement('script');
script.text = 'console.log("foo")';
document.body.appendChild(script);

以下写法是等价的

script.text = 'console.log("foo")';
script.innerText = 'console.log("foo")';
script.innerHTML = 'console.log("foo")';

需要注意的是内联脚本是否能够执行仍然受制于CSP策略指令, 该策略是由Content-Security-Policy响应头(rfc7762)控制的。 例如下列设置将会禁止执行harttle.com以外的任何内联脚本。

Content-Security-Policy: script-src harttle.com;

执行外部脚本

插入并执行外部脚本的方法与内联脚本类似,只需设置script.src属性并插入到 DOM。 例如:

var script = document.createElement('script');
script.src = 'foo.js';
document.body.appendChild(script);

与内联脚本不同的是,外部脚本的插入是异步的不会阻塞 DOM 解析。 详见异步渲染的下载和阻塞行为一文。

此外有一个细节可能需要注意:一旦设置了src属性,<script> 标签本身的所有内容就不会再被执行了。

innerHTML

innerHTML属性可用来设置 DOM 内容,但不可用来插入并执行<script>。 下面的内联脚本和外部脚本都不会被执行:

document.body.innerHTML = '<script src="foo.js"></script>'
document.body.innerHTML = '<script>console.log("foo")</script>'

在设置 innerHTML 时,浏览器会初始化一个新的 HTML Parser 来解析它。 只要与该 Parser 关联的 DOM 启用了 JavaScript(通常是启用的),脚本的 scripting flag 就为真, 但是即便如此,HTML 片段的解析过程中,脚本是不会执行的

Create a new HTML parser, and associate it with the just created Document node. – 12.4 Parsing HTML fragments, WHATWG

The scripting flag can be enabled even when the parser was originally created for the HTML fragment parsing algorithm, even though script elements don’t execute in that case. – 12.2.3.5 Other parsing state flags, WHATWG

事实上,设置innerHTMLouterHTML都不执行脚本,但document.write()是会同步执行的。

When inserted using the document.write() method, script elements execute (typically blocking further script execution or HTML parsing), but when inserted using innerHTML and outerHTML attributes, they do not execute at all. – 4.12.1 The script element WHATWG

jQuery DOM Eval

我们知道使用 jQuery html() 方法时插入的脚本总是执行的,jQuery 会检查传入的内容,并执行其中的每一个脚本。 源码在src/core/DOMEval.js

function DOMEval( code, doc ) {
    doc = doc || document;
    var script = doc.createElement( "script" );
    script.text = code;
    doc.head.appendChild( script ).parentNode.removeChild( script );
}

扩展阅读

转载请注明来源: http://harttle.com/2017/01/16/dynamic-script-insertion.html 欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论(可能需要在能访问 disqus 服务的网络),也可以邮件至 harttle@harttle.com

看看这个?