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


223

J'ai essayé de charger des scripts dans une page en utilisant innerHTMLsur <div>. Il semble que le script se charge dans le DOM, mais il n'est jamais exécuté (au moins dans Firefox et Chrome). Existe-t-il un moyen d'exécuter des scripts lors de leur insertion 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>

Réponses:


88

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 je suis sûr que jQuery le ferait également (selon la version. JQuery version 1.6+ utilise eval). Cela vous évite d'avoir à analyser les <script>balises et à échapper à votre contenu, ainsi qu'un tas d'autres "pièges".

Généralement, si vous y allez vous- eval()même, vous souhaitez créer / envoyer le code de script sans balisage HTML tel que <script>, car ceux-ci ne fonctionneront pas eval()correctement.


12
Ce que je veux vraiment faire, c'est charger un script externe, pas seulement évaluer un script local. Ajouter une balise de script avec innerHTML est beaucoup plus court que de créer un élément DOM de script et de l'ajouter au corps, et j'essaie de rendre mon code aussi court que possible. Devez-vous créer les éléments de script dom et les ajouter au dom plutôt que d'utiliser simplement quelque chose comme innerHTML? Existe-t-il un moyen de le faire avec document.write depuis une fonction?
Craig

5
Comme le suggère zombat, utilisez un framework Javascript pour charger le script externe, n'essayez pas de réinventer la roue. JQuery rend cela extrêmement facile, il suffit d'inclure JQuery et d'appeler: $ .getScript (url). Vous pouvez également fournir une fonction de rappel qui sera exécutée une fois le script chargé.
Ariel Popovsky

2
Ariel a raison. J'apprécie d'essayer de garder votre code court et d'ajouter une <script>balise avec innerHTMLpeut-être court, mais cela ne fonctionne pas. Ce n'est que du texte brut jusqu'à ce qu'il soit exécuté eval(). Et malheureusement, eval()n'analyse pas les balises HTML, vous vous retrouvez donc avec une chaîne de problèmes.
zombat

21
eval () n'est pas une excellente solution à tout problème.
buley

2
J'ai moi-même essayé eval (). C'est une horrible idée. Vous devez évaluer le tout CHAQUE FOIS . Même si vous déclarez un nom et une valeur de variable, vous devez à nouveau le déclarer / le réévaluer () à chaque fois pour le faire fonctionner. C'est un cauchemar d'erreurs.
Youstay Igo

88

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


12
C'est génial!
confile

Ne fonctionne pas pour moi, lorsqu'il est inséré dans le résultat d'une requête ajax: Erreur de syntaxe manquante; avant la déclaration au début de la chaîne de script
Oliver

Comment ajoutez-vous la ligne & lt; img src ... à votre code de page? Le documentez-vous.write () ou utilisez-vous l'approche document.body.innerHTML + = pour cela? Les deux échouent pour moi :(
Youstay Igo

Pas très pratique pour écrire beaucoup de code dans un onloadattribut. Cela nécessite également l'existence d'un fichier supplémentaire et son chargement. La solution de momo est moins un compromis.
fregante

15
Vous pourriez Base64 encoder votre image de déclenchement comme <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">(cela ne fera pas une demande réseau) En fait ... vous n'avez PAS besoin d'une image, référencer une image non existante et au lieu d' onloadutiliser onerror(mais cela fera une demande réseau)
Danny '365CSI' Engelman

83

Voici une méthode qui remplace récursivement 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]);

9
Je suis un peu surpris que votre réponse soit complète. À mon humble avis, c'est la meilleure solution, cette méthode vous permettrait même de restreindre les scripts avec des URL ou du contenu spécifiques.
davidmh

1
@ inf3rno fait ou ne fait pas? Cela fonctionnait, quelqu'un a-t-il déjà revendiqué quelque chose de différent?
mmm

quel est le but de [0]? pouvez-vous utiliser nodeScriptReplace (document.getElementById (). html);
Bao Thai

@BaoThai Oui. Vous pouvez.
mmm

Ne semble pas aider dans IWebBrowser2; Je peux confirmer que les balises de script sont recréées avec createElement, mais je ne parviens toujours pas à les appeler via InvokeScript ().
Dave

46

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 :)


1
Sauf s'il n'y a pas d'autres éléments de script dans le document. Utilisez document.documentElementplutôt.
Eli Gray

