JavaScript

Анатомия веб-компонентов

Архитектура внешнего интерфейса на основе компонентов - это новая мода в веб-разработке. Это делает React, Angular и другие трендовые фреймворки. Это обеспечивает гибкость для повторного использования общих компонентов. Давайте узнаем о веб-компонентах, которые представляют собой встроенный механизм в браузере для создания пользовательских компонентов.

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

Это поле содержит жаргоны для отображения полезного вида на экране или обеспечения некоторых интерактивных / неинтерактивных функций. Но данное условие состоит в том, что включенные в это поле CSS и JavaScript не должны влиять на другие части нашего приложения, если это не сделано намеренно.

Вот несколько требований к веб-компоненту

  1. Веб-компонент должен быть настраиваемым элементом HTML.
  2. Веб-компонент должен содержать всю логику для функционирования самого себя.
  3. Веб-компонент не должен непреднамеренно влиять на другие компоненты и другие части приложения.
  4. Веб-компонент должен инкапсулировать себя, чтобы избежать конфликтов между внешними кодами JavaScript или CSS.
  5. Несколько экземпляров одного и того же веб-компонента не должны мешать друг другу.

Когда мы говорим веб-компонент, представьте себе закрытое поле, содержащее все жаргоны (HTML, CSS, JavaScript) для обеспечения некоторых функций для Например, виджет, показывающий карточку пользователя. Эта карточка содержит изображение профиля пользователя, основную информацию, такую ​​как имя, адрес электронной почты и т. Д., И кнопку для подписки на него / нее. Эта закрытая коробка самодостаточна, чтобы нарисовать полную картину.

Итак, что это за закрытая коробка и как она выглядит в обычном веб-приложении? Что ж, это закрытое поле - не что иное, как настраиваемый элемент HTML. Например,

<user-card></user-card>

Здесь user-card похож на HTML-элемент div или span, но его внешний вид и поведение будут определяться нами, а не браузером. Так как его создать?

Создание настраиваемого элемента

Мы можем зарегистрировать пользовательский элемент, используя CustomElementRegistry интерфейс. customElements - это объект только для чтения (экземпляр CustomElementRegistry), присутствующий в объекте window, который предоставляет глобальный API для создания настраиваемых элементов.

Регистрация настраиваемого элемента

Пользовательский элемент может быть создан с использованием метода window.customElements.define() или просто customElement.define() с синтаксисом ниже.

customElements.define(name, constructor, options);
  • name: Имя тега настраиваемого элемента, например my-component
  • конструктор: функция-конструктор или класс ES6, которые обеспечивают механизм инициализации и поведение нашего настраиваемого элемента
  • параметры: объект, обеспечивающий дополнительную конфигурацию для нашего настраиваемого элемента

Давайте определим простой user-card пользовательский элемент, используя метод customElement.define().

document.registerElement() - еще один старый способ регистрации настраиваемого элемента, но он был устаревшим в пользу метода customElements.define().

В настоящий момент наш элемент user-card ничего не делает. Давайте посмотрим на часть JavaScript. Мы создали класс UserCard, который расширяет HTMLElement. Внутри у нас есть только конструктор, который вызывает super. Используя реестр customElements, мы определили элемент user-card.

Но почему мы должны расширять класс UserCard до класса HTMLElement? Что это за HTMLElement класс и откуда он?

HTMLElement - это глобальный класс, присутствующий в объекте window. Мы знаем, что такое DOM, это представление элементов HTML в объектах JavaScript. Каждый элемент DOM имеет некоторые общие свойства, такие как атрибуты, обработчики событий, методы манипуляции, средства получения и установки свойств и т.д. все еще имеют общее поведение.

У каждого элемента HTML есть функция-конструктор или класс. Например, элемент body является экземпляром HTMLBodyElement, который расширяет HTMLElement.

В приведенном выше примере document.body в консоли указывает на элемент HTML в DOM, но его фактическая реализация JavaScript скрыта от нас. Его функция-конструктор или класс HTMLBodyElement, который расширяет HTMLElement. Каждый раз, когда браузер встречает новый элемент тела на странице, он создает новый экземпляр HTMLBodyElement. Элементы DOM, наследующие HTMLElement, будут иметь общее поведение.

Чтобы проверить приведенные выше утверждения, просто откройте инструмент отладчика и console.dir любой элемент dom (вы можете использовать document.body), чтобы проверить его конструктор и цепочку прототипов. Ниже вы найдете иерархию наследования.

HTMLBodyElement > HTMLElement > Element > Node > EventTarget

Это означает, что когда вы хотите создать настраиваемый элемент, он должен иметь функцию-конструктор или класс, который должен наследовать класс HTMLElement. Наш собственный класс элемента также может расширять существующие классы элементов, такие как HTMLDivElement, чтобы расширить дополнительное поведение до div элемента. Все это делается с помощью функции customElements.define().

