¿Se pueden insertar scripts con innerHTML?

223

Traté de cargar algunos scripts en una página usando innerHTMLun <div>. Parece que el script se carga en el DOM, pero nunca se ejecuta (al menos en Firefox y Chrome). ¿Hay alguna forma de ejecutar scripts al insertarlos innerHTML?

Código de muestra:

<!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>

Craig
fuente

Respuestas:

88

Debe usar eval () para ejecutar cualquier código de script que haya insertado como texto DOM.

MooTools lo hará por usted automáticamente, y estoy seguro de que jQuery también lo hará (dependiendo de la versión. JQuery versión 1.6+ utiliza eval). Esto ahorra mucha molestia de analizar <script>etiquetas y escapar de su contenido, así como un montón de otras "trampas".

En general, si va a eval()hacerlo usted mismo, desea crear / enviar el código del script sin ningún marcado HTML como <script>, ya que estos no lo harán eval()correctamente.

zombat
fuente
12
Lo que realmente quiero hacer es cargar un script externo, no solo evaluar un script local. Agregar una etiqueta de script con innerHTML es mucho más corto que crear un elemento DOM de script y agregarlo al cuerpo, y estoy tratando de hacer mi código lo más corto posible. ¿Tiene que crear los elementos del script dom y agregarlos al dom en lugar de simplemente usar algo como innerHTML? ¿Hay alguna manera de hacer esto con document.write desde una función?
Craig el
55
Como sugiere zombat, use un marco de Javascript para cargar el script externo, no intente reinventar la rueda. JQuery lo hace extremadamente fácil, solo incluya JQuery y llame a: $ .getScript (url). También puede proporcionar una función de devolución de llamada que se ejecutará una vez que se cargue el script.
Ariel Popovsky
2
Ariel tiene razón. Aprecio tratar de mantener su código corto, y agregar una <script>etiqueta innerHTMLpuede ser corto, pero no funciona. Todo es solo texto plano hasta que se ejecuta eval(). Y lamentablemente, eval()no analiza las etiquetas HTML, por lo que terminas con una cadena de problemas.
zombat
21
eval () no es una gran solución para ningún problema.
buley
2
Intenté evaluar () yo mismo. Es una idea horrible. Tienes que evaluar todo el asunto CADA VEZ . Incluso si declara un nombre y un valor de variable, debe volver a declarar / reevaluar () cada vez para que funcione. Es una pesadilla de errores.
Youstay Igo
88

Aquí hay una solución muy interesante para su problema: http://24ways.org/2005/have-your-dom-and-script-it-too

Entonces use esto en lugar de las etiquetas de script:

<img src="empty.gif" onload="alert('test');this.parentNode.removeChild(this);" />

usuario447963
fuente
12
¡Esto es brillante!
Confile
No funciona para mí, cuando se inserta en un resultado de una solicitud ajax: falta el error de sintaxis; antes de la declaración al comienzo de la secuencia de comandos
Oliver
¿Cómo agregan ustedes la línea & lt; img src ... a su código de página? ¿Lo documentas, escribes () o usas el enfoque document.body.innerHTML + = para eso? Ambos me fallan :(
Youstay Igo
No es muy práctico escribir mucho código dentro de un onloadatributo. Además, esto requiere que exista un archivo adicional y que se cargue. La solución de momo es un compromiso menor.
fregante
15
Podría Base64 codificar su imagen de activación como <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">(esto no hará una solicitud de red) En realidad ... NO necesita una imagen, hacer referencia a una imagen no existente y en lugar de onloadusaronerror (pero esto hará una solicitud de red)
Danny '365CSI' Engelman
83

Aquí hay un método que reemplaza recursivamente todos los scripts por ejecutables:

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

        return node;
}
function nodeScriptIs(node) {
        return node.tagName === 'SCRIPT';
}
function nodeScriptClone(node){
        var script  = document.createElement("script");
        script.text = node.innerHTML;
        for( var i = node.attributes.length-1; i >= 0; i-- ) {
                script.setAttribute( node.attributes[i].name, node.attributes[i].value );
        }
        return script;
}

Llamada de ejemplo:

nodeScriptReplace(document.getElementsByTagName("body")[0]);
mmm
fuente
99
Estoy un poco sorprendido de que tu respuesta sea totalmente negativa. En mi humilde opinión, esta es la mejor solución, este método incluso le permitiría restringir las secuencias de comandos con URL o contenido específico.
davidmh
1
@ inf3rno hace o no? Solía ​​funcionar, ¿alguien alguna vez afirmó algo diferente?
mmm
¿Cuál es el propósito de [0]? ¿puedes usar nodeScriptReplace (document.getElementById (). html);
Bao Thai
@BaoThai Sí. Usted puede.
mmm
No parece ayudar en IWebBrowser2; Puedo confirmar que las etiquetas de script se recrean con createElement, pero aún no puedo invocarlas a través de InvokeScript ().
Dave
46

Puede crear un script y luego inyectar el contenido.

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

Esto funciona en todos los navegadores :)

Pablo Moretti
fuente
1
A menos que no haya otros elementos de script en el documento. Usar en su document.documentElementlugar.
Eli Gray
44
No es necesario porque está escribiendo un script desde otro script. <script> var g = document.createElement('script'); var s = document.getElementsByTagName('script')[0]; //reference this script g.text = "alert(\"hi\");" s.parentNode.insertBefore(g, s); </script>
Pablo Moretti
3
¿Quién dice que es de otro guión? Puede ejecutar JavaScript sin <script>elementos. Por ejemplo <img onerror="..." src="#">y <body onload="...">. Si desea ser técnico, esto no funcionará en documentos que no sean HTML / SVG debido al espacio de nombres no explícito.
Eli Gray
2
Facebook usa la respuesta de Pablo en su SDK. developers.facebook.com/docs/javascript/quickstart/v2.2#loading
geoyws
30

Usé este código, está funcionando bien

var arr = MyDiv.getElementsByTagName('script')
for (var n = 0; n < arr.length; n++)
    eval(arr[n].innerHTML)//run script inside div
Firas Nizam
fuente
1
Gracias. Solucionó mi problema de agregar el código Disqus Universal a una ventana emergente modal creada usando el complemento TinyBox2 Jquery.
gsinha
3
Desafortunadamente, esta solución no funciona cuando el script contiene funciones que se invocarán más adelante.
Jose Gómez
7

Tuve este problema con innerHTML, tuve que agregar un script Hotjar a la etiqueta "head" de mi aplicación Reactjs y tendría que ejecutarse justo después de agregarlo.

Una de las buenas soluciones para la importación dinámica de nodos en la etiqueta "head" es el módulo React-helment .


Además, hay una solución útil para el problema propuesto:

¡No hay etiquetas de script en innerHTML!

Resulta que HTML5 no permite que las etiquetas de script se agreguen dinámicamente usando la propiedad innerHTML. Por lo tanto, lo siguiente no se ejecutará y no habrá ninguna alerta que diga ¡Hola mundo!

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

Esto está documentado en la especificación HTML5:

Nota: los elementos de script insertados con innerHTML no se ejecutan cuando se insertan.

Pero cuidado, esto no significa que innerHTML esté a salvo de secuencias de comandos entre sitios. Es posible ejecutar JavaScript a través de innerHTML sin usar etiquetas como se ilustra en la página innerHTML de MDN .

Solución: agregar scripts dinámicamente

Para agregar dinámicamente una etiqueta de secuencia de comandos, debe crear un nuevo elemento de secuencia de comandos y agregarlo al elemento de destino.

Puede hacer esto para scripts externos:

var newScript = document.createElement("script");
newScript.src = "http://www.example.com/my-script.js";
target.appendChild(newScript);

Y guiones en línea:

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

Para cualquiera que todavía intente hacer esto, no, no puede inyectar un script usando innerHTML, pero es posible cargar una cadena en una etiqueta de script usando un Bloby URL.createObjectURL.

He creado un ejemplo que le permite ejecutar una cadena como secuencia de comandos y obtener las 'exportaciones' de la secuencia de comandos a través de una promesa:

function loadScript(scriptContent, moduleId) {
    // create the script tag
    var scriptElement = document.createElement('SCRIPT');

    // create a promise which will resolve to the script's 'exports'
    // (i.e., the value returned by the script)
    var promise = new Promise(function(resolve) {
        scriptElement.onload = function() {
            var exports = window["__loadScript_exports_" + moduleId];
            delete window["__loadScript_exports_" + moduleId];
            resolve(exports);
        }
    });

    // wrap the script contents to expose exports through a special property
    // the promise will access the exports this way
    var wrappedScriptContent =
        "(function() { window['__loadScript_exports_" + moduleId + "'] = " + 
        scriptContent + "})()";

    // create a blob from the wrapped script content
    var scriptBlob = new Blob([wrappedScriptContent], {type: 'text/javascript'});

    // set the id attribute
    scriptElement.id = "__loadScript_module_" + moduleId;

    // set the src attribute to the blob's object url 
    // (this is the part that makes it work)
    scriptElement.src = URL.createObjectURL(scriptBlob);

    // append the script element
    document.body.appendChild(scriptElement);

    // return the promise, which will resolve to the script's exports
    return promise;
}

...

function doTheThing() {
    // no evals
    loadScript('5 + 5').then(function(exports) {
         // should log 10
        console.log(exports)
    });
}

He simplificado esto desde mi implementación real, por lo que no prometo que no haya errores. Pero el principio funciona.

Si no le importa recuperar ningún valor después de que se ejecute el script, es aún más fácil; acaba de dejar de lado las Promisey los onloadbits. Ni siquiera necesita envolver el script o crear la window.__load_script_exports_propiedad global .

JayArby
fuente
1
Acabo de probarlo y funciona en Chrome 57. innerHTML en una etiqueta de script ejecuta el texto.
iPherian
Eso es interesante, no funcionó antes. Me pregunto si este comportamiento es entre navegadores o solo en Chrome 57.
JayArby
4

Aquí hay una función recursiva para establecer el innerHTML de un elemento que uso en nuestro servidor de anuncios:

// o: container to set the innerHTML
// html: html text to set.
// clear: if true, the container is cleared first (children removed)
function setHTML(o, html, clear) {
    if (clear) o.innerHTML = "";

    // Generate a parseable object with the html:
    var dv = document.createElement("div");
    dv.innerHTML = html;

    // Handle edge case where innerHTML contains no tags, just text:
    if (dv.children.length===0){ o.innerHTML = html; return; }

    for (var i = 0; i < dv.children.length; i++) {
        var c = dv.children[i];

        // n: new node with the same type as c
        var n = document.createElement(c.nodeName);

        // copy all attributes from c to n
        for (var j = 0; j < c.attributes.length; j++)
            n.setAttribute(c.attributes[j].nodeName, c.attributes[j].nodeValue);

        // If current node is a leaf, just copy the appropriate property (text or innerHTML)
        if (c.children.length == 0)
        {
            switch (c.nodeName)
            {
                case "SCRIPT":
                    if (c.text) n.text = c.text;
                    break;
                default:
                    if (c.innerHTML) n.innerHTML = c.innerHTML;
                    break;
            }
        }
        // If current node has sub nodes, call itself recursively:
        else setHTML(n, c.innerHTML, false);
        o.appendChild(n);
    }
}

Puedes ver la demo aquí .

Adnan Korkmaz
fuente
3

Podrías hacerlo así:

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);
}
Jake
fuente
3

Aquí una solución que no utiliza eval, y funciona con scripts , scripts vinculados , así como con módulos .

La función acepta 3 parámetros:

  • html : cadena con el código html para insertar
  • dest : referencia al elemento objetivo
  • append : bandera booleana para habilitar la adición al final del elemento objetivo 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;
}
colxi
fuente
Quiero decir ... Agregar una etiqueta de script con contenido de código es una evaluación, ¿no es así?
Kevin B
@KevinB Hay diferencias notorias ... intente eval('console.log(this)')y verá la más obvia
colxi
entonces el contexto es diferente, y? sigue siendo solo una evaluación.
Kevin B
@KevinB No, no es una evaluación. Intente esto eval('let b=100')... y luego intente acceder bdesde fuera de la evaluación ... buena suerte con eso, lo va a necesitar
colxi
Funciona para mi. Saludos
Bezzzo
1

Krasimir Tsonev tiene una gran solución que supera todos los problemas. Su método no necesita usar eval, por lo que no existen problemas de rendimiento ni seguridad. Le permite configurar la cadena innerHTML que contiene html con js y traducirla inmediatamente a un elemento DOM mientras también ejecuta las partes js que existen a lo largo del código. corto, simple y funciona exactamente como quieras.