4
N'est pas nécessaire car vous écrivez un script à partir d'un autre 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 a dit que c'était d'un autre script? Vous pouvez exécuter JavaScript sans <script>éléments. Par exemple <img onerror="..." src="#">et <body onload="...">. Si vous voulez être technique, cela ne fonctionnera pas non plus dans les documents non HTML / SVG en raison de l'espacement des noms inexplicite.
Eli Gray

2
Facebook utilise la réponse de Pablo dans son SDK. developers.facebook.com/docs/javascript/quickstart/v2.2#loading
geoyws

30

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

1
Merci. Il a résolu mon problème d'ajout de code Disqus Universal à une fenêtre contextuelle modale créée à l'aide du plugin TinyBox2 Jquery.
gsinha

3
Malheureusement, cette solution ne fonctionne pas lorsque le script contient des fonctions qui seront appelées ultérieurement.
Jose Gómez

7

J'ai eu ce problème avec innerHTML, j'ai dû ajouter un script Hotjar à la balise "head" de mon application Reactjs et elle devrait s'exécuter juste après l'ajout.

L'une des bonnes solutions pour l'importation dynamique de nœuds dans la balise "head" est le module React-helment .


Il existe également une solution utile au problème proposé:

Pas de balises de script dans innerHTML!

Il s'avère que HTML5 n'autorise pas l'ajout dynamique de balises de script à l'aide de la propriété innerHTML. Donc, ce qui suit ne s'exécutera pas et il n'y aura pas d'alerte disant Bonjour tout le monde!

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

Ceci est documenté dans la spécification HTML5:

Remarque: les éléments de script insérés à l'aide de innerHTML ne s'exécutent pas lorsqu'ils sont insérés.

Mais attention, cela ne signifie pas que innerHTML est à l'abri des scripts intersites. Il est possible d'exécuter JavaScript via innerHTML sans utiliser de balises comme illustré sur la page innerHTML de MDN .

Solution: ajout dynamique de scripts

Pour ajouter dynamiquement une balise de script, vous devez créer un nouvel élément de script et l'ajouter à l'élément cible.

Vous pouvez le faire pour les scripts externes:

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

Et des scripts en ligne:

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

5

Pour ceux qui essaient encore de le faire, non, vous ne pouvez pas injecter un script en utilisant innerHTML, mais il est possible de charger une chaîne dans une balise de script en utilisant un BlobetURL.createObjectURL .

J'ai créé un exemple qui vous permet d'exécuter une chaîne en tant que script et d'obtenir le retour des «exportations» du script 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 à partir de mon implémentation réelle, donc aucune promesse qu'il n'y a pas de bogues. Mais le principe fonctionne.

Si vous ne vous souciez pas de récupérer une valeur après l'exécution du script, c'est encore plus facile; il suffit de laisser de côté les bits Promiseet onload. Vous n'avez même pas besoin d'envelopper le script ou de créer la window.__load_script_exports_propriété globale .


1
Je viens de l'essayer et cela fonctionne sur le chrome 57. innerHTML sur une balise de script exécute le texte.
iPherian

C'est intéressant, ça n'a pas fonctionné avant. Je me demande si ce comportement est multi-navigateur ou uniquement en chrome 57.
JayArby

4

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

// 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 .


3

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

3

Voici une solution qui n'utilise pas eval, et fonctionne avec des scripts , des scripts liés , ainsi qu'avec des 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 activer 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;
}

Je veux dire ... Ajouter une balise de script avec du contenu de code est un eval, n'est-ce pas?
Kevin B

@KevinB Il y a des différences notoires ... essayez eval('console.log(this)')et vous verrez la plus évidente
colxi

donc le contexte est différent, et? ce n'est encore qu'un eval.
Kevin B

@KevinB Non ce n'est pas un eval. Essayez ceci eval('let b=100').. puis essayez d'accéder bdepuis l'extérieur de l'évaluation ... bonne chance avec lui, vous allez en avoir besoin
colxi

Travaille pour moi. A
bientôt

1

Krasimir Tsonev a une excellente solution qui surmonte tous les problèmes. Sa méthode n'a pas besoin d'utiliser eval, donc aucun problème de performance ou de sécurité n'existe. Il vous permet de définir la chaîne innerHTML contient du html avec js et de le traduire immédiatement en un élément DOM tout en exécutant les parties js qui existent 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 la balise div.
  3. Si vous écrivez la chaîne src directement et qu'elle comprend des parties js, veuillez prendre soin d'écrire correctement les balises de script de fermeture (avec \ avant /) car il s'agit d'une chaîne.

