Русский
Русский
English
Статистика
Реклама

Dom api

Про Shadow DOM

06.09.2020 20:21:10 | Автор: admin

Всем привет!

Продолжаю свой цикл публикаций о группе стандартов Web Components. Моя цель - сформировать реалистичные ожидания от данного набора технологий, а также, вместе с вами, прийти к более четкому пониманию того, где их не стоит применять, и где, напротив, ничего лучше еще не придумано. На этот раз, предлагаю подробнее остановится на Shadow DOM.

Начнем с азов, чтобы те из нас, кто пока не сталкивался с предметом обсуждения, не потеряли интерес к основной части статьи. Итак, Shadow DOM - это часть современного DOM API, которая позволяет создавать изолированные участки документа, со своей собственной внутренней разметкой и своими собственными стилями. Если вы откроете, с помощью инструментов разработчика в браузере, структуру документа, содержащего, к примеру, стандартный HTML-элемент audio - вы увидите в его составе область, обозначенную как #shadow-root и какую-то разметку внутри. Это и есть Shadow DOM элемента audio. Как видите, мы можем столкнуться с "теневыми" участками документов в приложениях и на страницах сайтов, даже если при их создании никак не использовались веб-компоненты или основанные на них библиотеки. Это утверждение справедливо для всех стандартных браузерных UI-примитивов, таких как кнопки, селекты, инпуты и т. д. Хорошая новость в том, что теперь у нас есть возможность создавать собственные универсальные элементы, подобные встроенным браузерным. И Shadow DOM, в данном случае, это ответ на вопрос "как?".

Какие основные вопросы решает Shadow DOM?

  1. Инкапсуляция. Внутри Shadow DOM создается отдельный "поддокумент", к которому можно применять свои стили, экранированные от воздействий внешней среды (вам не нужно писать многоэтажные имена классов, чтобы обезопасить ваш элемент или внешний документ от "протечек") и где создается свой контекст для методов DOM API, где, к примеру, с помощью селекторов можно получить только те элементы которые находятся внутри и остаются почти невидимыми снаружи (при этом, допустимо использование одинаковых ID у элементов в разных контекстах, без опасности все поломать).

  2. Композиция. Shadow DOM дает вам контроль над "размещением" непосредственных потомков сложного DOM-элемента в нужных местах его внутренней разметки. В этом случае, Shadow DOM выступает в роли некоего шаблона, в котором можно размещать содержимое в местах, обозначенных специальным тегом - slot. Это дает возможность, к примеру, создавать элементы-лейауты и повторно использовать их.

У тех, кто уже имел дело с библиотеками, основанными на веб-компонентах (такими как LitElement), могло сформироваться ложное впечатление о том, что Shadow DOM - это неотъемлемая часть любого веб-компонента. Это не так. Вы можете создавать компоненты с помощью стандарта Custom Elements и НЕ использовать Shadow DOM при этом, и напротив, создавать теневой DOM у обычных элементов, таких как старый добрый div. Теневой DOM открывает довольно интересные и нетривиальные возможности, например, когда вы динамически добавляете CSS-свойства к элементу через интерфейс "element.style", у вас нет возможности определить псевдоклассы и псеводоэлементы, а также, использовать media-запросы или создавать ключи анимации. Это большой недостаток модели работы со стилями через JavaScript в современных браузерах (работа в этом направлении ведется, но это отдельная сложная тема). Все меняет Shadow DOM:

let myElement = document.createElement('div');myElement.attachShadow({  mode: 'open',});myElement.shadowRoot.innerHTML = /*html*/ `<style>:host {  padding: 10px;}:host(:hover) {  color: red;}</style><slot></slot>`;

Теперь у нашего div есть реакция на наведение мыши при том, что мы не создавали для этого никаких классов и не вносили никаких изменений во внешние стили. Shadow DOM дает нам доступ к своему элементу-контейнеру через селектор :host, и используя этот селектор, мы можем создавать любые сложные стили для элемента в JS. Прошу принять во внимание, что код приведенный выше, написан исключительно для демонстрации самого принципа, в бою все может выглядеть немного иначе.

Когда стоит применять Shadow DOM?

Думаю, основными областями применения можно считать те-же, что и для веб-компонентов в целом: для создания встраиваемых виджетов и UI-библиотек-агностиков, минимально зависимых от конкретных мета-платформ, экосистем и фреймворков. Везде, где важна инкапсуляция, возможность спрятать "под ковер" реализацию и удобная независимая композиция.

