Могут ли скрипты быть вставлены с помощью innerHTML?

224

Я пытался загрузить некоторые скрипты на страницу, используя 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>

Craig
источник

Ответы:

88

Вы должны использовать eval () для выполнения любого кода скрипта, который вы вставили как текст DOM.

MooTools сделает это за вас автоматически, и я уверен, что jQuery также сделает это (в зависимости от версии. Используется версия jQuery 1.6+ eval). Это избавляет от хлопот, связанных с разбором <script>тегов и экранированием вашего контента, а также с кучей других «ошибок».

Как правило, если вы собираетесь eval()это сделать самостоятельно, вы хотите создать / отправить код сценария без какой-либо разметки HTML, например <script>, так как это будет eval()некорректно.

zombat
источник
12
Что я действительно хочу сделать, это загрузить внешний скрипт, а не просто проверить какой-нибудь локальный скрипт. Добавление тега script с помощью innerHTML намного короче, чем создание элемента DOM сценария и добавление его в тело, и я стараюсь сделать свой код максимально коротким. Нужно ли создавать элементы сценария DOM и добавлять их в DOM, а не просто использовать что-то вроде innerHTML? Есть ли способ сделать это с помощью document.write из функции?
Крейг
5
Как предлагает zombat, используйте Javascript Framework для загрузки внешнего скрипта, не пытайтесь изобретать колесо. JQuery делает это чрезвычайно просто, просто включите JQuery и вызовите: $ .getScript (url). Вы также можете предоставить функцию обратного вызова, которая будет выполняться после загрузки скрипта.
Ариэль Поповский
2
Ариэль прав. Я ценю попытку сделать ваш код коротким, и добавление <script>тега с innerHTMLможет быть коротким, но это не работает. Это все просто текст, пока он не пройдёт eval(). И, к сожалению, eval()не анализирует HTML-теги, так что вы столкнетесь с цепью проблем.
зомбат
21
eval () не является хорошим решением для любой проблемы.
Були
2
Я попробовал eval () сам. Это ужасная идея. Вы должны оценить все это КАЖДЫЙ РАЗ . Даже если вы объявляете имя и значение переменной, вы должны заново объявлять / повторно вычислять () каждый раз, чтобы она работала. Это кошмар ошибок.
Юстай Иго
88

Вот очень интересное решение вашей проблемы: http://24ways.org/2005/have-your-dom-and-script-it-too

Так что используйте это вместо тегов скрипта:

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

user447963
источник
12
Это великолепно!
конфил
У меня не работает, когда вставляется в результат запроса ajax: Синтаксическая ошибка отсутствует; оператор before в начале строки сценария
Оливер
Как вы, люди, добавляете строку & lt; img src ... в код своей страницы? Вы document.write () это или использовать document.body.innerHTML + = подход для этого? Оба терпят неудачу для меня :(
Youstay Igo
Не очень удобно писать много кода внутри onloadатрибута. Также для этого требуется дополнительный файл, чтобы существовать и быть загруженным. Решение Момо меньше компромисса.
fregante
15
Вы могли бы Base64 кодировать ваш триггер-образ как <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">(это не будет выполнять сетевой запрос) На самом деле ... вам НЕ нужно изображение, ссылаться на несуществующий образ и вместо onloadиспользования onerror(но это будет делать сетевой запрос)
Дэнни '365CSI' Энгельман
83

Вот метод, который рекурсивно заменяет все скрипты исполняемыми:

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

Пример вызова:

nodeScriptReplace(document.getElementsByTagName("body")[0]);
ммм
источник
9
Я немного удивлен, что твой ответ до конца. ИМХО, это лучшее решение, этот метод даже позволит вам ограничить скрипты конкретными URL-адресами или содержимым.
Давидмх
1
@ inf3rno делает или нет? Раньше работал, кто-нибудь когда-нибудь утверждал что-то другое?
ммм
каковы цели [0]? Вы можете использовать nodeScriptReplace (document.getElementById (). html);
Бао Тай
@BaoThai Да. Ты можешь.
ммм
Кажется, не помогает в IWebBrowser2; Я могу подтвердить, что теги сценария воссоздаются с помощью createElement, но я все еще не могу вызвать их через InvokeScript ().
Дейв
46

Вы можете создать скрипт, а затем внедрить содержимое.

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

Это работает во всех браузерах :)

