innerHTML是否可以插入脚本?

289
我尝试使用innerHTML在一个<div>中加载一些脚本。看起来脚本已经被加载到DOM中,但是它们从未被执行过(至少在Firefox和Chrome中是这样)。有没有办法在使用innerHTML插入脚本时让它们执行?
示例代码:

<!DOCTYPE html>
<html>
  <body onload="document.getElementById('loader').innerHTML = '<script>alert(\'hi\')<\/script>'">
    Shouldn't an alert saying 'hi' appear?
    <div id="loader"></div>
  </body>
</html>

26个回答

133

下面是一种递归替换所有脚本为可执行脚本的方法:

function nodeScriptReplace(node) {
        if ( nodeScriptIs(node) === true ) {
                node.parentNode.replaceChild( nodeScriptClone(node) , node );
        }
        else {
                var i = -1, children = node.childNodes;
                while ( ++i < children.length ) {
                      nodeScriptReplace( children[i] );
                }
        }

        return node;
}
function nodeScriptClone(node){
        var script  = document.createElement("script");
        script.text = node.innerHTML;

        var i = -1, attrs = node.attributes, attr;
        while ( ++i < attrs.length ) {                                    
              script.setAttribute( (attr = attrs[i]).name, attr.value );
        }
        return script;
}

function nodeScriptIs(node) {
        return node.tagName === 'SCRIPT';
}

示例调用:

nodeScriptReplace(document.getElementsByTagName("body")[0]);

13
我有点惊讶你的回答在下面。依我之见,这是最好的解决方案,该方法甚至允许您限制特定网址或内容的脚本。 - davidmh
[0]的目的是什么?你能使用nodeScriptReplace(document.getElementById().html)吗? - Bao Thai
1
这太神奇了。完美运作。 - Stefan Reich
1
了不起的解决方案,谢谢 :) 如果有人正在寻找更现代化的版本,请看这个链接:https://dev59.com/Z3M_5IYBdhLWcg3w2XLw#69190644 - Johannes Ewald
1
这太棒了。谢谢。 - Dave Guerrero
显示剩余5条评论

107

如果您要执行已插入为DOM文本的任何脚本代码,必须使用eval()

MooTools会自动完成此操作,我相信jQuery也会这样做(具体取决于版本。jQuery 1.6+使用eval)。这可以避免解析<script>标记和转义内容以及其他一些问题。

通常,如果您要使用eval(),则需要创建/发送不带任何HTML标记的脚本代码,例如<script>,因为这些将无法正确地进行eval()


12
我真正想做的是加载外部脚本,而不仅仅是评估一些本地脚本。使用innerHTML添加一个脚本标签比创建脚本DOM元素并将其添加到body中要短得多,我正在尝试使我的代码尽可能简短。你必须创建DOM脚本元素并将它们添加到DOM中,而不是仅仅使用innerHTML吗?有没有办法在函数内部使用document.write来实现这个目的? - Craig
5
正如zombat所建议的那样,使用一个Javascript框架来加载外部脚本,不要试图重新发明轮子。JQuery使这变得非常容易,只需包含JQuery并调用:$.getScript(url)。您还可以提供一个回调函数,一旦脚本加载完成就会执行该函数。 - Ariel Popovsky
2
Ariel是正确的。我很感激你试图让你的代码更简短,并添加一个带有innerHTML的<script>标签可能很短,但它不起作用。直到通过eval()运行之前,它都只是纯文本。不幸的是,eval()无法解析HTML标记,因此您最终会遇到一系列问题。 - zombat
35
eval() 不是解决任何问题的好方法。 - buley
2
我自己尝试过eval()。这是一个可怕的想法。你必须每次都eval整个东西。即使你声明了变量名和值,你也必须每次重新声明/重新eval()它才能工作。这是一个错误的噩梦。 - Youstay Igo
显示剩余2条评论

99