1

Utiliser $(parent).html(code)au lieu deparent.innerHTML = code .

Ce qui suit corrige également les scripts qui utilisent document.writeet les scripts chargés via l' srcattribut. 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
    })
}

La source


1
un peu tard ici, mais quiconque utilise cette méthode, notez que dans JQuery vous devez charger vos scripts en utilisant $ .loadScript (url) plutôt que <script src = "url> </script> - ce dernier provoquera un erreur obsolète Synchronous XMLHttpRequest sur les navigateurs
Stavm

1

Essayez d'utiliser le modèle et le 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>


1
Cela ne fonctionne pas avec Microsoft Edge, une autre solution de contournement?
Soul

1

Vous pouvez également envelopper votre <script>comme ceci et il sera exécuté:

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

Veuillez noter: la portée à l'intérieur srcdocfait référence à l'iframe, vous devez donc utiliser topcomme dans l'exemple ci-dessus pour accéder au document parent.


0

Oui, vous pouvez le faire, mais vous devez le faire en dehors du DOM et la commande doit être correcte.

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

16
Pour ce que ça vaut: cela ne semble pas fonctionner sur les navigateurs actuels.
Wichert Akkerman

0

Ma solution à ce problème consiste à définir un observateur de mutation pour détecter les <script></script>nœuds, puis à le remplacer par un nouveau <script></script>nœud 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});

1
Merci de me voter sans dire pourquoi. J'vous aime tous.
gabriel garcia

0

La mention de MutationObservers par Gabriel Garcia est sur la bonne voie, mais n'a pas vraiment fonctionné pour moi. Je ne sais pas si c'était à cause d'une bizarrerie du navigateur ou à cause d'une erreur de ma part, 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.addedest utilisé pour stocker les balises de script qui sont ajoutées à document.head. Dans la fonction utilisée pour charger la page externe, vous pouvez utiliser quelque chose comme le suivant 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
}

Vous remarquez que vous ajoutez un nouveau MutationObserverchaque fois qu'un élément est ajouté au document, non? Btw, je me demande pourquoi dites-vous que mon code n'est pas fonctionnel.
gabriel garcia

@gabrielgarcia J'ai dit que votre code n'était pas fonctionnel car je l'ai essayé et cela n'a tout simplement pas fonctionné. En le regardant maintenant, il est tout à fait possible que c'était sur moi, pas sur vous, et je m'excuse sincèrement pour la façon dont j'ai formulé cela. Réparer maintenant.
pixelherodev

re: ajouter un MutationObserver chaque fois qu'un élément est ajouté au document, de quoi parlez-vous? DOMContentLoaded, et je cite MDN ici, "se déclenche lorsque le document HTML initial a été complètement chargé et analysé, sans attendre que les feuilles de style, les images et les sous-trames terminent le chargement." C'est une fois et une seule fois. De plus, ce script fonctionne sans problème sur mon site, et le débogage montre qu'il ne se produit qu'une seule fois, donc c'est une fois en pratique ainsi qu'en théorie.
pixelherodev

1
tu as raison ... Je l'ai raté. Mes excuses également.
gabriel garcia

@gabrielgarcia Pas de problème :)
pixelherodev

0

Pour moi, le meilleur moyen est d'insérer le nouveau contenu HTML via innerHtml puis d'utiliser

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

SetTimeout n'est pas requis mais il fonctionne mieux. Cela a fonctionné pour moi.


0

Je le fais chaque fois que je veux insérer dynamiquement une balise de script!

  const html =
    `<script>
        alert('👋 there ! Wanna grab a 🍺'); 
    </script>`;

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

REMARQUE: ES6 utilisé

EDIT 1: Clarification pour vous les gars - J'ai vu beaucoup de réponses utilisées appendChildet je voulais vous faire savoir que cela fonctionne exactement commeappend


-1

Exécuter (balise Java Script) de innerHTML

Remplacez votre élément de script par div ayant un attribut de classe class = "javascript" et fermez-le avec </div>

Ne modifiez pas le contenu que vous souhaitez exécuter (auparavant, il était dans la balise de script et maintenant il est dans la balise div)

Ajoutez un style à votre page ...

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

Maintenant, exécutez eval en utilisant jquery (Jquery js devrait déjà être inclus)

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

    });`

Vous pouvez en découvrir plus ici , sur mon blog.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.