Пабло Моретти
источник
1
Если в документе нет других элементов скрипта. Используйте document.documentElementвместо этого.
Эли Грей
4
Нет необходимости, потому что вы пишете сценарий из другого сценария. <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>
Пабло Моретти
3
Кто сказал, что это из другого сценария? Вы можете запустить JavaScript без <script>элементов. Например <img onerror="..." src="#">и <body onload="...">. Если вы хотите быть техническим, это не будет работать в документах не HTML / SVG из-за неявного пространства имен.
Эли Грей
2
Facebook использует ответ Пабло в своем SDK. developers.facebook.com/docs/javascript/quickstart/v2.2#loading
geoyws
30

Я использовал этот код, он работает нормально

var arr = MyDiv.getElementsByTagName('script')
for (var n = 0; n < arr.length; n++)
    eval(arr[n].innerHTML)//run script inside div
Firas Nizam
источник
1
Спасибо. Это исправило мою проблему добавления кода Disqus Universal в модальное всплывающее окно, созданное с помощью плагина TinyBox2 Jquery.
Гсинха
3
К сожалению, это решение не работает, когда скрипт содержит функции, которые будут вызваны позже.
Хосе Гомес
7

У меня была эта проблема с innerHTML, мне пришлось добавить скрипт Hotjar в тег «head» моего приложения Reactjs, и он должен был выполняться сразу после добавления.

Одним из хороших решений для динамического импорта узлов в тег head является модуль React-helment .


Также есть полезное решение для предложенного вопроса:

Нет тегов сценария в innerHTML!

Оказывается, HTML5 не позволяет динамически добавлять теги сценария с помощью свойства innerHTML. Таким образом, следующее не будет выполнено, и не будет никакого предупреждения, говорящего Привет Мир!

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

Это задокументировано в спецификации HTML5:

Примечание. Элементы сценария, вставленные с использованием innerHTML, не выполняются при вставке.

Но будьте осторожны, это не означает, что innerHTML безопасен от межсайтовых скриптов. Возможно выполнение JavaScript через innerHTML без использования тегов, как показано на странице innerHTML MDN .

Решение: динамическое добавление скриптов

Чтобы динамически добавить тег сценария, вам нужно создать новый элемент сценария и добавить его к целевому элементу.

Вы можете сделать это для внешних скриптов:

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);
p.daniel
источник
5

Для тех, кто все еще пытается это сделать, нет, вы не можете внедрить скрипт, используя innerHTML, но есть возможность загрузить строку в тег скрипта, используя Blobи URL.createObjectURL.

Я создал пример, который позволяет вам запускать строку в качестве сценария и получать «экспорт» сценария, возвращаемый через обещание:

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

Я упростил это из моей реальной реализации, так что никаких обещаний, что в нем нет ошибок. Но принцип работает.

Если вам не нужно возвращать какое-либо значение после запуска скрипта, это еще проще; просто оставь Promiseи onloadбиты. Вам даже не нужно оборачивать скрипт или создавать глобальное window.__load_script_exports_свойство.

JayArby
источник
1
Я только что попробовал, и он работает на Chrome 57. innerHTML на тег скрипта выполняет текст.
iPherian
Это интересно, это не сработало раньше. Интересно, является ли это поведение кросс-браузерным или только в Chrome 57.
JayArby
4

Вот рекурсивная функция для установки innerHTML элемента, который я использую на нашем рекламном сервере:

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

Вы можете увидеть демо здесь .

Аднан Коркмаз
источник
3

Вы можете сделать это так:

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

Здесь решение, которое не использует 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;
}
colxi
источник
Я имею в виду ... Добавление тега сценария с содержимым кода - это оценка, не так ли?
Кевин Б.
@KevinB Есть пресловутые различия ... попробуйте, eval('console.log(this)')и вы увидите самый очевидный из них
Colxi
так контекст другой, а? это все еще просто eval.
Кевин Б.
@KevinB Нет, это не оценка. Попробуйте это eval('let b=100')... и затем попытайтесь получить доступ bизвне eval .... удачи с ним, он вам понадобится
colxi
Работает для меня. Ура
Безззо
1

Красимир Цонев имеет отличное решение, которое преодолевает все проблемы. Его метод не требует использования eval, поэтому проблем с производительностью и безопасностью не существует. Это позволяет вам установить строку innerHTML, содержащую html с js, и немедленно преобразовать ее в элемент DOM, а также выполнить части js, существующие в коде. Короче говоря, просто и работает именно так, как вы хотите.

Наслаждайтесь его решением:

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

Важные заметки:

  1. Вам нужно обернуть целевой элемент тегом div
  2. Вам нужно обернуть строку src тегом div.
  3. Если вы пишете строку src напрямую и она содержит части js, обратите внимание на правильную запись закрывающих тегов сценария (с \ before /), так как это строка.
user3198805
источник
1