Disfruta su solución:

http://krasimirtsonev.com/blog/article/Convert-HTML-string-to-DOM-element

Notas importantes:

  1. Debe envolver el elemento de destino con la etiqueta div
  2. Necesita envolver la cadena src con la etiqueta div.
  3. Si escribe la cadena src directamente e incluye partes js, preste atención para escribir las etiquetas de script de cierre correctamente (con \ before /) ya que esta es una cadena.
usuario3198805
fuente
1

Usar en $(parent).html(code)lugar de parent.innerHTML = code.

Lo siguiente también corrige los scripts que usan document.writey los scripts cargados a través de srcatributos. Desafortunadamente, incluso esto no funciona con los scripts de Google AdSense.

var oldDocumentWrite = document.write;
var oldDocumentWriteln = document.writeln;
try {
    document.write = function(code) {
        $(parent).append(code);
    }
    document.writeln = function(code) {
        document.write(code + "<br/>");
    }
    $(parent).html(html); 
} finally {
    $(window).load(function() {
        document.write = oldDocumentWrite
        document.writeln = oldDocumentWriteln
    })
}

Fuente

iirekm
fuente
1
un poco tarde aquí, pero cualquiera que esté usando este método, tenga en cuenta que en JQuery necesita cargar sus scripts usando $ .loadScript (url) en lugar de <script src = "url> </script> - esto último causará un obsoleta error síncrono XMLHttpRequest en los navegadores.
Stavm
1

Intente usar template y document.importNode. Aquí hay un ejemplo:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Sample</title>
</head>
<body>
<h1 id="hello_world">Sample</h1>
<script type="text/javascript">
 var div = document.createElement("div");
  var t = document.createElement('template');
  t.innerHTML =  "Check Console tab for javascript output: Hello world!!!<br/><script type='text/javascript' >console.log('Hello world!!!');<\/script>";
  
  for (var i=0; i < t.content.childNodes.length; i++){
    var node = document.importNode(t.content.childNodes[i], true);
    div.appendChild(node);
  }
 document.body.appendChild(div);
</script>
 
</body>
</html>

gris
fuente
1
Esto no funciona con Microsoft Edge, ¿alguna otra solución?
Soul
1

También puede envolverlo <script>así y se ejecutará:

<your target node>.innerHTML = '<iframe srcdoc="<script>alert(top.document.title);</script>"></iframe>';

Tenga en cuenta: el alcance interno se srcdocrefiere al iframe, por lo que debe usar topcomo en el ejemplo anterior para acceder al documento principal.

naden
fuente
0

Sí, puede hacerlo, pero debe hacerlo fuera del DOM y el orden debe ser correcto.

var scr = '<scr'+'ipt>alert("foo")</scr'+'ipt>';
window.onload = function(){
    var n = document.createElement("div");
    n.innerHTML = scr;
    document.body.appendChild(n);
}

... alertará a 'foo'. Esto no funcionará:

document.getElementById("myDiv").innerHTML = scr;

E incluso esto no funcionará, porque el nodo se inserta primero:

var scr = '<scr'+'ipt>alert("foo")</scr'+'ipt>';
window.onload = function(){
    var n = document.createElement("div");
    document.body.appendChild(n);
    n.innerHTML = scr;  
}
mwilcox
fuente
16
Por lo que vale: esto no parece funcionar en los navegadores actuales.
Wichert Akkerman
0

Mi solución para este problema es configurar un Observador de mutaciones para detectar <script></script>nodos y luego reemplazarlo por un nuevo <script></script>nodo con el mismo src. Por ejemplo:

let parentNode = /* node to observe */ void 0
let observer = new MutationObserver(mutations=>{
    mutations.map(mutation=>{
        Array.from(mutation.addedNodes).map(node=>{
            if ( node.parentNode == parentNode ) {
                let scripts = node.getElementsByTagName('script')
                Array.from(scripts).map(script=>{
                    let src = script.src
                    script = document.createElement('script')
                    script.src = src
                    return script
                })
            }
        })
    })
})
observer.observe(document.body, {childList: true, subtree: true});
gabriel garcia
fuente
1
Gracias por votarme sin decir por qué. Los amo a todos.
gabriel garcia
0

