Узнайте о преимуществах использования веб-компонентов, о том, как они работают, а также о том, как начать их использовать
С помощью веб-компонентов (далее — компоненты) разработчики могут создавать собственные HTML-элементы. В данном руководстве вы узнаете все, что должны знать о компонентах. Мы начнем с того, что такое компоненты, каковы их преимущества, а также из чего они состоят.
После этого мы приступим к созданию компонентов, сначала с помощью шаблонов HTML (HTML templates) и интерфейса теневого DOM (shadow DOM), затем немного углубимся в тему и посмотрим как создать кастомизированный встроенный элемент (customized build-in element).
Что такое компоненты?
Разработчики любят компоненты (здесь имеется ввиду реализация паттерна проектирования «Модуль»). Это отличный способ определения блока кода, который может быть использован когда и где угодно. В течение многих лет предпринималось несколько более-менее успешных попыток реализовать эту идею на практике.
XML Binding Language от Mozilla и спецификации HTML Component от Microsoft для Internet Explorer 5 появились около 20 лет назад. К сожалению, обе реализации были очень сложными и не смогли заинтересовать производителей других браузеров, а потому вскоре были забыты. Несмотря на это, именно они заложили основы того, что мы имеем в этой сфере сегодня.
JavaScript-фреймворки, такие как React, Vue и Angular используют аналогичный подход. Одной из главных причин их успеха является возможность инкапсуляции общей логики приложения в некие шаблоны, легко переходящие из одной формы в другую.
Хотя данные фреймворки улучшают опыт разработки, за все приходится платить. Особенности языка, такие как JSX, нуждаются в компиляции, и большинство фреймворков используют движок JavaScript для управления своими абстракциями. Существует ли другой подход к решению проблемы разделения кода на компоненты? Ответ — веб-компоненты.
4 столпа компонентов
Компоненты состоит из трех интерфейсов (API) — кастомные элементы (custom elements), шаблоны HTML (HTML templates) и теневой DOM (shadow DOM), а также из лежащих в их основе модулей JavaScript (ES6 modules). С помощью инструментов, предоставляемых этими интерфейсами, можно создавать кастомные HTML-элементы, которые ведут себя подобно нативным аналогам.
Компоненты используются также, как и обычные элементы HTML. Их можно настраивать с помощью атрибутов, получать с помощью JavaScript, стилизовать с помощью CSS. Главное уведомить браузер о том, что они существуют.
Это позволяет компонентам взаимодействовать с другими фреймворками и библиотеками. Благодаря использованию того же механизма коммуникации, что и обычные элементы, они могут использоваться как любым существующим фреймворком, так и инструментами, которые появятся в будущем.
Также необходимо отметить, что компоненты соответствуют веб-стандартам. Веб основан на идее обратной совместимости. Это означает, что созданные сегодня компоненты, будут прекрасно работать продолжительное время.
Рассмотрим каждую спецификацию по-отдельности.
1. Кастомные элементы
Ключевые особенности:
- Определение поведения элемента
- Реагирование на изменения атрибутов
- Расширение существующих элементов
Часто при разговоре о компонентах, люди имеют ввиду интерфейс кастомных элементов.
Этот API позволяет расширять элементы, определяя их поведение при добавлении, обновлении и удалении.
class ExampleElement extends HTMLElement {
static get observedAttributes() {
return [...]
}
attributeChangedCallback(name, oldValue, newValue) {}
connectedCallback() {}
}
customElements.define('example-element', ExampleElement)
Каждый кастомный элемент имеет похожую структуру. Он расширяет функционал существующего класса «HTMLElements».
Внутри кастомного элемента содержится несколько методов, которые называются реакциями (reactions), отвечающих за обработку того или иного изменения элемента. Например, connectedCallback вызывается при добавлении элемента на страницу. Это похоже на стадии жизненного цикла, используемые в фреймворках (componentDidMount в React, mounted в Vue).
Изменение атрибутов элемента влечет за собой изменение его поведения. Когда происходит обновление, вызывается attributeChangedCallback, содержащий информацию об изменении. Это происходит только для атрибутов, указанных в массиве, возвращаемом observedAttributes.
Элемент должен быть определен перед тем, как браузер сможет его использовать. Метод «define» принимает два аргумента — название тега и его класс. Все теги должны содержать символ "-" во избежание конфликтов с существующими и будущими нативными элементами.
<example-element>Content</example-element>
Элемент может использовать как обычный тег HTML. При обнаружении такого элемента, браузер связывает его поведение с указанным классом. Данный процесс называется «обновлением» (upgrading).
Существует два типа элементов — «автономные» (autonomous) и «кастомизированные встроенные» (customized build-in). До сих пор мы рассматривали автономные элементы. Это такие элементы, которые не связаны с существующими HTML-элементами. Подобно тегам «div» и «span», которые не имеют определенного семантического значения.
Кастомизированные встроенные элементы — как следует из их названия — расширяют функционал существующих элементов HTML. Они наследуют семантическое поведение этих элементов и могут его изменять. Например, если элемент «input» был кастомизирован, он все равно останется полем для ввода данных и частью формы при ее отправке.
class CustomInput extends HTMLInputElement {}
customElements.define('custom-input', CustomInput, { extends: 'input' })
Класс кастомизированного встроенного элемента расширяет класс кастомизируемого элемента. При определении (define) встроенного элемента в качестве третьего аргумента передается расширяемый элемент.
<input is="custom-input" />
Использование тега также немного отличается. Вместо нового тега используется существующий с указанием специального атрибута расширения «is». Когда браузер встречает этот атрибут, он понимает, что имеет дело с кастомным элементом и обновляет его соответствующим образом.
В то время, как автономные элементы поддерживаются большинством современных браузеров, кастомизированные встроенные элементы поддерживаются только Chrome и Firefox. При использовании последних в браузере, который их не поддерживает, они будут рассматриваться как обычные HTML-элементы, так что, по большему счету, их использование является безопасным даже в таких браузерах.
2. Шаблоны HTML
- Создание готовых структур
- Не отображаются на странице до вызова
- Содержат HTML, CSS и JS
Исторически создание шаблонов на стороне клиента предполагало конкатенацию строк в JavaScript или использование таких библиотек, как Handlebars, разбирающих блоки специальной разметки. Недавно в спецификации появился тег «template», который может содержать все, что мы хотим использовать.
<template id="tweet">
<div class="tweet">
<span class="message"></span>
Written by @
<span class="username"></span>
</div>
</template>
Сам по себе он никак не влияет на страницу, т.е. он не парсится движком, запросы на получение ресурсов (аудио, видео) не отправляются. JavaScript не может получить к нему доступ, а для браузеров — это пустой элемент.
const template = document.getElementById('tweet')
const node = document.importNode(template.content, true)
document.body.append(node)
Сначала мы получаем элемент «template». Метод «importNode» создает копию его содержимого, второй аргумент (true) означает глубокое копирование. Наконец, мы добавляем его на страницу, как любой другой элемент.
Шаблоны могут содержать все, что может содержать обычный HTML, включая CSS и JavaScript. При добавлении элемента на страницу, к нему будут применены стили и запущены скрипты. Помните, что стили и скрипты являются глобальными, а, значит, могут перезаписать другие стили и значения, используемые скриптами.
Этим шаблоны не ограничены. Они предстают во всей красе при использовании с другими частями компонентов, в частности, с теневым DOM.
3. Теневой DOM
- Позволяет избежать конфликта стилей
- Придумывать названия (классов, например) становится проще
- Инкапсуляция логики реализации
Объектная модель документа (Document Object Model, DOM) — это то, как браузер интерпретирует структуру страницы. Читая разметку, браузер определяет какие элементы какой контент содержат и на основе этого принимает решение о том, что следует отображать на странице. При использовании document.getElemetById(), например, браузер обращается к DOM в поисках нужного элемента.
Для макета (layout) страницы такой подход является приемлемым, но что насчет деталей, скрытых внутри элемента? Например, страницу не должно беспокоить то, какой интерфейс содержится внутри элемента «video». Вот где теневой DOM приходит на помощь.
<div id="shadow-root"></div>
<script>
const host = document.getElementById('shadow-root')
const shadow = host.attachShadow({ mode: 'open' })
</script>
Теневой DOM создается в момент применения к элементу. В теневой DOM можно добавлять любой контент, как и в обычный («светлый», light) DOM. На теневой DOM не влияет то, что происходит снаружи, т.е. за его пределами. Обычный DOM также не может получить доступ к теневому напрямую. Это означает, что в теневом DOM мы можем использовать любые названия классов, стили и скрипты и не переживать о возможных конфликтах.
Наилучший результат дает использование теневого DOM вкупе с кастомными элементами. Благодаря теневому DOM при повторном использовании компонента его стили и структура никак не влияют на другие элементы на странице.
ES и HTML модули
- Добавление при необходимости
- Предварительная генерация не требуется
- Все хранится в одном месте
В то время, как три предыдущие спецификации прошли долгий путь в своем развитии, способ их упаковки и переиспользования остается предметом напряженных дискуссий.
Спецификация импортов HTML (HTML Imports) определяет способ экспорта и импорта HTML документов, а также CSS и JavaScript. Это позволило бы кастомным элементам вместе с шаблонами и теневым DOM находится в другом месте и использоваться по необходимости.
Однако, Firefox отказался от реализации данной спецификации в своем браузере и предложил иной способ на основе JavaScript-модулей.
export class ExampleElement external HTMLElement {}
import { ExampleElement } from 'ExampleElement.js'
Модули по умолчанию имеют собственное пространство имен, т.е. их содержимое не является глобальным. Экспортируемые переменные, функции и классы, могут импортироваться где и когда угодно и использоваться как локальные ресурсы.
Это прекрасно подходит для компонентов. Кастомные элементы, содержащие шаблон и теневой DOM могут экспортироваться из одного файла и использовать в другом.
import { ExampleElement } from 'ExampleElement.html'
Microsoft выдвинула предложение о расширении спецификации JavaScript-модулей экспортом/импортом HTML. Это позволит создавать компоненты с помощью декларативного и семантического HTML. Данная возможность скоро появится в Chrome и Edge.
Создание собственного компонента
Несмотря на множество вещей, связанных с компонентами, которые могут показаться вам сложными, создание и использование простого компонента занимает всего несколько строк кода. Рассмотрим примеров.
Компоненты позволяют отображать комментарии пользователей с помощью интерфейсов шаблонов HTML и теневого DOM.
Создадим компонент для отображения комментариев пользователей с помощью шаблонов HTML и теневой DOM.
1. Создание шаблона
Компоненту нужен шаблон для копирования перед генерацией разметки. Шаблон может находиться где угодно на странице, класс кастомного элемента получает к нему доступ через идентификатор.
Добавляем элемент «template» на страницу. Любые стили, определенные в этом элементе, будут влиять только на него.
<template id="user-comment-template">
<style>
...
</style>
</template>
2. Добавление разметки
Кроме стилей, компонент может содержать макет (структуру). Для этих целей используется элемент «div».
Динамический контент передается через слоты (slots). Добавим слоты для аватара, имени и сообщения пользователя с соответствующими атрибутами «name»:
<div class="container">
<div class="avatar-container">
<slot name="avatar"></slot>
</div>
<div class="comment">
<slot name="username"></slot>
<slot name="comment"></slot>
</div>
</div>
Содержимое слота по умолчанию
Контент по умолчанию будет отображаться при отсутствии переданной слоту информации
Данные, переданные слоту, перезаписывают данные в шаблоне. Если слоту не передается информации, отображается контент по умолчанию.
В данном случае, если имя пользователя не было передано, на его месте отображается сообщение «No name»:
<slot name="username">
<span class="unknown">No name</span>
</slot>
3. Создание класса
Создание кастомного элемента начинается с расширения класса «HTMLElement». Частью процесса настройки является создание теневого корневого узла (shadow root) для рендеринга контента элемента. Открываем его для получения доступа на следующем этапе.
Наконец, сообщаем браузеру о новом классе «UserComment».
class UserComment extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
}
customElements.define('user-comment', UserComment)
4. Применение теневого контента
Когда браузер встречает элемент «user-comment», он обращается к теневому корневому узлу для получения его содержимого. Второй аргумент указывает браузеру копировать весь контент, а не только первый слой (элементы верхнего уровня).
Добавляем разметку в теневой корневой узел, что приводит к немедленному обновлению внешнего вида компонента.
connectedCallback() {
const template = document.getElementById('user-comment-template')
const node = document.importNode(template.content, true)
this.shadowRoot.append(node)
}
5. Использование компонента
Теперь компонент готов к использованию. Добавляем тег «user-comment» и передаем ему необходимую информацию.
Поскольку все слоты имеют названия, все что будет передано за их пределами, будет проигнорировано. Все, что находится внутри слотов копируется в точности как передается, включая стилизацию.
<user-comment>
<img alt="" slot="avatar" src="avatar.png" />
<span slot="username">Matt Crouch</span>
<div slot="comment">This is an example of a comment</div>
</user-comment>
Рсширенный код примера:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Example</title>
<style>
body {
display: grid;
place-items: center;
}
img {
width: 80px;
border-radius: 4px;
}
</style>
</head>
<body>
<template id="user-comment-template">
<div class="container">
<div class="avatar-container">
<slot name="avatar">
<slot class="unknown"></slot>
</slot>
</div>
<div class="comment">
<slot name="username">No name</slot>
<slot name="comment"></slot>
</div>
</div>
<style>
.container {
width: 320px;
clear: both;
margin-bottom: 1rem;
}
.avatar-container {
float: left;
margin-right: 1rem;
}
.comment {
height: 80px;
display: flex;
flex-direction: column;
justify-content: center;
}
.unknown {
display: block;
width: 80px;
height: 80px;
border-radius: 4px;
background: #ccc;
}
</style>
</template>
<user-comment>
<img alt="" slot="avatar" src="avatar1.jpg" />
<span slot="username">Matt Crouch</span>
<div slot="comment">Fisrt comment</div>
</user-comment>
<user-comment>
<img alt="" slot="avatar" src="avatar2.jpg" />
<!-- no username -->
<div slot="comment">Second comment</div>
</user-comment>
<user-comment>
<!-- no avatar -->
<span slot="username">John Smith</span>
<div slot="comment">Second comment</div>
</user-comment>
<script>
class UserComment extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
const template = document.getElementById("user-comment-template");
const node = document.importNode(template.content, true);
this.shadowRoot.append(node);
}
}
customElements.define("user-comment", UserComment);
</script>
</body>
</html>
Создание кастомизированного встроенного элемента
Как отмечалось ранее, кастомные элементы могут расширять существующие. Это помогает сэкономить время за счет сохранения стандартного поведения элемента, обеспечиваемого брузером. В данном разделе мы рассмотрим, как можно расширить элемент «time».
1. Создание класса
Встроенные элементы, как и автономные, появляются в момент расширения класса, но вместо общего класса «HTMLElement», они расширяют конкретный класс.
В нашем случае таким классом является HTMLTimeElement — класс, используемый элементами «time». Он включает поведение, связанное с атрибутом «datetime», включая формат данных.
class RelativeTime extends HTMLTimeElement {}
2. Определение элемента
Элемент регистрируется браузером с помощью метода «define». Однако, в отличие от автономного элемента, при регистрации встроенного элемента методу «define» необходимо передать третий аргумент — объект с настройками.
Наш объект будет содержать один ключ со значением кастомизируемого элемента. Он принимает название тега. В случае отсутствия такого ключа будет выброшено исключение.
customElements.define('relative-time', RelativeTime, { extends: 'time' })
3. Установка времени
Поскольку на странице у нас может быть несколько компонентов, в компоненте должен быть предусмотрен метод для установки значения элемента. Внутри этого метода компонент передает библиотеке «timeago» значение времени и устанавливает возвращаемое этой библиотекой значение в качестве значения элемента (извиняюсь за тавтологию).
Наконец, мы устанавливаем атрибут «title», позволяющий пользователю при наведении курсора увидеть установленное значение.
setTime() {
this.innerHTML = timeago().format(this.getAttribute('datetime'))
this.setAttribute('title', this.getAttribute('datetime'))
}
4. Обновление соединения
Компонент может использовать метод сразу после отображения на странице. Поскольку встроенные компоненты не имеют теневого DOM, они не нуждаются в конструкторе.
connectedCAllback() {
this.setTime()
}
5. Слежение за изменением атрибутов
Если программно обновить время, компонент не отреагирует. Он не знает о том, что должен следить за изменениями атрибута «datetime».
После определения наблюдаемых атрибутов (observed attributes), attributeChangedCallback будет вызываться при каждом их изменении.
static get observedAttributes() {
return ['datetime']
}
attributeChangedCallback() {
this.setTime()
}
6. Добавление на страницу
Поскольку наш элемент является расширением нативного элемента, его имплементация немного отличается. Для его использования добавляем на страницу тег «time» со специальным атрибутом «is», значением которого является название встроенного элемента, определенного при регистрации. Браузеры, которые не поддерживают компоненты, будут отображать запасной контент.
<time is="relative-time" datetime="2020-09-20T12:00:00+0000">
20 сентября 2020 г. 12:00
</time>
Расширенный код примера:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Another Example</title>
<!-- timeago.js -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"
integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="
crossorigin="anonymous"
></script>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
}
input,
button {
margin-bottom: 0.5rem;
}
time {
font-size: 2rem;
}
</style>
</head>
<body>
<input type="text" placeholder="2020-10-20" value="2020-08-19" />
<button>Set Time</button>
<time is="relative-time" datetime="2020-09-19">
19 сентября 2020 г.
</time>
<script>
class RelativeTime extends HTMLTimeElement {
setTime() {
this.innerHTML = timeago.format(this.getAttribute("datetime"));
this.setAttribute("title", this.getAttribute("datetime"));
}
connectedCallback() {
this.setTime();
}
static get observedAttributes() {
return ["datetime"];
}
attributeChangedCallback() {
this.setTime();
}
}
customElements.define("relative-time", RelativeTime, { extends: "time" });
const button = document.querySelector("button");
const input = document.querySelector("input");
const time = document.querySelector("time");
button.onclick = () => {
const { value } = input;
time.setAttribute("datetime", value);
};
</script>
</body>
</html>
Надеюсь, я помог вам сформировать общее представление о том, что такое веб-компоненты, для чего они нужны и как используются.
Full-R
Ха-ха. Я так понимаю, что теперь и слово реактивность ни чего не значит? То есть то что рожали эти React, Vue и Angular и за что горлопанили эти разрабы у нас теперь есть нативно? Браузер все правильно делает. Он таких разработчиков просто отжимает потихоньку и то что нужно в оконцовке появляется в правильном виде. Спасибо за перевод.