Давайте разберемся, как можно создать настраиваемый встроенный элемент.

В приведенном выше примере у нас есть тот же элемент user-card, но мы передали дополнительную конфигурацию с {extends: 'div'}, в котором говорится, что мы хотим создать этот элемент из встроенного элемента div, поэтому наш UserCard должен расширять конкретный конструктор элемента div, который равен HTMLDivElement. Функциональность не сильно изменилась, если мы увидим результат, но внутренне элемент user-card ведет себя как элемент div.

Главное помнить, что div ведет себя как элемент block, потому что стили CSS по умолчанию, предоставляемые браузером, делают это АКА user-agent styleshee t. Пользовательский элемент не имеет встроенных стилей CSS, потому что он не определен в таблице стилей пользовательского агента, поэтому он ведет себя как встроенный элемент, как показано в приведенном ниже примере, но мы исправим это позже.

Если встроенный элемент расширяет HTMLElement, то этот элемент называется автономным настраиваемым элементом. С другой стороны, если встроенный элемент расширяет встроенный элемент, например HTMLDivElement, как показано выше, он называется настраиваемым встроенным элементом. Просто чтобы вы знали, если кто-то спросит.

Жизненный цикл настраиваемого элемента

Подобно жизненному циклу компонента в React или Angular, наш пользовательский анонимный элемент также имеет жизненный цикл, и мы можем подключиться к событию жизненного цикла и выполнить некоторые действия. Когда мы определяем класс настраиваемого элемента, у нас могут быть методы жизненного цикла, которые будут вызываться на протяжении всего жизненного цикла настраиваемого элемента. Ниже приведены несколько важных методов жизненного цикла, а вот полный список здесь.

  • connectedCallback (): этот обратный вызов вызывается, когда (экземпляр) пользовательского элемента добавляется в DOM. Здесь мы можем выполнять операции DOM с настраиваемым элементом, например добавлять новых дочерних элементов.
  • disconnectedCallback (): этот обратный вызов вызывается, когда (экземпляр) пользовательского элемента удаляется в DOM. Здесь мы можем выполнить некоторые операции по очистке, например отправить запрос AJAX.
  • attributeChangedCallback ( attrName , oldVal , newVal ): когда атрибут добавляется или удаляется, а также при изменении значения атрибута. Нам нужен метод получения observedAttributes в классе, чтобы это работало. Этот метод получения должен возвращать массив атрибутов для наблюдения. Здесь мы можем выполнять некоторые операции стилей CSS или DOM на основе значения атрибута.

this внутри вышеуказанных методов жизненного цикла указывает на экземпляр класса, который является не чем иным, как user-card элементом DOM.

Используя вышеуказанные методы, мы можем построить наш элемент user-card с реализацией ниже. Прямо сейчас мы просто записываем сообщение в консоль всякий раз, когда вызываются эти методы.

class UserCard extends HTMLElement {
  constructor() {
    super();
    
    console.log('constructor()');
  }
  
  connectedCallback() {
    console.log('connectedCallback()');
  }
  
  disconnectedCallback() {
    console.log('disconnectedCallback()');
  }
  static get observedAttributes() {
    return [ 'name', 'some-other-attribute' ];
  }
  
  attributeChangedCallback(name, oldVal, newVal) {
    console.log('attributeChangedCallback()', name, oldVal, newVal);
  }
}
// define user card element
customElements.define('user-card', UserCard);

В приведенном выше примере мы не сделали ничего сложного. Мы создали контейнер, в котором размещены три кнопки для добавления и удаления элемента user-card, добавления и удаления атрибута name и, наконец, для изменения значения атрибута name. На основе действия нажатия кнопки вызывается другой метод жизненного цикла нашего элемента user-card, как вы можете видеть в консоли.

Давайте заполним наш элемент user-card некоторыми элементами HTML и добавим несколько стилей CSS, чтобы он выглядел профессионально.

В приведенном выше примере, используя некоторые простые методы создания и управления элементами DOM, мы заполнили user-card элемент. Мы также использовали некоторые классы CSS, чтобы на них было удобнее смотреть. Ничего сложного.

Использование элемента ‹template›

Но недостаток здесь в том, что нам нужно выполнять заполнение всех элементов в JavaScript. Этого можно избежать, используя элемент template. template - это встроенный элемент, который обертывает HTML, который браузер не будет рисовать на экране, что делает его идеальным для хранения HTML, который будет извлечен в JavaScript.

В приведенном выше примере мы создали template обертки для элементов, которые мы хотим заполнить в карточке. В JavaScript мы извлекаем содержимое шаблона, используя свойство .content элемента template, которое возвращает объект типа DocumentFragment. Используя cloneNode(true), мы клонируем содержимое элемента documentFragmenet вместе с его внутренним содержимым.

