Чи можна вставляти скрипти з InternalHTML?


223

Я спробував завантажити деякі сценарії на сторінку за 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>

Відповіді:


88

Ви повинні використовувати eval () для виконання будь-якого коду сценарію, який ви вставили як текст DOM.

MooTools зробить це автоматично для вас, і я впевнений, що так само буде jQuery (залежно від версії. Використовується версія jQuery 1.6+ eval). Це економить багато клопоту з розбору <script>тегів та виходу з вмісту, а також з купою інших "готчей".

Як правило, якщо ви збираєтеся до eval()нього самостійно, ви хочете створити / надіслати код сценарію без будь-якої розмітки HTML, наприклад <script>, оскільки це не буде eval()належним чином.


12
Що я дійсно хочу зробити, це завантажити зовнішній скрипт, а не просто оцінювати якийсь локальний сценарій. Додавання тегу скрипту до innerHTML набагато коротше, ніж створення елемента DOM скрипта та додавання його до тіла, і я намагаюся зробити свій код якомога коротшим. Чи потрібно створювати елементи сценарію dom і додавати їх до dom, а не просто використовувати щось на зразок innerHTML? Чи є спосіб це зробити з document.write з функції?
— Крейг

5
Як пропонує Zombat, використовуйте рамку Javascript для завантаження зовнішнього скрипту, не намагайтеся винаходити колесо. JQuery робить це надзвичайно просто, просто включіть JQuery і виклик: $ .getScript (url). Ви також можете надати функцію зворотного дзвінка, яка буде виконуватися після завантаження сценарію.
— Аріель Поповський

2
Аріель має рацію. Я вдячний намагатися тримати ваш код коротким, і додавання <script>тегу з innerHTMLможе бути коротким, але це не працює. Це все просто звичайний текст, поки його не запустять eval(). І, на жаль, eval()HTML-теги не розбирають, тож у вас виникає ланцюжок проблем.
— zombat

21
eval () - не чудове рішення будь-якої проблеми.
— buley

2
Я сам спробував eval (). Це жахлива ідея. Ви повинні піднести все це ВСІЙ ЧАС . Навіть якщо ви оголошуєте ім'я та значення змінної, вам доведеться повторно оголошувати / повторно оцінювати () це кожного разу заново, щоб змусити її працювати. Це кошмар помилок.
— Youstay Igo

88

Ось дуже цікаве рішення вашої проблеми: http://24ways.org/2005/have-your-dom-and-script-it-too

Тому використовуйте це замість тегів сценарію:

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


12
Це геніально!
— confile

Не працює для мене, якщо вставлено в результат запиту ajax: помилка синтаксису відсутня; перед заявою на початку рядка сценарію
— Олівер

Як ви додаєте рядок & lt; img src ... до коду сторінки? Ви документуєте його.write () або використовуєте document.body.innerHTML + = підхід для цього? Обидва мені не вдається :(
— Youstay Igo

Не дуже практично писати багато коду всередині onloadатрибута. Також для цього потрібен додатковий файл для існування та завантаження. Рішення momo менше компромісу.
— фреганте

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
Я трохи здивований, що у вашій відповіді це аж донизу. IMHO, це найкраще рішення, цей метод навіть дозволить вам обмежувати сценарії певними URL-адресами або вмістом.
— davidmh

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

1
Дякую. Це вирішило мою проблему додавання коду Disqus Universal до модального спливаючого вікна, створеного за допомогою плагіна TinyBox2 Jquery.
— gsinha

3
На жаль, це рішення не працює, коли скрипт містить функції, які будуть викликані згодом.
— Хосе Гомес

7

У мене була проблема з InternalHTML, мені довелося додати скрипт Hotjar до тегу "head" мого додатку Reactjs, і він повинен був би виконатись відразу після додавання.

Одне з хороших рішень для динамічного імпорту Вузла в тег "head" - це модуль React-helment .


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

У InternalHTML немає тегів сценаріїв!

Виявляється, HTML5 не дозволяє теги сценарію динамічно додаватись за допомогою властивості innerHTML. Отже, наступне не буде виконано, і не буде оповіщення про привіт World!

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

Це задокументовано у специфікації HTML5:

Примітка: елементи сценарію, вставлені за допомогою InternalHTML, не виконуються, коли вони вставлені.

Але будьте обережні, це не означає, що InternalHTML є безпечним від перехресних сценаріїв. Можна виконувати JavaScript через innerHTML без використання тегів, як показано на внутрішній сторінці MDHTML 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);

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_властивість.