Используйте $(parent).html(code)вместо parent.innerHTML = code.

Следующее также исправляет сценарии, которые используют, document.writeи сценарии, загружаемые через srcатрибут. К сожалению, даже это не работает со скриптами 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
    })
}

Источник

iirekm
источник
1
немного поздно здесь, но любой, кто может использовать этот метод, заметит, что в JQuery вам нужно загружать свои скрипты, используя $ .loadScript (url), а не <script src = "url> </ script> - последний вызовет устарела синхронная ошибка XMLHttpRequest в браузерах
Став
1

Попробуйте использовать template и document.importNode. Вот пример:

<!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
Это не работает с Microsoft Edge, любой другой обходной путь?
Душа
1

Вы также можете обернуть <script>это так, и оно будет выполнено:

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

Обратите внимание: область видимости внутри srcdocотносится к iframe, поэтому вы должны использовать topкак в примере выше, чтобы получить доступ к родительскому документу.

Naden
источник
1

Я делаю это каждый раз, когда хочу динамически вставить тег сценария!

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

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

ПРИМЕЧАНИЕ: ES6 используется

РЕДАКТИРОВАТЬ 1: Разъяснение для вас, ребята - я видел много ответов использовать appendChildи хотел, чтобы вы, ребята, знали, что это работает точно так же, какappend

Тилак Мэдди
источник
0

Да, вы можете, но вы должны сделать это вне DOM, и порядок должен быть правильным.

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

... предупредит 'Foo'. Это не сработает:

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

И даже это не сработает, потому что узел вставляется первым:

var scr = '<scr'+'ipt>alert("foo")</scr'+'ipt>';
window.onload = function(){
    var n = document.createElement("div");
    document.body.appendChild(n);
    n.innerHTML = scr;  
}
mwilcox
источник
16
Для чего это стоит: это не похоже на работу в современных браузерах.
Wichert Akkerman
0

Мое решение этой проблемы - установить Mutation Observer для обнаружения <script></script>узлов, а затем заменить его новым <script></script>узлом с тем же src. Например:

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
Спасибо, что отказался от меня, не сказав почему. Всех люблю.
Габриэль Гарсиа
0

Упоминание Габриэля Гарсии о MutationObservers находится на правильном пути, но у меня не получилось. Я не уверен, было ли это из-за причуды браузера или из-за ошибки на моем конце, но версия, которая в итоге работала для меня, была следующей:

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

Конечно, вы должны заменить element_to_watchимя изменяемого элемента.

node.parentElement.addedиспользуется для хранения тегов сценария, которые добавляются в document.head. В функции, используемой для загрузки внешней страницы, вы можете использовать что-то вроде следующего, чтобы удалить больше не релевантные теги скрипта:

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

И пример начала функции загрузки:

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
источник
Вы замечаете, что вы добавляете новый MutationObserverкаждый раз, когда элемент добавляется в документ, верно? Кстати, мне интересно, почему вы говорите, мой код не работает.
Габриэль Гарсия
@ gabrielgarcia Я сказал, что твой код не работает, потому что я попробовал его, и он просто не работал. Глядя на это сейчас, вполне возможно, что это было на мне, а не на вас, и я искренне извиняюсь за то, как я это сформулировал. Исправляем это сейчас.
pixelherodev
Re: добавление MutationObserver каждый раз, когда элемент добавляется в документ, о чем вы говорите? DOMContentLoaded, и я цитирую здесь MDN, «срабатывает, когда исходный HTML-документ полностью загружен и проанализирован, не дожидаясь окончания загрузки таблиц стилей, изображений и подкадров». Это один раз и только один раз. Кроме того, этот сценарий работает без проблем на моем сайте, и отладка показывает, что это происходит только один раз, поэтому он применяется как на практике, так и в теории.
pixelherodev
1
Вы правы ... Я это не понял. Мои извинения также.
Габриэль Гарсия
@gabrielgarcia Не проблема :)
pixelherodev
0

Для меня лучший способ - вставить новый HTML-контент через innerHtml, а затем использовать

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

SetTimeout не требуется, но работает лучше. Это сработало для меня.

Луис Тьяго Флорес Кристован
источник
-1

Выполнить (Java Script) тег из innerHTML

Замените ваш элемент скрипта на div с атрибутом class class = "javascript" и закройте его </div>

Не изменяйте содержимое, которое вы хотите выполнить (раньше это было в теге script, а теперь в теге div)

Добавить стиль на своей странице ...

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

Теперь запустите eval, используя jquery (Jquery js должен быть уже включен)

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

    });`

Вы можете узнать больше здесь , в моем блоге.

Служба знаний
источник