Запуск скрипта в контексте пользовательского элемента HTML

Я пишу некоторые пользовательские элементы HTML5, которые содержат код, который транспилируется, чтобы он мог фактически работать на странице (графический код, латекс и т. д.), но я не могу найти какую-либо страницу или вопрос Stackoverlow, который объясняет, и как загрузить скрипт так, чтобы он ограничивался самим настраиваемым элементом.

Вместо этого все, что я могу найти, это полное отсутствие информации, поэтому я использую довольно глупую вставку ее в заголовок документа, переписанную так, чтобы она сначала брала правильный элемент из глобального реестра, что очень грязно. I работает, но было бы намного лучше иметь возможность внедрять сценарии в теневой DOM таким образом, чтобы они выполнялись, точно зная, для какого элемента они выполняются.

Прямо сейчас код (в сокращенном виде) выглядит так:

import { uuid } from "./uuid.js";
import { Parser } from "./code-parser.js";

class MyElement extends HTMLElement {
  constructor() {
    super();

    this.uid = uuid.v4();
    window[this.uid] = this;

    const jsCode = `
      import { Base, API, ... } from "./lib.js";
      class Example extends Base {
         constructor() {
           super(window[${this.uid}]);
           delete window[${this.uid}];
         }
         ${Parser.rewrite(this.textContent)}
      }
      new Example();
    `;

    const script = document.createElement(`script`);
    script.type = `module`;
    script.src = URL.createObjectURL(
      new Blob([jsCode], {type: `text/javascript`})
    );

    this.attachShadow({ mode: 'open' }).append(script);
  }
}

customElements.define(`my-element`, MyElement);
export { MyElement }

Это работает, конечно, но работает (временно) загрязняя window. Есть ли способ прикрепить элемент сценария к пользовательскому элементу или теневой DOM пользовательского элемента, чтобы он выполнялся со знанием того, для какого пользовательского элемента или какой теневой DOM он выполняется?

Изменить: чтобы быть явным, это должно работать для сценариев, использующих современный код, основанный на модули: любой оператор import все еще нуждается в разрешении. Также обратите внимание, что все, что основано на подходах, требующихunsafe-eval (6.1 .11.3) или unsafe-inline для добавления в CSP script-src использовать нельзя.


person Mike 'Pomax' Kamermans    schedule 22.07.2020    source источник
comment
Как вы думаете, почему вам нужно добавить новый элемент ‹script›? Если только для операторов import, вы можете очень хорошо преобразовать их в динамические вызовы import() и выполнить все из конструктора, очень нормально и очень чисто.   -  person Kaiido    schedule 01.08.2020
comment
потому что код в самом скрипте нигде не существует. Он генерируется динамически. Код существует в виде строковых данных, переписывается на правильный JS и затем внедряется. Нет полностью сформированного модуля для импорта.   -  person Mike 'Pomax' Kamermans    schedule 02.08.2020
comment
(Я работаю над этим над pomax.github.io/custom-graphics-element, если вы хотите понять всю цепочку событий, которые не имеют отношения к этому вопросу, но могут иметь отношение к вашим интересам)   -  person Mike 'Pomax' Kamermans    schedule 02.08.2020
comment
Извините, у меня нет времени, чтобы полностью проверить ваш репозиторий, но быстро прочитайте его, вам не нужно, чтобы эти скрипты выполнялись в элементах ‹script›. Вы можете просто сгенерировать свои динамические скрипты с помощью конструктора new Function(). Что-то вроде const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; new AsyncFunction(`const API = await import("./api.js"); const Base= await import("./base.js"); ...   -  person Kaiido    schedule 02.08.2020
comment
К сожалению, я не могу: разумный CSP (например, без unsafe-eval) запрещает eval(), new Function() и малоизвестные формы исполнения строк setTimeout и setInterval. Этот элемент предназначен для работы на произвольных сайтах, поэтому он должен по-прежнему работать на сайтах с обычным CSP. Необходимость unsafe-inline прямо сейчас уже довольно сомнительна, и мне, вероятно, придется вместо этого переключиться на инъекцию src="blob:...". Независимо от этого, первоначальный вопрос остается в силе: как скрипт, внедренный в теневой DOM, может получить ссылку на этот теневой DOM или на владельца теневого DOM?   -  person Mike 'Pomax' Kamermans    schedule 02.08.2020
comment
Однако разумное предложение в отсутствие подробностей, поэтому я обновил сообщение, чтобы прояснить, что unsafe-inline и unsafe-eval не обсуждаются, а код обновлен для использования вместо этого внедрения больших двоичных объектов.   -  person Mike 'Pomax' Kamermans    schedule 02.08.2020
comment
Таким образом, вы на самом деле пытаетесь оценить содержимое, не являющееся сценарием, как сценарии в заблокированном контексте CSP. Даже blob:// URI будут заблокированы правилом script-url: 'self'. Я думаю, вам нужно немного реорганизовать свой проект. На ум приходят несколько решений (все менее привлекательных, чем другие): 1) Определите свой собственный язык, разберите его из содержимого «кода программы» и выполните его на своем собственном движке. 2) Создайте нединамический инструмент предварительной сборки, который ваши пользователи должны будут вызывать для создания встроенного файла. 3) заставить использовать unsafe-eval. (Я думаю, что 1 и 2 на самом деле то, что делают подобные проекты, такие как реакция).   -  person Kaiido    schedule 04.08.2020
comment
Да, но большинство CSP — это не просто self, это также белые списки протоколов и доменов, а blob: как часть CSP не так уж и редкость, тогда как никто в здравом уме не добавит unsafe-eval в свой список CSP. Что касается предложения 3, очевидно, что оно не выполняется, и хотя 1 и 2 возможны, оба они имеют недостатки, которые гораздо серьезнее, чем работающее в настоящее время решение, основанное на временном глобальном загрязнении. Похоже, вы уверены, что скрипт никогда не сможет узнать, в каком теневом DOM он находится: с мотивацией, основанной на спецификациях, это было бы хорошим ответом.   -  person Mike 'Pomax' Kamermans    schedule 04.08.2020
comment
Почему кто-то в здравом уме разрешил blob:// для скриптов, но не unsafe-eval? Оба ведут к одинаковым рискам...   -  person Kaiido    schedule 05.08.2020
comment
тогда у нас разные представления о том, как на самом деле используется CSP, и мы также сильно срываем эту ветку комментариев: предполагают ли различные спецификации теневого DOM и пользовательских элементов, как сценарии type="module", вставленные в теневой DOM, могут найти свой теневой корень ? Потому что я нигде не могу найти ничего, что объясняло бы, как это могло произойти. Если вы знаете (или если вы знаете, что это явно невозможно), то ответ, который я могу пометить как принятый, будет оценен. Альтернативные подходы не обсуждаются: этот вопрос явно и только касается скрипта, внедренного в теневой DOM.   -  person Mike 'Pomax' Kamermans    schedule 05.08.2020
comment
Хотел бы добавить любой из моих баллов к награде. Но я не думаю, что есть ответ, потому что (в отличие от использования IFRAME или Workers) фундаментальная проблема ? заключается в следующем: <SCRIPT> в shadowDOW не ограничен до shadowDOM. Он выполняется в глобальной области видимости.   -  person Danny '365CSI' Engelman    schedule 08.08.2020
comment
Правильно: это был бы буквально тот ответ, который я хочу видеть, чтобы я мог принять его, если это может быть подкреплено ссылками на (различные) спецификации, которые поясняют, что это предполагаемое поведение. И тогда ваш способ обойти этот ответ - отличный не ответ, а суперполезный информационный пост в дополнение к тому, что можно принять, чтобы будущие посетители с той же проблемой знали, каково положение дел.   -  person Mike 'Pomax' Kamermans    schedule 08.08.2020
comment
Я так и не нашел ничего связанного, кроме Роба Додсона в его блоге, в котором была одна строка: сценарий не имеет области действия и Supersharp, его ответы здесь, на SO (говорят, что сценарий не ограничен областью). Может быть, сделать это проблемой на github.com/w3c/webcomponents/issues.   -  person Danny '365CSI' Engelman    schedule 08.08.2020
comment
Я должен. Первоначально я подал для этого github.com/whatwg/html/issues/5754, но трудно понять, где лучше всего регистрировать проблемы с вещами, которые, кажется, подходят как минимум к четырем разным репозиториям в двух разных организациях.   -  person Mike 'Pomax' Kamermans    schedule 08.08.2020


Ответы (1)


Обновление №2

Приведенный ниже обходной путь не будет работать, если используется CSP
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP



Вы можете внедрить элемент SCRIPT, но, конечно, это не так просто.

Проблема в том, что SCRIPT получает window область, а не область пользовательского элемента.

Отказ от ответственности:
Приведенный ниже код работает на Chredge и FireFox.
Но мое внутреннее чувство говорит, что есть потенциальная проблема, я просто не могу ее понять< /эм>

Чтобы выбрать правильную область, вы должны добавить TAG, который знает свое местоположение в shadowDOM и может получить пользовательский элемент с помощью .getRootNode().host

<style onload=console.log(this)> является кандидатом, но по какой-то причине он будет выполняться только для одного элемента на странице.

Поэтому я перешел на <img src onerror=console.log(this)>

Фрагмент SO ниже не может правильно обработать этот код.
Вот JSFiddle: https://jsfiddle.net/CustomElementsExamples/g134yp7v/

<template id=scriptContainer>

  <!--start-->
    <script>
      console.log("script",this); // window
      function run(scope) {
        const element = scope.getRootNode().host;
        element.ran();
      }
    </script>
    <img src onerror="run(this)">
  <!--end-->

</template>
<script>
  customElements.define('my-element',
    class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'})
            .append(scriptContainer.content.cloneNode(true));
      }
      ran() {
        const pre = document.createElement("PRE");
        pre.innerHTML = this.shadowRoot.innerHTML.replace(/</g,"&lt;");
        this.shadowRoot.append("shadowRoot content: " , this.id , pre );
      }
    });
</script>
<my-element id=ONE></my-element>
<my-element id=TWO></my-element>

Обновление №1

Да, я знал, что что-то не так

   function run(scope) {
     const element = scope.getRootNode().host;
     element.ran();
   }

Становится window. глобальной функцией, перезаписываемой каждым новым my-element на странице.

Код по-прежнему работает, просто не храните в функции какие-либо локальные элементы (элементы).
И, возможно, дайте ей очень непонятное имя функции.

Выполнить добавленный СКРИПТ с правильной областью действия пользовательского элемента

Чтобы не создавать глобальные переменные, заполните весь код как IIFE в IMG onerror (сценарий INLine!!),
стрелочная функция обеспечивает правильную область видимости:

  <img src onerror="(()=>{
    this.onerror = null;// prevent endless loop if function generates an error
    const element = this.getRootNode().host;
    console.log('IIFE',element);
  })()">

ИЛИ в качестве метода пользовательского элемента:

  runScript(script) {
    const span = document.createElement("SPAN");
    const onerror = `this.onerror=null;const element=this.getRootNode().host;` + script;
    span.innerHTML = `<img src onerror="${onerror}">`;
    this.shadowRoot.append(span);
    setTimeout(()=>span.remove());
  }

звонок: this.runScript(`console.log(666,element.id)`);

Игровая площадка: https://jsfiddle.net/CustomElementsExamples/g134yp7v/

результаты:

IMG IIFE работает в правильной области

Последние 2 строки консоли взяты из:

TWO.runScript(`console.log(${this.id}.id,'runs in',element.id)`);//duplicate line in console!
TWO.runScript(`console.log(${this.id}.id,'runs in',element.id)`);//duplicate line in console!

демонстрация запуска скрипта в правильной области при вызове этого метода runScript для пользовательского элемента

person Danny '365CSI' Engelman    schedule 22.07.2020
comment
Хм, если заставить скрипт вообще выполнять то, что вставлено в теневой дом, это может открыть способ, возможно, получить правильную область, позвольте мне посмотреть, смогу ли я немного поиграть с вашим первым кодом, чтобы заставить это работать тоже . - person Mike 'Pomax' Kamermans; 22.07.2020
comment
поигрался с этим еще немного, но проблема на самом деле не в том, чтобы запустить скрипт вообще, а в том, как, черт возьми, он находит своего владельца, не проходя window =( - person Mike 'Pomax' Kamermans; 26.07.2020
comment
Вот почему IMG необходим, он вызовет событие onerror (или onload) с правильной областью действия this (IMG). Отсюда вы можете сделать this.getRootNode().host Воспроизвести JSFiddle, с которым я связан. Да, это обходной путь с большой буквы W. Но он работает - person Danny '365CSI' Engelman; 27.07.2020
comment
Хм, похоже, что на самом деле это не так: если я запускаю немодифицированный jsbin, я вижу журналы консоли, в которых говорится, что сценарий для ОДНОГО выполняется в ДВА (два журнала), и что сценарий для ДВУХ выполняется в ДВА (тоже два журнала). ), поэтому похоже, что это не работает надежно, когда на странице существует более одного экземпляра пользовательского элемента. - person Mike 'Pomax' Kamermans; 27.07.2020
comment
Я добавил скриншот выше; Важной частью являются журналы IIFE с правильной областью действия. Эти TWO.runscript строки являются бонусом; они создают 4 строки журнала, все 4 выполняются в области ДВА элемента, демонстрируя, что скрипт выполняется в правильной области, когда вы вызываете element.runScript( ... ) Этот метод ``runScript не требуется, я добавил это, чтобы показать расширяемость. Я знаю, что этот обходной путь IMG/scope работает; использовали его в течение многих лет в мире MS-SharePoint, где у нас были ограниченные возможности внедрения скриптов - person Danny '365CSI' Engelman; 27.07.2020
comment
Исправление: с SharePoint 2001 по 2016 вы могли не внедрять JavaScript во все столбцы данных IN... Но вы могли добавлять HTML... и Microsoft не выполняла никаких XSS-проверок... поэтому IMG.onload или IMG .onerror был способом «попасть». В более поздних версиях Microsoft полностью переключилась на React. - person Danny '365CSI' Engelman; 27.07.2020
comment
Верно, но в этом и проблема: старые технологии работали со старыми трюками для старых спецификаций, но мне нужно что-то, что опирается на новые (2 года назад, но новые по сравнению с ними) функции: сценарии, которые мне нужно запустить, используют операторы import , и поэтому не может быть встроен через атрибут on..., код в обработчике on... каким-то образом должен получить область действия элемента, а затем добавить эту область в код, загруженный через элемент скрипта с type="module" (если вы не знаете подлый встроенный модуль код в любом случае, конечно. Это было бы здорово) - person Mike 'Pomax' Kamermans; 27.07.2020
comment
Итак, у вас две проблемы: (1). Как получить область действия (ответ: сделано с помощью обходного пути IMG) и (2). Как передать область видимости в функцию модуля. --- Последнее будет работать только в том случае, если в модуле есть запись function( ), которую можно определить с помощью .bind или вызвать с помощью .call или .apply Но это означает рефакторинг скриптов по олдскульным шаблонам. - person Danny '365CSI' Engelman; 28.07.2020
comment
К сожалению, оказывается, что все становится еще интереснее, чем предполагалось изначально, потому что нам также нужно учитывать еще одну особенность современной сети: CSP. Таким образом, проблема (3) заключается в том, что решения здесь не могут нарушать политику CSP в отношении script-src, что означает отсутствие unsafe-inline и отсутствие unsafe-eval, что полностью исключает этот метод, даже если он работает на неограниченной странице =( - person Mike 'Pomax' Kamermans; 02.08.2020
comment
смешнее? Меньше веселья. CSP - это презерватив. - person Danny '365CSI' Engelman; 02.08.2020
comment
К сожалению, в уценке нет синтаксиса сарказма. И еще: да, это именно то, что есть. Разумный CSP примерно так же эффективен, как и правильно надетый презерватив: и то, и другое предотвратит легко предотвратимые проблемы в подавляющем большинстве случаев. Так что, как бы они ни раздражали, все же лучше их использовать, чем не использовать;) - person Mike 'Pomax' Kamermans; 03.08.2020