你们是如何将<img src...行添加到网页代码中的?您使用 document.write() 方法还是使用 document.body.innerHTML+= 方法来实现这个目标?对我来说,两者都失败了 :( - Youstay Igo
在“onload”属性中编写大量代码并不切实际。此外,这需要存在并加载另一个文件。momo的解决方案则是更好的选择。 - fregante
两个缺点:1.它无法调用通过innerHTML添加的任何脚本/函数(以及此IMG标记),因为就浏览器而言,它们不存在。2.如果".removeChild()"之前的内联代码部分抛出异常,则img元素将不会被删除。 - user3526
一个快速的提示。如果您正在加载比空gif文件更大的图像(因为它们可能需要比空gif文件更长的时间下载),则此解决方案可能无法为您提供准确的结果。 - newshorts
24
您可以将触发图片进行 Base64 编码,以 <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"> 的形式插入页面中(这不会产生网络请求)。实际上……您并不需要一张实际存在的图片,可以引用一个不存在的图片,并使用 onerror 代替 onload(但这样将产生网络请求)。 - Danny '365CSI' Engelman
显示剩余3条评论

52

您可以创建脚本,然后注入内容。

var g = document.createElement('script');
var s = document.getElementsByTagName('script')[0];
g.text = "alert(\"hi\");"
s.parentNode.insertBefore(g, s);

这在所有浏览器中都有效 :)


1
除非文档中没有其他脚本元素,否则请使用 document.documentElement - Eli Grey
4
不必要,因为您正在从另一个脚本编写脚本。<script> var g = document.createElement('script'); var s = document.getElementsByTagName('script')[0]; // 引用此脚本 g.text = "alert(\"hi\");" s.parentNode.insertBefore(g, s); </script> - Pablo Moretti
3
谁说这是来自另一个脚本的呢?你可以在没有<script>元素的情况下运行JavaScript。例如,使用<img onerror="..." src="#"><body onload="...">。如果您想要技术上的解释,由于名称空间不明确,这在非HTML/SVG文档中也无法工作。 - Eli Grey
2
Facebook在其SDK中使用了Pablo的答案。https://developers.facebook.com/docs/javascript/quickstart/v2.2#loading - geoyws

51
每次我想动态插入一个脚本标签时,我都会这样做:
const html =
  `<script>
      alert(' there ! Wanna grab a '); 
  </script>`;

const scriptEl = document.createRange().createContextualFragment(html);
parent.append(scriptEl);

注意:这里使用的是ES6。
我看到很多答案都使用了appendChild,它的用法和append完全一样。

你可以使用这个不错的React组件 - https://github.com/christo-pr/dangerously-set-html-content - Yash Vekaria
8
这个方法完美地起作用了,而且显然是这里最简单的解决方案。我不明白为什么没有更高的投票数。 - Jesse
1
这不会运行脚本。 - eeerrrttt
1
它按预期工作。我设法将所有内容放在一行中。 - Neyelson Alves
2
谢谢您的回答!非常好的解决方案!小小的澄清:appendChildappend并不完全相同:append接受文本/字符串,而appendChild方法只能用于将节点对象插入到DOM中。在某些情况下,一个更适合使用其中之一(如果您想强制使用节点对象并避免在代码中使用类似<p>的字符串,或者如果您想要附加字符串)。但在大多数情况下,两者都可以使用。 - Berci
显示剩余2条评论

31

我使用了这段代码,它正常工作。

var arr = MyDiv.getElementsByTagName('script')
for (var n = 0; n < arr.length; n++)
    eval(arr[n].innerHTML)//run script inside div

1
谢谢。这解决了我在使用TinyBox2 Jquery插件创建的模态弹出窗口中添加Disqus Universal代码的问题。 - gsinha
5
很遗憾,当脚本包含稍后将被调用的函数时,此解决方案无法运行。 - Jose Gómez

26

我遇到了innerHTML的问题,我必须将Hotjar脚本附加到Reactjs应用程序的"head"标签中,并且它必须在附加后立即执行。

动态将节点导入到“head”标签的一个好的解决方案之一是React-helmet模块。


此外,针对所提出的问题有一个有用的解决方案:

innerHTML中不要包含脚本标签!

事实证明,HTML5不允许使用innerHTML属性动态添加脚本标签。因此,以下代码将不会执行,也不会弹出Hello World!的警告。

element.innerHTML = "<script>alert('Hello World!')</script>";

这在HTML5规范中有记录:

注意:使用innerHTML插入的script元素在插入时不会执行。

但要注意,这并不意味着innerHTML对跨站脚本攻击是安全的。可以通过innerHTML执行JavaScript而不使用标签,如MDN的innerHTML页面所示。
解决方案:动态添加脚本
要动态添加脚本标记,您需要创建一个新的script元素并将其附加到目标元素。
您可以为外部脚本执行此操作:
var newScript = document.createElement("script");
newScript.src = "http://www.example.com/my-script.js";
target.appendChild(newScript);

还有内联脚本:

var newScript = document.createElement("script");
var inlineScript = document.createTextNode("alert('Hello World!');");
newScript.appendChild(inlineScript); 
target.appendChild(newScript);

5
你的newScript元素具有HTMLScriptElement接口,所以你可以通过newScript.text = "alert('Hello World!')"直接设置内联代码,无需创建和附加新文本节点。 - Martin
1
当然,在编程世界中有许多不同的实现方式!而且那样更清晰、易于维护 :)。 - daniellalasa

12

这里是 mmm 的优秀方案的更现代(且更简洁)版本:

function executeScriptElements(containerElement) {
  const scriptElements = containerElement.querySelectorAll("script");

  Array.from(scriptElements).forEach((scriptElement) => {
    const clonedElement = document.createElement("script");

    Array.from(scriptElement.attributes).forEach((attribute) => {
      clonedElement.setAttribute(attribute.name, attribute.value);
    });
    
    clonedElement.text = scriptElement.text;

    scriptElement.parentNode.replaceChild(clonedElement, scriptElement);
  });
}

注意:我还尝试使用cloneNode()outerHTML等替代方案,但都没奏效。


1
美观而完整。我喜欢脚本元素最终位于containerElement内部的方式。 - Herbert Van-Vliet
1
在我的情况下,添加的HTML包含两个脚本标签,第一个加载外部脚本,第二个内联引用它。这会导致错误,说外部脚本导出的变量未定义。 如果我直接包含这两个脚本标签,代码就可以正常工作。我猜测是第一个脚本标签在第二个执行时还没有完全完成。 - lionelbrits

7
你可以这样做:
var mydiv = document.getElementById("mydiv");
var content = "<script>alert(\"hi\");<\/script>";

mydiv.innerHTML = content;
var scripts = mydiv.getElementsByTagName("script");
for (var i = 0; i < scripts.length; i++) {
    eval(scripts[i].innerText);
}

7

这里提供了一种不使用 eval 的解决方案,可以与脚本链接脚本以及模块一起使用。

该函数接受 3 个参数:

  • html:包含要插入的 HTML 代码的字符串
  • dest:指向目标元素的引用
  • append:布尔标记,允许将 HTML 追加到目标元素的末尾
function insertHTML(html, dest, append=false){
    // if no append is requested, clear the target element
    if(!append) dest.innerHTML = '';
    // create a temporary container and insert provided HTML code
    let container = document.createElement('div');
    container.innerHTML = html;
    // cache a reference to all the scripts in the container
    let scripts = container.querySelectorAll('script');
    // get all child elements and clone them in the target element
    let nodes = container.childNodes;
    for( let i=0; i< nodes.length; i++) dest.appendChild( nodes[i].cloneNode(true) );
    // force the found scripts to execute...
    for( let i=0; i< scripts.length; i++){
        let script = document.createElement('script');
        script.type = scripts[i].type || 'text/javascript';
        if( scripts[i].hasAttribute('src') ) script.src = scripts[i].src;
        script.innerHTML = scripts[i].innerHTML;
        document.head.appendChild(script);
        document.head.removeChild(script);
    }
    // done!
    return true;
}

我的意思是...附加一个带有代码内容的脚本标签就是一个eval,不是吗? - Kevin B
1
@KevinB,这里有一些臭名昭著的差异...尝试使用eval('console.log(this)'),你会看到最明显的一个。 - colxi
1
@KevinB 不是 eval。试试这个 eval('let b=100') ..然后尝试从 eval 外部访问 b....祝你好运,你会需要它的。 - colxi
1
没问题,干杯 - Bezzzo
哇,它确实运行良好。 - Jintor
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接