1
Я щойно спробував це, і він працює на chrome 57. innerHTML на скрипті тегу виконує текст.
— iPherian

Це цікаво, раніше не працювало. Цікаво, чи така поведінка крос-браузер чи лише у хромі 57.
— JayArby

4

Ось рекурсивна функція встановлення внутрішньогоHTML елемента, який я використовую на нашому рекламному сервері:

// 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 : прапорець boolean для включення додавання в кінці цільового елемента 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;
}

Я маю на увазі ... Додавання тегу сценарію до вмісту коду - це eval, чи не так?
— Кевін Б

@KevinB Є сумнозвісні відмінності ... спробуйте, eval('console.log(this)')і ви побачите найочевидніший
— colxi

тому контекст інший, а? це все ще просто евал.
— Кевін Б

@KevinB Ні, це не евал. Спробуйте це eval('let b=100').. а потім спробуйте отримати доступ bіз-за меж евалу .... удачі з цим, вам це знадобиться
— 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 частини, будь ласка, зверніть увагу, щоб правильно писати теги сценарію закриття (з \ до /), оскільки це рядок.

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

Джерело


1
тут трохи пізно, але кожен, хто, можливо, використовує цей метод, зауважує, що в JQuery вам потрібно завантажувати свої сценарії за допомогою $ .loadScript (url), а не <script src = "url> </script> - останній призведе до застаріла помилка синхронного XMLHttpRequest у браузерах
— Ставм

1

Спробуйте використовувати шаблон і 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як у наведеному вище прикладі, для доступу до батьківського документа.


1

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

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

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

ПРИМІТКА: ES6 використовується

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

16
Для чого це варто: це, здається, не працює в поточних браузерах.
— Вічер Акеркерман

0

Моє рішення цієї проблеми - встановити спостережувача мутації для виявлення <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
Дякуємо, що відмовились від мене, не кажучи чому. Любіть усіх.
— gabriel garcia

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
}

Ви помічаєте, що u додаєте новий MutationObserverщоразу, коли елемент додається до документа, правда? До речі, мені цікаво, чому ти кажеш, що мій код не функціонує.
— gabriel garcia

@gabrielgarcia Я сказав, що ваш код не функціонує, тому що я спробував його, і він просто не спрацював. Дивлячись на це зараз, цілком можливо, що було саме на мене, а не на вас, і я щиро вибачаюся за те, як я це висловив. Виправлення зараз.
— pixelherodev

re: додавання MutationObserver кожного разу, коли елемент додається до документа, про що ви говорите? DOMContentLoaded, і я цитую тут з MDN, "запускається, коли початковий документ HTML повністю завантажений та проаналізований, не чекаючи, коли таблиці стилів, зображення та підрамники закінчуються завантаженням". Це раз, і лише раз. Крім того, цей сценарій працює без проблем на моєму сайті, і налагодження показує, що це відбувається лише один раз, так що це один раз на практиці, а також теоретично.
— піксельгеродев

1
ти маєш рацію ... Я його помиляв. Мої вибачення також.
— gabriel garcia

@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) тег із InternalHTML

Замініть елемент сценарію на div, який має атрибут class class = "javascript" і закрийте його </div>

Не змінюйте вміст, який ви хочете виконати (раніше він був у тезі сценарію, а зараз він знаходиться у тезі div)

Додайте стиль на свою сторінку ...

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

Тепер запустіть eval за допомогою jquery (Jquery js повинен бути вже включений)

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

    });`

Ви можете дізнатися більше тут , у моєму блозі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.