La mención de Gabriel Garcia de MutationObservers está en el camino correcto, pero no funcionó para mí. No estoy seguro de si fue por una peculiaridad del navegador o por un error de mi parte, pero la versión que terminó funcionando para mí fue la siguiente:

document.addEventListener("DOMContentLoaded", function(event) {
    var observer = new MutationObserver(mutations=>{
        mutations.map(mutation=>{
            Array.from(mutation.addedNodes).map(node=>{
                if (node.tagName === "SCRIPT") {
                    var s = document.createElement("script");
                    s.text=node.text;
                    if (typeof(node.parentElement.added) === 'undefined')
                        node.parentElement.added = [];
                    node.parentElement.added[node.parentElement.added.length] = s;
                    node.parentElement.removeChild(node);
                    document.head.appendChild(s);
                }
            })
        })
    })
    observer.observe(document.getElementById("element_to_watch"), {childList: true, subtree: true,attributes: false});
};

Por supuesto, debe reemplazarlo element_to_watchcon el nombre del elemento que se está modificando.

node.parentElement.addedse usa para almacenar las etiquetas de script que se agregan a document.head. En la función utilizada para cargar la página externa, puede usar algo como lo siguiente para eliminar las etiquetas de script que ya no son relevantes:

function freeScripts(node){
    if (node === null)
        return;
    if (typeof(node.added) === 'object') {
        for (var script in node.added) {
            document.head.removeChild(node.added[script]);
        }
        node.added = {};
    }
    for (var child in node.children) {
        freeScripts(node.children[child]);
    }
}

Y un ejemplo del comienzo de una función de carga:

function load(url, id, replace) {
    if (document.getElementById(id) === null) {
        console.error("Element of ID "+id + " does not exist!");
        return;
    }
    freeScripts(document.getElementById(id));
    var xhttp = new XMLHttpRequest();
    // proceed to load in the page and modify innerHTML
}
pixelherodev
fuente
Te das cuenta de que estás agregando un nuevo MutationObservercada vez que se agrega un elemento al documento, ¿verdad? Por cierto, me pregunto por qué dices que mi código no es funcional.
gabriel garcia
@gabrielgarcia Dije que su código no funcionaba porque lo probé y simplemente no funcionó. Mirándolo ahora, es completamente posible que haya estado en mí, no en ti, y me disculpo sinceramente por la forma en que lo expresé. Arreglando ahora.
pixelherodev
re: agregar un MutationObserver cada vez que se agrega un elemento al documento, ¿de qué estás hablando? DOMContentLoaded, y cito de MDN aquí, "se dispara cuando el documento HTML inicial se ha cargado y analizado por completo, sin esperar a que las hojas de estilo, imágenes y subtramas terminen de cargarse". Eso es una vez, y solo una vez. Además, este script funciona sin problemas en mi sitio, y la depuración muestra que solo ocurre una vez, por lo que es una vez en la práctica, así como en teoría.
pixelherodev
1
tienes razón ... lo he equivocado. Mis disculpas también.
gabriel garcia
@gabrielgarcia No es un problema :)
pixelherodev
0

Para mí, la mejor manera es insertar el nuevo contenido HTML a través de innerHtml y luego usar

setTimeout(() => {
        var script_el = document.createElement("script")
        script_el.src = 'script-to-add.js'
        document.body.appendChild(script_el)
    }, 500)

El setTimeout no es obligatorio pero funciona mejor. Esto funcionó para mí.

Luis Tiago Flores Cristóvão
fuente
-1

Ejecutar (Java Script) etiqueta de innerHTML

Reemplace su elemento de script con div que tenga un atributo de clase class = "javascript" y ciérrelo con </div>

No cambie el contenido que desea ejecutar (anteriormente estaba en la etiqueta del script y ahora está en la etiqueta div)

Agregue un estilo en su página ...

<style type="text/css"> .javascript { display: none; } </style>

Ahora ejecute eval usando jquery (Jquery js ya debería estar incluido)

   $('.javascript').each(function() {
      eval($(this).text());

    });`

Puedes explorar más aquí , en mi blog.

Servicio de conocimiento
fuente