web-dev-qa-db-fra.com

Les scripts peuvent-ils être insérés avec innerHTML?

J'ai essayé de charger des scripts dans une page en utilisant innerHTML sur un <div>. Il semble que le script se charge dans le DOM, mais il n’est jamais exécuté (du moins dans Firefox et Chrome). Est-il possible d'exécuter des scripts lors de leur insertion avec innerHTML?

Exemple de code:

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

Vous devez utiliser eval () pour exécuter tout code de script que vous avez inséré en tant que texte DOM.

MooTools le fera automatiquement pour vous, et jQuery le ferait également (en fonction de la version. JQuery version 1.6+ utilise eval). Cela évite beaucoup de difficultés à analyser les balises <script> et à échapper à votre contenu, ainsi qu'à de nombreux autres "pièges".

Généralement, si vous allez vous-même eval(), vous voulez créer/envoyer le code de script sans aucun balisage HTML tel que <script>, car ils ne vont pas eval() correctement.

76
zombat

Voici une solution très intéressante à votre problème: http://24ways.org/2005/have-your-dom-and-script-it-too

Utilisez donc ceci à la place des balises de script:

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

78
user447963

Voici une méthode qui remplace de manière récursive tous les scripts par des exécutables:

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;
}

Exemple d'appel:

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

Vous pouvez créer un script puis injecter le contenu.

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

Cela fonctionne dans tous les navigateurs:)

38
Pablo Moretti

J'ai utilisé ce code, ça marche bien

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

Pour ceux qui essaient encore de faire cela, non, vous ne pouvez pas injecter de script avec innerHTML, mais il est possible de charger une chaîne dans une balise de script à l'aide de Blob et URL.createObjectURL.

J'ai créé un exemple qui vous permet d'exécuter une chaîne en tant que script et d'obtenir les "exportations" du script renvoyées via une promesse:

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)
    });
}

J'ai simplifié cela depuis mon implémentation réelle, donc aucune promesse qu'il n'y a pas de bugs. Mais le principe fonctionne.

Si vous ne souhaitez pas récupérer de valeur après l'exécution du script, c'est encore plus simple. laissez simplement les bits Promise et onload. Vous n'avez même pas besoin d'encapsuler le script ou de créer la propriété globale window.__load_script_exports_.

4
JayArby

Voici une fonction récursive pour définir le innerHTML d'un élément que j'utilise dans notre serveur de publicité:

// 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);
    }
}

Vous pouvez voir la démo ici .

2
Adnan Korkmaz

Utilisez $(parent).html(code) au lieu de parent.innerHTML = code.

Ce qui suit corrige également les scripts qui utilisent document.write et les scripts chargés via l'attribut src. Malheureusement, même cela ne fonctionne pas avec les scripts 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
    })
}

Source

1
iirekm

Krasimir Tsonev a une excellente solution qui résout tous les problèmes. Sa méthode n'a pas besoin d'utiliser eval, il n'y a donc aucun problème de performances ni de sécurité. Il vous permet de définir une chaîne innerHTML contenant html avec js et de la traduire immédiatement en un élément DOM tout en exécutant les parties js existant le long du code. court, simple et fonctionne exactement comme vous le souhaitez.

Profitez de sa solution:

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

Notes IMPORTANTES:

  1. Vous devez envelopper l'élément cible avec la balise div
  2. Vous devez envelopper la chaîne src avec une balise div.
  3. Si vous écrivez directement la chaîne src et qu'elle inclut des parties js, veillez à écrire correctement les balises de script de fermeture (avec\before /) car il s'agit d'une chaîne.
1
user3198805

Voici une solution qui n'utilise pas eval et fonctionne avec scripts, scripts liés, ainsi qu'avec modules.

La fonction accepte 3 paramètres:

  • html: Chaîne avec le code html à insérer
  • dest: référence à l'élément cible
  • append: indicateur booléen pour permettre l'ajout à la fin de l'élément cible 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;
}
1
colxi

Vous pouvez le faire comme ceci:

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

Essayez d’utiliser template et document.importNode. Voici un exemple:

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

Ma solution à ce problème est de définir un Mutation Observer pour détecter les nœuds <script></script>, puis le remplacer par un nouveau nœud <script></script> avec le même src. Par exemple:

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});
0
gabriel garcia

La mention de MutationObservers par Gabriel Garcia est sur la bonne voie, mais ne m'a pas tout à fait fonctionné. Je ne sais pas si cela était dû à un caprice du navigateur ou à une erreur de mon côté, mais la version qui a fini par fonctionner pour moi était la suivante:

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});
};

Bien sûr, vous devez remplacer element_to_watch par le nom de l'élément en cours de modification.

node.parentElement.added est utilisé pour stocker les balises de script ajoutées à document.head. Dans la fonction utilisée pour charger la page externe, vous pouvez utiliser quelque chose comme ceci pour supprimer les balises de script qui ne sont plus pertinentes:

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]);
    }
}

Et un exemple du début d'une fonction de chargement:

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
}
0
pixelherodev

Oui, vous le pouvez, mais vous devez le faire en dehors du DOM et l'ordre doit être correct.

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

... alertera 'foo'. Cela ne fonctionnera pas:

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

Et même cela ne fonctionnera pas, car le nœud est inséré en premier:

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