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>
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.
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);" />
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]);
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:)
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
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_
.
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 .
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
})
}
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:
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:
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;
}
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);
}
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>
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});
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
}
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;
}