Чи можна вставляти скрипти з 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.