Предлагаю вашему вниманию перевод статьи «Introducing Slot-Based Shadow DOM API» автора Ryosuke Niwa, написанную им в блоге WebKit осенью прошлого года.
Мы рады анонсировать что базовая поддержка нового Shadow DOM API на основе слотов, которую мы предлагали в апреле (прим. переводчика: речь идёт об апреле 2015) уже доступна в ночных сборках WebKit после r190680. Shadow DOM это часть Веб Компонентов – набора спецификаций, изначально предложенных Google для того чтобы сделать возможным создание переиспользуемых виджетов и компонентов в вебе. Shadow DOM, в частности, предоставляет легковесную инкапсуляцию DOM дерева, позволяя создавать на элементе параллельное дерево, так называемое «теневое shadow дерево», с помощью которого изменяется отрисовка элемента без изменения DOM. Пользователи такого компонента не смогут ненароком что-то в нём изменить, ведь его shadow дерево не является привычным потомком элемента-хоста. Кроме того, действие стилей также ограничено областью действия (scope), а значит CSS правила, объявленные снаружи shadow дерева не применяются к элементам внутри такого дерева, а правила, объявленные внутри – к элементам снаружи.
Изоляция стилей
Первое значительное преимущество использования shadow DOM – это изоляция стилей. Представим, что мы хотим создать собственный прогресбар. Например, следующим образом мы могли бы использовать два вложенных div'а для того чтобы представить сам прогресбар и ещё один div с текстом, в котором показывать процент выполнения:
<style>
.progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; }
.progress > .bar { background: #9cf; height: 100%; }
.progress > .label { position: absolute; top: 0; left: 0; width: 100%;
text-align: center; font-size: 0.8rem; line-height: 1.1rem; }
</style>
<template id="progress-bar-template">
<div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="bar"></div>
<div class="label">0%</div>
</div>
</template>
<script>
function createProgressBar() {
var fragment = document.getElementById('progress-bar-template').content.cloneNode(true);
var progressBar = fragment.querySelector('div');
progressBar.updateProgress = function (newPercentage) {
this.setAttribute('aria-valuenow', newPercentage);
this.querySelector('.label').textContent = newPercentage + '%';
this.querySelector('.bar').style.width = newPercentage + '%';
}
return progressBar;
}
</script>
Обратите внимание на элемент template, использование которого позволяет автору включить сниппет HTML-текста, чтобы позже быть инстанциированным путём создания клона. Это первая фича «веб компонентов», внедрённая нами в WebKit; позже её включили в спецификацию HTML5. Элементу template в документе разрешено появляться в любом месте (скажем, между table
и tr
), а содержимое внутри template инертно и не выполняет скриптов и загрузку изображений или любых других ресурсов. Таким образом, пользователю сего прогресбара будет достаточно инстанциировать и обновлять его как показано ниже:
var progressBar = createProgressBar();
container.appendChild(progressBar);
...
progressBar.updateProgress(10);
С такой реализацией прогресбара есть одна проблема: оба его div'а доступны любому желающему, а стили не ограничены только рамками самого элемента. К примеру, стили прогресбара, определённые для CSS класса progress
будут так же применены и к следующему HTML:
<section class="project">
<p class="progress">Pending an approval</p>
</section>
А стили других элементов будут переопределять внешний вид прогресбара:
<style>
.label { font-weight: bold; }
</style>
Мы могли бы обойти эти ограничения, дав прогресбару имя custom element, например custom-progressbar
чтобы ограничить область действия стилей, а затем проинициализировать все остальные свойства в all: initial
, однако в мире Shadow DOM есть более элегантное решение. Основная идея в том, чтобы представить внешний div в качестве дополнительного слоя инкапсуляции так что пользователи не увидят что происходит внутри (создание div'ов для лейбы и самого ползунка), стили прогресбара не будут вмешиваться в работу остальной страницы и наоборот. Для этого нам понадобится сначала создать ShadowRoot
, вызвав метод attachShadow({mode: 'closed'})
у прогресбара, а следом вставить в него DOM узлы, необходимые для нашей реализации. Допустим, мы и дальше используем div для задания хоста данному shadow root, тогда мы можем следующим образом создать новый div и приаттачить shadow root:
<template id="progress-bar-template">
<style>
.progress { position: relative; border: solid 1px #000; padding: 1px; width: 100px; height: 1rem; }
.progress > .bar { background: #9cf; height: 100%; }
.progress > .label { position: absolute; top: 0; left: 0; width: 100%;
text-align: center; font-size: 0.8rem; line-height: 1.1rem; }
</style>
<div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="bar"></div>
<div class="label">0%</div>
</div>
</template>
<script>
function createProgressBar() {
var progressBar = document.createElement('div');
var shadowRoot = progressBar.attachShadow({mode: 'closed'});
shadowRoot.appendChild(document.getElementById('progress-bar-template').content.cloneNode(true));
progressBar.updateProgress = function (newPercentage) {
shadowRoot.querySelector('.progress').setAttribute('aria-valuenow', newPercentage);
shadowRoot.querySelector('.label').textContent = newPercentage + '%';
shadowRoot.querySelector('.bar').style.width = newPercentage + '%';
}
return progressBar;
}
</script>
Обратите внимание, что элемент style находится внутри template и будет склонирован в shadow root вместе с div'ами. Это ограничит область действия стилей этим самым shadow root. Точно так же стили снаружи не применяются к элементам внутри.
Совет: во время дебага может оказаться полезным режим open
shadow DOM, при котором shadow root будет доступен через свойство shadowRoot элемента-хоста. Например, {mode: DEBUG ? 'open' : 'closed'}
Копозиция слотов
Внимательный читатель к этому моменту наверняка задался вопросом: почему бы не сделать это средствами CSS и не лезть в DOM? Стилизация это концепция представления, зачем же мы добавляем новые элементы в DOM? На самом деле, первый публичный рабочий черновик CSS Scoping Module Level 1 определяет правило @scope как раз для этих целей. Зачем же понадобился ещё один механизм изолирования стилей? Хорошей причиной для имплементации послужило то, что элементы реализованные внутри компонента скрыты от внешних механизмов обхода узлов, таких как querySelectorAll
и getElementsByTagName
. Из-за того что по умолчанию узлы внутри shadow root не обнаруживаются этими API, пользователи компонент могут не задумываться о внутренней реализации каждого компонента. Каждый компонент представлен в виде непрозрачного элемента, детали реализации которого инкапсулированы внутри его shadow DOM. Имейте в виду, что shadow DOM никоим образом не заботится о cross-origin ограничениях как это делает элемент iframe. При необходимости другие сценарии смогут проникнуть внутрь shadow DOM. Однако, есть и другая причина по которой появился этот механизм – композиция. Допустим, у нас есть список контактов:
<ul id="contacts">
<li>
Commit Queue
(<a href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br>
One Infinite Loop, Cupertino, CA 95014
</li>
<li>
Niwa, Ryosuke
(<a href="mailto:rniwa@webkit.org">rniwa@webkit.org</a>)<br>
Two Infinite Loop, Cupertino, CA 95014
</li>
</ul>
и хотим мы каждому пункту контактной информации из списка добавить красивостей при включённых скриптах:
Вместо того чтобы копировать весь этот текст в наш собственный shadow DOM, мы могли бы следующим образом использовать именованные слоты для отрисовки текста в коде нашего shadow DOM не меняя его:
<template id="contact-template">
<style>
:host { border: solid 1px #ccc; border-radius: 0.5rem; padding: 0.5rem; margin: 0.5rem; }
b { display: inline-block; width: 5rem; }
</style>
<b>Name</b>: <slot name="fullName"><slot name="firstName"></slot> <slot name="lastName"></slot></slot><br>
<b>Email</b>: <slot name="email">Unknown</slot><br>
<b>Address</b>: <slot name="address">Unknown</slot>
</template>
<script>
window.addEventListener('DOMContentLoaded', function () {
var contacts = document.getElementById('contacts').children;
var template = document.getElementById('contact-template').content;
for (var i = 0; i < contacts.length; i++)
contacts[i].attachShadow({mode: 'closed'}).appendChild(template.cloneNode(true));
});
</script>
Концептуально слоты – это незаполненные пробелы в shadow DOM, заполняемые потомками элемента-хоста. Каждый элемент назначается слоту с именем, определённым в атрибуте slot
:
<ul id="contacts">
<li>
<span slot="fullName">Commit Queue</span>
(<a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>)<br>
<span slot="address">One Infinite Loop, Cupertino, CA 95014</span>
</li>
</ul>
Таким образом мы присоединяем к li
наш shadow root, а каждый span
с атрибутом slot
назначается слоту с соответствующим именем внутри shadow DOM. Взглянем подробнее на шаблон shadow DOM:
<b>Name</b>:
<slot name="fullName">
<slot name="firstName"></slot>
<slot name="lastName"></slot>
</slot><br>
<b>Email</b>: <slot name="email">Unknown</slot><br>
<b>Address</b>: <slot name="address">Unknown</slot>
В этом шаблоне имеется два слота с названиями email
и address
, а также слот с названием fullName
, содержащий внутри себя два других слота firstName
и lastName
. Слот fullName
пользуется техникой фолбека, когда firstName
и lastName
отображаются только в случае отсутствия узлов назначенных fullName
. Несмотря на то, что в данном случае каждому слоту назначен ровно один узел, мы могли бы назначить множество элементов с одинаковым атрибутом slot
одному и тому же слоту, тогда бы они отображались в том же порядке, в каком они расположены потомками хост-элемента. Можно также использовать безымянные стандартные слоты, их заполнят те потомки хоста, у которых не указан атрибут slot
. Когда браузер рендерит этот компонент, содержимое li заменяется на shadow DOM, а слоты внутри него заменяются назначенными узлами так, будто на самом деле отображается следующий DOM:
<ul id="contacts">
<li>
<!--shadow-root-start-->
<b>Name</b>:
<slot name="fullName">
<!--slot-content-start-->
<span slot="fullName">Commit Queue</span>
<!--slot-content-end-->
</slot><br>
<b>Email</b>:
<slot name="email">
<!--slot-content-start-->
<a slot="email" href="mailto:commit-queue@webkit.org">commit-queue@webkit.org</a>
<!--slot-content-end-->
</slot><br>
<b>Address</b>:
<slot name="address">
<!--slot-content-start-->
<span slot="address">One Infinite Loop, Cupertino, CA 95014</span>
<!--slot-content-end-->
</slot>
<!--shadow-root-end-->
</li>
</ul>
Как видите, основанная на слотах композиция это мощный инструмент, позволяющий виджетам вставлять в страницу содержимое без клонирования и изменения DOM. С его помощью виджеты могут реагировать на изменения их потомков не прибегая к MutationObserver или каким-либо явным уведомлениям от сценариев. В сущности, композиция превращает DOM в связующий механизм коммуникации между компонентами.
Стилизация хост-элемента
Есть ещё один момент, который стоит отметить в предыдущем примере – мистический псевдокласс :host
:
<template id="contact-template">
<style>
:host { border: solid 1px #ccc; border-radius: 0.5rem; padding: 0.5rem; margin: 0.5rem; }
b { display: inline-block; width: 5rem; }
</style>
...
</template>
Этот псевдокласс, как следует из его имени, применяется к хосту shadow DOM, в котором находится это правило. По умолчанию, авторские стили снаружи shadow DOM имеют более высокий приоритет в сравнении со стилями внутри shadow DOM. Это сделано чтобы внутри компонента можно было определить «стили по умолчанию», а пользователям компонента дать возможность их переопределять когда нужно. В дополнение, компонент может определить принципиально важные для его отображения стили (такие, например, как ширина или display
) с ключевым словом !important
. Любые !important
правила внутри shadow DOM считаются более приоритетными тех !important
, что объявлены снаружи.
Дальнейшая работа
Много работы ещё впереди относительно внедрения Веб Компонентов. Мы бы хотели позволить стилизировать слоты shadow DOM через стили соответствующих внешних узлов. Есть также пожелания научить компоненты встраиваться в тему документа, а также выставлять наружу стилизируемые части в виде псевдоэлементов CSS. В долгосрочной перспективе мы бы хотели увидеть императивный DOM API для манипуляции назначением слотов, мы уже давно предлагали это сделать. А ещё мы заинтересованы в дополнении shadow DOM произвольными (custom) элементами. Вкратце, custom elements API даёт авторам возможность ассоциировать классы JavaScript с конкретным именем элемента в документах HTML; отличный способ идеоматически назначать произвольное поведение и shadow DOM. К сожалению, на данный момент существует несколько противоречивых предложений о том как и когда создавать произвольные элементы. Чтобы помочь направить обсуждение в W3C мы планируем сделать прототип в WebKit. Для сборки пакетов и поставки Веб Компонентов мы работаем над модулями ES6. Как и Mozilla, мы верим что модули радикально изменят отношение авторов к структурированию их страниц. В конечном счёте мы хотели бы спроектировать API создания полностью изолированных веб компонент с подобными iframe политиками безопасности, основанными на shadow DOM и произвольных элементах. В заключение, хотелось бы отметить что мы действительно очень гордимся тем, что значительные возможности Веб Компонентов появляются в WebKit, мы будем и дальше писать о новых появляющихся возможностях. В случае если у вас остались какие-то вопросы обращайтесь напрямую ко мне, @WebKit или к Джону Дэвису.
Автор: nilfalse