Инкапсуляция в коде веб-приложений: что такое shadow DOM
Перевод статьи Дмитрия Глазкова о тонкостях реализации инкапсуляции в процессе разработки веб-приложений и о shadow DOM.
Если вы разрабатываете веб-сайты, то, вероятнее всего, используете JavaScript-библиотеки. В таком случае вы, должно быть, признательны безымянным героям, которые делают эти библиотеки лучше.
Одной из типичных проблем, с которыми приходится сталкиваться этим бравым воинам, является инкапсуляция. Каким образом вы осуществляете связь между кодом, который вы написали, и тем, который будет его использовать?
За исключением SVG (об этом несколько позже), сегодня веб предлагает только один встроенный механизм, позволяющий изолировать один участок кода от другого, и он не очень-то хорош. Да, речь об iframe-ах. В большинстве случаев, где требуется инкапсуляция, фреймы слишком тяжелые и неповоротливые.
Что значит, я должен поместить каждую кнопку в отдельный фрейм? Вы спятили?
Так что нам нужно кое-что получше. Оказывается, большинство браузеров уже давно втихаря реализуют мощную технику реализации инкапсуляции. Эта техника называется shadow DOM.
Меня зовут DOM, Shadow DOM
Shadow DOM основывается на способности браузеров включать поддерево элементов в рендеринг документа, но не в основное DOM-дерево этого документа. Рассмотрим простейший слайдер:
<input id="foo" type="range">
В любом браузере на движке Webkit этот код будет выглядеть примерно так:
Достаточно просто. Есть дорожка слайдера и бегунок, который может по ней двигаться. Стоп, что? Отдельный движущийся элемент в input-е? Почему я не вижу его в JavaScript-коде?
var slider = document.getElementsById("foo"); console.log(slider.firstChild); // возвращает null
Это какое-то колдовство?
Никакой магии, мой юный волшебник, а просто shadow DOM в действии. Видишь ли, разработчики браузеров поняли, что программировать внешний вид и поведение всех элементов HTML во-первых, сложно, во-вторых, глупо. Так что они решили схитрить.
Они сделали связку между тем, что доступно тебе, веб-программисту, и деталями реализации, скрытыми от твоих глаз. Однако сам браузер при желании может пересекать эту границу. Благодаря этому можно создавать HTML-элементы используя старые добрые веб-технологии, вне div-ов и span-ов.
Некоторые из них совсем простые, как наш слайдер. Другие довольно сложны. Возьмите, например, video со всеми его кнопками, переключателями, контроллерами звука и так далее.
Все это — просто HTML и CSS — спрятанные в DOM-поддереве.
Как это работает? Для наглядности представим, что мы можем сунуться туда с помощью JavaScript.
Пусть дана страница:
<html> <head> <style> p { color: Green; } </style> </head> <body> <p>My Future is so bright</p> <div id="foo"></div> <script> var foo = document.getElementById('foo'); // WARNING: Pseudocode, not a real API. foo.shadow = document.createElement('p'); foo.shadow.textContent = 'I gotta wear shades'; </script> </body> </html>
Наше DOM-дерево выглядит примерно так:
<p>My Future is so bright</p> <div id="foo"></div>
Но рендерится оно так, как если бы выглядело следующим образом:
<p>My Future is so bright</p> <div id="foo"> <!-- shadow subtree begins --> <p>I gotta wear shades</p> </div> <!-- shadow subtree ends -->
Визуально получится что-то вроде этого:
Заметили, что вторая половина предложения не зеленая? Это из-за того, что селектор p не может достичь shadow DOM. Круто, а? Что бы отдал разработчик фреймворка за такую суперспособность? Возможность написать виджет, не беспокоясь о том, что какой-нибудь селектор может вступить в конфликт с основными стилями… одна мысль об этом пьянит и будоражит.
Ход событий
Дабы избежать излишней противоестественности, события, происходящие в shadow DOM, доступны из основного документа. Например, если вы кликнете на кнопку отключения звука в аудио элементе, ваши event listener-ы из ближайшего div-а услышат этот клик:
<div onclick="alert('who dat?')"> <audio controls src="test.wav"></audio> </div>
Однако, если вы зададитесь вопросом, где произошло событие, ответом будет сам аудиоэлемент, а не какая-то кнопка внутри него.
<div onclick="alert('fired by:' + event.target)"> <audio controls src="test.wav"></audio> </div>
Почему? Потому что при пересечении границы shadow DOM события перенаправляются, чтобы избежать доступа извне к теневому поддереву.
Мы имеем еще одного туза в рукаве. Это способ, которым CSS достигает элементов поддерева. Дело в том, что стилизовать элементы shadow DOM можно, даже не имея к ним прямого доступа. При построении поддерева оно само определяет, к каким именно его частям должны примениться стили.