Интерфейс DocumentFragment используется для создания document подобных объектов, которые можно использовать для хранения любых элементов DOM, используя для этого appendChild или append методы. Вы можете создать его экземпляр с помощью функции document.createDocumentFragment(). Как правило, он используется в качестве контейнера для сборки внутри него дерева DOM и использования его для добавления в существующий элемент DOM. Подробнее читайте в MDN.

Динамическое заполнение данных

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

Мы также можем использовать элемент slot для заполнения данных в шаблоне, но на данный момент он поддерживается не всеми браузерами. Подробнее читайте в MDN.

В приведенном выше примере мы добавили некоторые атрибуты к элементу user-card и, используя эти значения атрибутов, мы изменяем содержимое template. На данный момент вы можете иметь сколько угодно user-card элементов. Вы также должны использовать attributeChangedCallback для обновления user-card при изменении значения атрибута.

Использование Shadow DOM

До сих пор мы узнали, как создать настраиваемый элемент HTML и заполнить его шаблоном и настраиваемыми данными. Мы подошли к тому моменту, когда нам нужно обсудить инкапсуляцию.

На данный момент стили user-card CSS определены глобально и основаны на имени тега элемента. Нам нужно изменить это, потому что, если бы нам пришлось изменить имя тега, нам также нужно было бы изменить CSS. Мы можем сделать его на основе классов, тогда есть вероятность конфликта имен классов, что означает, что любой CSS на странице, использующий то же имя класса, может повлиять на макет user-card.

Чтобы решить все эти проблемы, мы должны использовать Shadow DOM. Shadow DOM - огромная тема для обсуждения, поэтому я рекомендую вам сначала прочитать этот документ MDN. Но в двух словах, Shadow DOM - это дерево DOM с родительским элементом (AKA Shadow Root), который прикреплен к элементу HTML (AKA Shadow Host). Это объясняется на скриншоте ниже из MDN.

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

Поскольку для теневого корня требуется теневой хост, он создается только с использованием метода element.attachShadow(), где element - это теневой хост.

Давайте перенесем эту теорию в наш user-card элемент. Мы прикрепим Shadow Root к элементу user-card и заполним его элементами из шаблона.

В приведенном выше примере мы прикрепили Shadow Root к нашему элементу user-card с помощью метода this.attachedShadow. Мы передали дополнительную конфигурацию {mode: 'open'}, что означает, что JavaScript вне конструктора элемента разрешает доступ к Shadow DOM.

Мы не использовали connectedCallback метод жизненного цикла, описанный выше, поскольку мы не выполняем никаких операций DOM с основными элементами DOM в документе.

Как видно из приведенного выше снимка экрана, элемент user-card содержит элемент Shadow Root с режимом открытый. Поскольку режим открыт, мы можем получить доступ к его содержимому извне.

Теневой DOM полностью инкапсулирует свое DOM-дерево, поэтому ross.firstChild возвращает null, поскольку у него нет дочерних элементов в обычном DOM-дереве. Вы можете получить доступ к дочерним элементам только через Shadow Root, и чтобы получить Shadow Root, нам нужно использовать свойство element.shadowRoot, которое его возвращает. Используя shadowRoot, мы можем получить доступ к инкапсулированному дереву DOM.

Если вы не хотите, чтобы кто-либо имел доступ к дереву Shadow DOM, установите mode в значение closed, и element.shadowRoot просто вернет null, что сделает невозможным доступ к Shadow DOM.

Как мы видим, почему-то наш элемент user-card выглядит не очень хорошо. Так что случилось? Это произошло потому, что ни один из глобально определенных стилей CSS не будет работать внутри Shadow DOM. Это означает, что нам нужно поместить все стили в наш Shadow DOM.

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

Псевдоселектор :host соответствует Теневому узлу, а :host(.some-class) соответствует Теневому узлу с именем класса .some-class. Вы можете увидеть список псевдо-выбора в веб-компонентах на MDN.

- - И мы только что создали веб-компонент. - -

Сегодня мы многое узнали о веб-компонентах, но приходило ли вам в голову, что мы используем веб-компоненты в течение некоторого времени? Вы знаете об элементах video или audio? Элемент video записывается так.

<video controls width="250">
    <source src="http://localhost/flower.mp4" type="video/mp4">
</video>

Вы когда-нибудь задумывались, почему этот простой фрагмент кода создает такой сложный видеоплеер с информацией и элементами управления? Где для этого написан код? Что ж, теперь вы знаете. video и audio - это встроенные веб-компоненты, и мы можем видеть, откуда берутся скрытые элементы.

Несмотря на то, что веб-компонент является довольно мощным инструментом, он поддерживается не всеми браузерами. Вы можете посетить webcomponents.org, чтобы увидеть, как браузер поддерживает веб-компоненты и полифилы.

Вы можете прочитать эту замечательную статью на тему Разработчики Google, чтобы узнать еще более сложный, но красивый мир веб-компонентов.