Введение
Веб-компоненты – это набор современных стандартов, которые:
- Создание виджетов
- …которые можно использовать повторно.
- …и не нарушит работу страниц, если в следующей версии компонента изменятся внутренние детали реализации.
Означает ли это, что вам нужно решить, когда использовать HTML/JavaScript, а когда веб-компоненты? Нет! HTML и JavaScript могут создавать интерактивные визуальные вещи. Виджеты – это интерактивные визуальные элементы. При разработке виджета имеет смысл использовать свои навыки работы с HTML и JavaScript. Стандарты веб-компонентов разработаны, чтобы помочь вам в этом.
Но есть фундаментальная проблема, которая усложняет использование виджетов, созданных на основе HTML и JavaScript: дерево DOM внутри виджета не инкапсулировано от остальной части страницы. Из-за отсутствия инкапсуляции таблица стилей документа может случайно примениться к частям виджета, код JavaScript – изменить их, а идентификаторы – совпасть с идентификаторами виджета.
Веб-компоненты состоят из трех частей:
Shadow DOM решает проблему инкапсуляции дерева DOM. Четыре части веб-компонентов предназначены для совместной работы, но вы также можете выбрать, какие части веб-компонентов использовать. В этом руководстве рассказывается, как использовать Shadow DOM.
Привет, Теневой мир!
С помощью Shadow DOM элементы могут получить новый тип узла, связанный с ними. Этот новый тип узла называется теневым корнем. Элемент, с которым связан корневой элемент теневого дерева, называется теневым хостом. Контент хоста теневого дерева не отображается, вместо него показывается контент корня теневого дерева.
Например, если у вас есть такая разметка:
<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>
то вместо
<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
Array.prototype.forEach.call(
document.querySelectorAll(selector),
function (node) { node.parentNode.removeChild(node); });
}
if (!HTMLElement.prototype.createShadowRoot) {
remove('#ex1a');
document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>
как выглядит ваша страница
<button id="ex1b">Hello, world!</button>
<script>
(function () {
if (!HTMLElement.prototype.createShadowRoot) {
remove('#ex1b');
document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
return;
}
var host = document.querySelector('#ex1b');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
})();
</script>
Более того, если JavaScript на странице запрашивает, что такое textContent кнопки, он не получит "こんにちは、影の世界!", а "Hello, world!", потому что поддерево DOM под корнем тени инкапсулировано.
Разделение контента и оформления
Теперь мы рассмотрим, как использовать Shadow DOM для отделения контента от представления. Допустим, у нас есть такой тег:
<style>
.ex2a.outer {
border: 2px solid brown;
border-radius: 1em;
background: red;
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
}
.ex2a .boilerplate {
color: white;
font-family: sans-serif;
padding: 0.5em;
}
.ex2a .name {
color: black;
background: white;
font-family: "Marker Felt", cursive;
font-size: 45pt;
padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div>
Вот разметка. Вот что вы написали бы сегодня. Он не использует Shadow DOM:
<style>
.outer {
border: 2px solid brown;
border-radius: 1em;
background: red;
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
}
.boilerplate {
color: white;
font-family: sans-serif;
padding: 0.5em;
}
.name {
color: black;
background: white;
font-family: "Marker Felt", cursive;
font-size: 45pt;
padding-top: 0.2em;
}
</style>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div>
Поскольку в дереве DOM нет инкапсуляции, вся структура бейджа доступна документу. Если другие элементы на странице случайно используют те же имена классов для стилизации или скриптов, у нас будут проблемы.
Мы можем избежать неприятностей.
Шаг 1. Скройте сведения о презентации
С точки зрения семантики нас, скорее всего, интересует только следующее:
- Это бейдж.
- Имя – "Боб".
Сначала мы пишем разметку, которая ближе к истинной семантике, которую мы хотим:
<div id="nameTag">Bob</div>
Затем мы помещаем все стили и элементы div, используемые для презентации, в элемент <template>:
<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
border: 2px solid brown;
… same as above …
</style>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
Bob
</div>
</div></span>
</template>
На этом этапе отображается только "Боб". Поскольку мы переместили элементы DOM, отвечающие за представление, внутрь элемента <template>, они не отображаются, но к ним можно получить доступ из JavaScript. Теперь мы делаем это, чтобы заполнить теневой корень:
<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);
Теперь, когда мы настроили теневой корень, тег name снова отображается. Если вы нажмете правой кнопкой мыши на бейдж и выберете "Просмотреть код", то увидите, что это семантическая разметка:
<div id="nameTag">Bob</div>
Это показывает, что с помощью Shadow DOM мы скрыли детали представления бейджа из документа. Сведения о представлении инкапсулированы в Shadow DOM.
Шаг 2. Отделите контент от оформления
Теперь наш тег name скрывает детали представления со страницы, но на самом деле он не отделяет представление от контента, потому что, хотя контент (имя «Боб») находится на странице, отображаемое имя — это то, которое мы скопировали в теневой корень. Если мы захотим изменить имя на бейдже, нам придется сделать это в двух местах, и они могут рассинхронизироваться.
Элементы HTML являются композиционными, например, вы можете поместить кнопку внутрь таблицы. В данном случае нам нужна композиция: бейдж должен состоять из красного фона, текста "Привет!" и содержимого бейджа.
Вы, как автор компонента, определяете, как композиция работает с вашим виджетом, используя новый элемент <content>. Это создает точку вставки в представлении виджета, и точка вставки выбирает контент из теневого хоста для представления в этой точке.
Если мы изменим разметку в Shadow DOM следующим образом:
<span class="unchanged"><template id="nameTagTemplate">
<style>
…
</style></span>
<div class="outer">
<div class="boilerplate">
Hi! My name is
</div>
<div class="name">
<content></content>
</div>
</div>
<span class="unchanged"></template></span>
При отображении отметки содержимое теневого хоста проецируется в место, где появляется элемент <content>.
Теперь структура документа стала проще, поскольку название находится только в одном месте – в документе. Если на вашей странице когда-нибудь потребуется обновить имя пользователя, вы просто пишете:
document.querySelector('#nameTag').textContent = 'Shellie';
и всё. Отрисовка бейджа автоматически обновляется браузером, потому что мы проецируем содержимое бейджа на место с помощью <content>.
<div id="ex2b">
Теперь мы добились разделения контента и презентации. Контент находится в документе, а его представление – в Shadow DOM. Они автоматически синхронизируются браузером, когда приходит время что-то отрисовать.
Шаг 3. Получайте прибыль
Разделяя контент и его представление, мы можем упростить код, который управляет контентом. В примере с бейджем этому коду нужно иметь дело только с простой структурой, содержащей один элемент <div> вместо нескольких.
Теперь, если мы изменим презентацию, нам не нужно будет менять код.
Например, мы хотим локализовать отметку. Это по-прежнему тег name, поэтому семантическое содержание документа не меняется:
<div id="nameTag">Bob</div>
Код настройки корня теневого дерева остается прежним. Изменится только то, что помещается в теневой корень:
<template id="nameTagTemplate">
<style>
.outer {
border: 2px solid pink;
border-radius: 1em;
background: url(sakura.jpg);
font-size: 20pt;
width: 12em;
height: 7em;
text-align: center;
font-family: sans-serif;
font-weight: bold;
}
.name {
font-size: 45pt;
font-weight: normal;
margin-top: 0.8em;
padding-top: 0.2em;
}
</style>
<div class="outer">
<div class="name">
<content></content>
</div>
と申します。
</div>
</template>
Это значительное улучшение по сравнению с текущей ситуацией в интернете, поскольку код обновления имени может зависеть от структуры компонента, которая является простой и согласованной. Коду, который обновляет имя, не нужно знать структуру, используемую для отрисовки. Если мы посмотрим на то, что отображается, имя появляется вторым на английском языке (после "Привет! My name is"), а в японском – в начале (перед "と申します"). С точки зрения обновления отображаемого имени это различие не имеет семантического значения, поэтому коду обновления имени не нужно знать об этой детали.
Дополнительное задание: Продвинутое проектирование
В примере выше элемент <content> выбирает весь контент из теневого хоста. С помощью атрибута select можно управлять тем, что проецирует элемент контента. Вы также можете использовать несколько элементов контента.
Например, если у вас есть документ, содержащий следующее:
<div id="nameTag">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
и корневой элемент shadow root, который использует селекторы CSS для выбора определенного контента:
<div style="background: purple; padding: 1em;">
<div style="color: red;">
<content **select=".first"**></content>
</div>
<div style="color: yellow;">
<content **select="div"**></content>
</div>
<div style="color: blue;">
<content **select=".email">**</content>
</div>
</div>
Элемент <div class="email"> соответствует элементам <content select="div"> и <content
select=".email">. Сколько раз и в каких цветах появляется адрес электронной почты Боба?
Адрес электронной почты Боба появляется один раз и выделен желтым цветом.
Причина в том, что, как знают люди, которые взламывают Shadow DOM, построение дерева того, что на самом деле отображается на экране, похоже на огромную вечеринку. Элемент content – это приглашение, которое позволяет контенту из документа попасть на вечеринку отрисовки Shadow DOM за кулисами. Приглашения доставляются по порядку. Получатель приглашения зависит от того, кому оно адресовано (то есть от атрибута select). Контент, получив приглашение, всегда принимает его (кто бы отказался?!) и отправляется в путь. Если на этот адрес будет отправлено последующее приглашение, то оно не дойдет до адресата.
В примере выше <div class="email"> соответствует селекторам div и .email, но поскольку элемент контента с селектором div находится в документе раньше, <div class="email"> попадает на желтую вечеринку, а на синюю никто не приходит. (Возможно, поэтому он такой грустный, хотя в компании веселее, так что кто знает.)
Если же элемент не приглашен ни на одну вечеринку, то он вообще не будет отрисован. Именно это произошло с текстом "Hello, world" в самом первом примере. Это полезно, когда вы хотите добиться совершенно другого рендеринга: запишите семантическую модель в документе, который доступен для скриптов на странице, но скройте его для целей рендеринга и подключите его к совершенно другой модели рендеринга в Shadow DOM с помощью JavaScript.
Например, в HTML есть удобное окно выбора даты. Если вы введете <input
type="date">, то увидите всплывающий календарь. Но что, если вы хотите позволить пользователю выбрать диапазон дат для отпуска на десертном острове (ну, знаете, с гамаками из мармелада)? Вы настраиваете документ следующим образом:
<div class="dateRangePicker">
<label for="start">Start:</label>
<input type="date" name="startDate" id="start">
<br>
<label for="end">End:</label>
<input type="date" name="endDate" id="end">
</div>
но создайте Shadow DOM, который использует таблицу для создания красивого календаря, который выделяет диапазон дат и так далее. Когда пользователь нажимает на дни в календаре, компонент обновляет состояние в полях ввода startDate и endDate. Когда пользователь отправляет форму, отправляются значения из этих элементов ввода.
Зачем я добавил в документ ярлыки, если они не будут отображаться? Причина в том, что если пользователь просматривает форму в браузере, который не поддерживает Shadow DOM, форма по-прежнему будет работать, но будет выглядеть не так красиво. Пользователь увидит примерно следующее:
<div class="dateRangePicker">
<label for="start">Start:</label>
<input type="date" name="startDate" id="start">
<br>
<label for="end">End:</label>
<input type="date" name="endDate" id="end">
</div>
Вы прошли курс "Основы Shadow DOM"
Это основы Shadow DOM. Вы прошли базовый курс! Вы можете делать больше с Shadow DOM, например, вы можете использовать несколько теней на одном хосте теней или вложенные тени для инкапсуляции, или спроектировать свою страницу, используя Model-Driven Views (MDV) и Shadow DOM. И веб-компоненты – это не только Shadow DOM.
О них мы расскажем в следующих публикациях.