Модные микрофронтенды также являются интересной областью для применения возможностей Shadow DOM.

Также, можно рассмотреть вариант применения в случаях, когда вам необходимо внедрить свое локальное решение в большой и неповоротливый проект с кучей легаси-кода, в котором можно закопаться неоправданно надолго и есть опасность сломать что-то неочевидное: изоляция участка может оказаться быстрым и эффективным решением.

Вялотекущий рефакторинг сложной системы тоже можно проводить через создание "островков безопасности".

Какие могут возникнуть сложности?

Следует понимать, что Shadow DOM в отдельности, НЕ решает вопрос контроля жизненного цикла ваших компонентов и инициализации компонентов во внешней среде (помните, для этого есть Custom Elements).

Shadow DOM в документе может быть создан только через JavaScript, а потому, вы не сможете напрямую использовать предварительный рендер (SSR) для внутренней разметки. Данное ограничение можно обойти, но это отдельный непростой разговор.

В случае, если на сайте используется CSP (Content Security Policy) - вы будете ограничены в выборе способов добавления стилей для элементов внутри теневого DOM. Любая попытка парсить стили из строки вызовет ошибку. Не будет работать ни innerHTML, ни insertRule, ничто иное в этом роде. Самое простое и быстрое решение, но, на мой взгляд, наименее красивое - CSP-флаг unsafe-inline. Если вы создаете виджет для интеграции его на сторонний сайт, рекомендовать пользователям использование небезопасных настроек - это не комильфо. Для браузеров на основе Chromium, выходом может быть использование adoptedStylesheets. Более универсальными решениями будет создание динамических стилей через element.style (что, как писалось выше, имеет свои ограничения), либо добавление в Shadow DOM внешнего файла стилей:

let myElement = document.createElement('div');myElement.attachShadow({  mode: 'open',});myElement.shadowRoot.innerHTML = /*html*/ `<link rel="stylesheet" href="styles.css"><slot></slot>`;

На создание Shadow DOM, при прочих равных условиях, уходят дополнительные ресурсы, поэтому, если для вас важен вопрос производительности, старайтесь не использовать теневые участки DOM бездумно повсюду. Часто для создания виджета достаточно всего одного общего теневого DOM, без лишней вложенности и сопутствующих этому дополнительных расходов.

Ну и, поскольку данный вопрос все еще всплывает в обсуждениях, затрону и его: если среди требований к вашему проекту имеется поддержка IE - теневой DOM не ваш выбор. Радует, что все большее число разработчиков отказывается от поддержки некробраузеров, надеюсь скоро не нужно будет вообще об этом вспоминать.

Вывод

Shadow DOM - мощная и гибкая технология. Ее использование может существенно облегчить решение многих задач и открывает простор для творчества в решении задач нетипичных. Но не ждите от нее волшебного ответа на все свои вопросы и полного отсутствия сложностей.

Подробнее..

Как я закрыл трехлетний issue в TypeScript

21.06.2020 14:07:34 | Автор: admin


Всё началось с моего желания описать структуру сообщений между web worker'ами. К сожалению, на тот момент встроенные возможности TypeScript этого не позволяли.

Я засучил рукава и решил это исправить.

Суть проблемы


Попробуйте написать простой воркер и повесить на него слушателя событий. Посмотрите, какие типы выведет компилятор для параметров функции обратного вызова:

new Worker().addEventListener('message', (message) => {    message // MessageEvent    message.data // any})

В поле data находятся именно те данные, что вы, автор кода, отправляете. И именно тип этого поля хочется определять.

Изучаем MessageEvent поближе


MessageEvent интерфейс, описывающий сообщения при коммуникации между вкладками, воркерами, сокетами, WebRTC каналами и т.д.

В экосистеме TypeScript этот интерфейс является частью lib.dom.ts и lib.webworker.d.ts, и описан следующим образом:

interface MessageEvent extends Event {    readonly data: any;    readonly lastEventId: string;    readonly origin: string;    readonly ports: ReadonlyArray<MessagePort>;    readonly source: MessageEventSource | null;}

Опытные разработчики сразу увидят тут проблему. Этот интерфейс не Generic поле data описано строго как any, и мы никак не можем на это повлиять из вне.

Решив поискать информацию по этому поводу в репозитории TypeScript я быстро нашел issue в котором описана эта проблема. И даже больше автор предложил решение. Но за почти три года, оно так и не было имплементировано. Я засучил рукава, и решил сделать это.

Всего-то и нужно что привести интерфейс MessageEvent, в файлах lib/lib.dom.d.ts и lib/lib.webworker.d.ts к такому виду:

interface MessageEvent<T = any> extends Event {    readonly data: T;    readonly lastEventId: string;    readonly origin: string;    readonly ports: ReadonlyArray<MessagePort>;    readonly source: MessageEventSource | null;}

Сделаем это.

Изучаем TypeScript Instructions for Contributing Code


В нем есть целый раздел посвященный изменениям в файлах lib.d.ts. Оттуда узнаём две вещи:

  1. Файлы в папке lib/ напрямую изменять нельзя. Там находятся last-known-good версии и они периодически обновляются на основе соответствующих файлов из папки src/lib/. Вот в них то и нужно вносить правки. В нашем случае это src/lib/dom.generated.d.ts и src/lib/webworker.generated.d.ts
  2. Практически все файлы в директории src/lib/ можно просто отредактировать. За исключением генерируемых (.generated.d.ts). Такие файлы создаются с помощью утилиты TSJS-lib-generator и мы должны вносить правки именно в неё.

Изучаем TSJS-lib-generator


TSJS-lib-generator это инструмент (написанный на TS) который принимает все известные Microsoft Edge веб-интерфейсы и преобразовывает их в набор TypeScript интерфейсов. При этом существует возможность переопределить характеристики каких-либо интерфейсов, удалить некоторые или добавить новые.

Все эти правила описываются в json формате в файлах addedTypes.json, overridingTypes.json и removedTypes.json.

Правило для изменения MessageEvent


Нам нужно изменить существующий интерфейс, поэтому будем редактировать overridingTypes.json.

К сожалению, я не нашел подробной документации об синтаксисе этих файлов, но существующих примеров было достаточно для понимания концепции.

Итак, в overridingTypes.json в свойстве interfaces добавляем новый интерфейс, пока что без каких либо свойств:

{  "interfaces": {    "interface": {      "MessageEvent": {}    }  }}

Пробуем запустить сборку и проверим, что ничего не сломалось:

npm run build

TSJS-lib-generator сгенерирует те самые *.generated.d.ts файлы. И сейчас они должны быть идентичны *.generated.d.ts файлам в репозитории TS.

Добавляем свойство type-parameters, тем самым превращая MessageEvent в Generic:

{  "interfaces": {    "interface": {      "MessageEvent": {        "type-parameters": [          {            "name": "T",            "default": "any"          }        ]      }    }  }}

Запускаем сборку и проверяем результат:

interface MessageEvent<T = any> extends Event {    readonly data: any;    readonly lastEventId: string;    readonly origin: string;    readonly ports: ReadonlyArray<MessagePort>;    readonly source: MessageEventSource | null;}

Уже ближе к тому, что мы в итоге хотим получить. Добавим описание свойства data и сигнатуры конструктора:

{  "interfaces": {    "interface": {      "MessageEvent": {        "name": "MessageEvent",        "type-parameters": [          {            "name": "T",            "default": "any"          }        ],        "properties": {          "property": {            "data": {              "name": "data",              "read-only": 1,              "override-type": "T"            }          }        },        "constructor": {          "override-signatures": [            "new<T>(type: string, eventInitDict?: MessageEventInit<T>): MessageEvent<T>"          ]        }      }    }  }}

И вуаля! Генерируется в точности то, что нам необходимо.

interface MessageEvent<T = any> extends Event {    readonly data: T;    readonly lastEventId: string;    readonly origin: string;    readonly ports: ReadonlyArray<MessagePort>;    readonly source: MessageEventSource | null;}

Далее дело за малым:

  • Запустить тесты
  • Оформить Pull Request в соответствии со всеми правилами описанными в Contribution Guidelines
  • И подписать CLA от Microsoft.

Итог


Спустя неделю с момента отправки мой Pull Request был принят. 12.06.2020 были обновлены src/lib/dom.generated.d.ts и src/lib/webworker.generated.d.ts в репозитории TypeScript. А на момент написания статьи все правки перенесены в lib/lib.dom.ts и lib/lib.webworker.d.ts и уже доступны в TypeScript Nightly.
Подробнее..
Категории: Typescript , Dom api , Contributing

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru