strokoff

Полное погружение в веб-компоненты в 2023 году

Погружение в веб-компоненты с webislife.ru

У нас всех есть проекты, над которыми мы не стали продолжать работать. Код стал неуправляемым, область применения расширилась, быстрые фиксы стали применяться поверх других фиксов, а структура кода рухнула под тяжестью спаггетти, программирование может быть грязным делом.

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

Применение модульных концепций к одному ЯП может быть простым, но веб-разработка требует разнообразного сочетания технологий. Браузеры анализируют HTML, CSS, JavaScript для отображения одной страницы, ее стилей и функциональности. И эти языки не так легко смешивать потому что:

Связанный код может быть разделен между тремя или более файлами и глобальные стили и объекты JavaScript могут неожиданным образом мешать друг другу.

Эти проблемы дополняют проблемы, с которыми сталкиваются языковые среды выполнения, платформы, базы данных и другие зависимости, используемые на сервере.

Что такое веб-компоненты?

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

Рассмотрим HTML-тег video. Получив URL-адрес, пользователь может использовать такие элементы управления, как воспроизведение, пауза, перемотка назад\вперед и регулировка громкости.

В таком варианте нам доступны стили и функциональность тега video, мы можем управлять его CSS свойствами, а также обращаться к нему через WEB api. Также Внутри других тегов мы можем размещать другие теги video и они не будут конфликтовать.

Что делать, если вам потребуется собственный пользовательский функционал? Например, элемент показывающий количество слов на странице? HTML-тега <wordcount> не существует, как и если бы мы захотели добавить функциональность лайков, нам пришлось бы разработать свой wc-likes

Такие фреймворки, как React или Vue.js позволяют разработчикам создавать веб-компоненты, в которых контент, стиль и функциональность могут быть определены в одном файле JavaScipt. Они решают многие сложные проблемы программирования, но имейте в виду, что:

Вы должны научиться использовать эту структур и обновлять свой код по мере развития фреймворка, иначе станете просто не актуальны и через год современные разработчики уже не захотят устраиваться на ваш устаревающий стек. 

Компонент написанный на одном фреймворке, не совместим с другими фреймворками (с поправками, на такие случаи как Vue\Nuxt, react\preact и тп)

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

Стандартные веб-компоненты могут добавлять функциональные возможности браузера, чего трудно достичь только с помощью JavaScript (например, Shadow DOM, Canvas) К счастью разработчики популярных фреймворков разрабатывают их с оглядкой на общие веб-стандарты, потребуется еще время чтобы и веб-компоненты обрели свой стандартизированный образ.

Краткая история веб-компонентов

После нескольких фальстартов связанных больше с поставщиками технологий и браузеров, концепция стандартных веб-компонентов была впервые представлена Алексом Расселом на конференции Fronteers в 2011 году. Библиотека Polymer от google (полифилл, основанный на текущих предложениях) появилась двумя годами позже, но ранние реализации так и не увидели свет в Chrome и Safari до 2016 года

Разработчикам браузеров требовалось время, чтобы обсудить детали, веб-компоненты были представлены в Firefox в 2018 году и Edge 2020 году (когда Microsoft перешла на движок Chromium)

Понятно, что немногие разработчики хотели или могли внедрить веб-компоненты, но мы, наконец, достигли хорошего уровня поддержки браузеров с достаточно стабильным и стандартизированным API. Как и везде, не все конечно же идеально, но веб-компоненты становятся все более жизнеспособной альтернативой компонентам на основе фреймворков. И наш блог webislife еще одно тому доказательство.

Даже если вы пока не хотите или не готовы отказываться от своего любимого фреймворка, веб-компоненты уже совместимы со всеми фреймворками, а API-интерфейсы будут поддерживаться долгие годы.

Начало работы с веб-компонентами

Веб-компоненты — это настраиваемые элементы HTML, такие как <hello-world></hello-world>. Имя должно содержать тире, чтобы никогда не конфликтовать с официальными HTML тегами.

Первым делом вы должны определить класс ES2015 для управления элементом. Его можно назвать как угодно, но для поддержания традиций будем называть его HelloWorld. Он должен расширять интерфейс HTMLElement, который представляет свойства и методы по умолчанию для каждого элемента HTML.

Примечание: Firefox позволяет расширять определенные HTML элементы. Такие как HTMLParagraphElement, HTMLImageElement или HTMLButtonElement. Это не поддерживается в других браузерах и не позволяет создавать Shadow DOM.

Чтобы сделать что-то полезное, классу требуется метод с именем connectedCallback(), который вызывается при добавления элемента в DOM дерево HTML документа

class HelloWorld extends HTMLElement {

  // подключаем логику в компонент
  connectedCallback() {
    this.textContent = 'Привет Мир!';
  }

}

В этом примере, мы устанавливаем textContent нашего веб-компонента равному Hello World

Класс должен быть зарегистрирован в CustomElementRegistry, чтобы определить его как обработчик для элемента:

customElements.define( 'hello-world', HelloWorld );

Теперь браузер связывает элемент <hello-world> с классом HelloWorld при загрузке  JavaScript (например, <script type="module" src="./helloworld.js"></script>).

Поздравляю, только что мы сделали свой первый веб-компонент!

Этот компонент может быть оформлен в CSS, как и любой другой элемент:

hello-world {
  font-weight: bold;
  color: red;
}

Добавление атрибутов

Этот компонент бесполезен, так как в любом случае выводится один и тот же текст. Как и любой другой элемент, мы можем добавить к нему HTML атрибуты:

<hello-world name="Иван"></hello-world>

Это может переопределить текст, поэтому «Привет, Иван!» отображается. Для этого вы можете добавить в класс HelloWorld функцию constructor(), которая запускается при создании каждого объекта. Для этого нужно:

  1. Вызвать, метод super() для инициализации родительского элемента HTMLElement
  2. И вызвать другие инициализации, в нашем случае мы определим свойство имени для которого будем выводить наше Hello сообщение
class HelloWorld extends HTMLElement {

  constructor() {
    super();
    this.name = 'Мир';
  }

  // еще код...

Веб-компонент заботится только об атрибуте имени. Статическое свойство visibleAttributes() должно возвращать массив свойств для наблюдения:

// component attributes
static get observedAttributes() {
  return ['name'];
}

Метод attributeChangedCallback() вызывается, когда атрибут определен в HTML или изменен с помощью JavaScript. Ему передаются имя свойства, старое и новое значение:

// attribute change
attributeChangedCallback(property, oldValue, newValue) {

  if (oldValue === newValue) return;
  this[ property ] = newValue;

}

В этом примере будет обновляться только свойство имени, но при необходимости вы можете добавить дополнительные свойства.

Настроим сообщение в методе connectCallback():

// connect component
connectedCallback() {

  this.textContent = `Привет ${ this.name }!`;

}

Методы жизненного цикла

Браузер автоматически вызывает шесть методов на протяжении всего жизненного цикла состояния веб-компонента. Полный список приведен здесь, хотя первые четыре вы уже видели в приведенных выше примерах:

1. constuctor()
Он вызывается при первой инициализации компонента. Он должен вызывать super() и может устанавливать любые значения по умолчанию или выполнять другие процессы предварительного рендеринга.

2. static observedAttributes()

статические наблюдаемые observedAttributes() Возвращает массив атрибутов, которые будет отслеживать браузер.

3. attributeChangedCallback(propertyName, oldValue, newValue)

attributeChangedCallback (имя свойства, старое значение, новое значение) Вызывается при изменении наблюдаемого атрибута. Те, которые определены в HTML, передаются немедленно, но JavaScript может их модифицировать:

document.querySelector('hello-world').setAttribute('name', 'Everyone');

В этом случае методу может потребоваться инициировать повторную визуализацию.

4. connectedCallback()
Эта функция вызывается, когда веб-компонент добавляется к DOM дереву документа. Он должен запускать любой необходимый рендеринг.

5. disconnectedCallback()
Он вызывается, когда веб-компонент удаляется из DOM дерева документа. Это может быть полезно, если вам нужно очистить, например, удалить сохраненное состояние или прервать запросы Ajax, а также убрать внешние слушатели событий

6. adoptedCallback()
Эта функция вызывается при перемещении веб-компонента из одного документа в другой. Вы можете найти для этого применение, хотя я изо всех сил пытался придумать какие-либо случаи! Вам когда-нибудь приходилось перемещать node элемент между разными window ?

Как веб-компоненты взаимодействуют с другими элементами

Веб-компоненты предлагают некоторые уникальные функции, которых нет в фреймворках JavaScript.

Shadow DOM

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

Shadow DOM решает эту проблему инкапсуляции, присоединяя отдельный DOM к веб-компоненту с помощью:

const shadow = this.attachShadow({ mode: 'closed' });

Режимами могут быть

1.«open» — JavaScript на внешней странице может получить доступ к Shadow DOM (используя Element.shadowRoot) или

2.«closed» — доступ к Shadow DOM возможен только из веб-компонента.

Shadow DOM можно манипулировать, как и любым другим элементом DOM:

connectedCallback() {

  const shadow = this.attachShadow({ mode: 'closed' });

  shadow.innerHTML = `
    <style>
      p {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>

    <p>Hello ${ this.name }!</p>`;

}

Теперь компонент отображает текст «Привет» внутри элемента <p> и стилизует его. Его нельзя изменить с помощью JavaScript или CSS вне компонента, хотя некоторые стили, такие как шрифт и цвет, наследуются со страницы, поскольку они не были определены явно.

Стили, относящиеся к этому веб-компоненту, не могут влиять на другие абзацы на странице или даже на другие компоненты <hello-world>.

Обратите внимание, что селектор CSS :host может стилизовать внешний элемент <hello-world> из веб-компонента:

:host {
  transform: rotate(180deg);
}

Вы также можете установить стили, которые будут применяться, когда элемент использует определенный класс, например. <hello-world class="rotate90">:

:host(.rotate90) {
  transform: rotate(90deg);
}

HTML шаблоны

Определение HTML внутри скрипта может стать непрактичным для более сложных веб-компонентов. Шаблон позволяет вам определить фрагмент HTML на вашей странице, который может использовать ваш веб-компонент. Это имеет несколько преимуществ:

1. Вы можете настроить HTML-код, не переписывая строки внутри вашего JavaScript.

2. Компоненты можно настраивать без необходимости создавать отдельные классы JavaScript для каждого типа. HTML проще определить в HTML — и его можно изменить на сервере или клиенте до того, как компонент отобразится.

3. Шаблоны определяются в теге <template>, и удобно им назначать идентификатор, чтобы мы могли ссылаться на него в классе компонента. В этом примере три абзаца для отображения сообщения «Привет»:

<template id="hello-world">

  <style>
    p {
      text-align: center;
      font-weight: normal;
      padding: 0.5em;
      margin: 1px 0;
      background-color: #eee;
      border: 1px solid #666;
    }
  </style>

  <p class="hw-text"></p>
  <p class="hw-text"></p>
  <p class="hw-text"></p>

</template>

Класс веб-компонента может получить доступ к этому шаблону, получить его содержимое и клонировать элементы, чтобы убедиться, что вы создаете уникальный фрагмент DOM везде, где он используется:

const template = document.getElementById('hello-world').content.cloneNode(true);

DOM можно изменить и добавить непосредственно в Shadow DOM:

connectedCallback() {

  const

    shadow = this.attachShadow({ mode: 'closed' }),
    template = document.getElementById('hello-world').content.cloneNode(true),
    hwMsg = `Hello ${ tkshis.name }`;

  Array.from( template.querySelectorAll('.hw-text') )
    .forEach( n => n.textContent = hwMsg );

  shadow.append( template );

}

Слоты для шаблонов

Слоты позволяют настроить шаблон. Предположим, вы хотите использовать свой веб-компонент <hello-world>, но поместить сообщение в заголовок <h1> в Shadow DOM. Вы можете написать этот код:

<hello-world name="Craig">

  <h1 slot="msgtext">Здравствуйте по умолчанию :) !</h1>

</hello-world>

(Обратите внимание на атрибут слота.)

При желании вы можете добавить другие элементы, такие как еще один абзац:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Слоты теперь могут быть реализованы в вашем шаблоне:

<template id="hello-world">

  <slot name="msgtext" class="hw-text"></slot>

  <slot></slot>

</template>
Атрибут слота элемента, для которого установлено значение «msgtext» (<h1>), вставляется в точку, где есть <slot> с именем «msgtext». <p> не имеет назначенного имени слота, но он используется в следующем доступном безымянном <slot>. По сути, шаблон становится:

<template id="hello-world">

  <slot name="msgtext" class="hw-text">
    <h1 slot="msgtext">Hello Default!</h1>
  </slot>

  <slot>
    <p>Этот текст часть веб-компонента.</p>
  </slot>

</template>

Не все так просто на самом деле. Элемент <slot> в Shadow DOM указывает на вставленные элементы. Вы можете получить к ним доступ, только найдя <slot>, а затем используя метод .assignedNodes() для возврата массива внутренних дочерних элементов. Обновленный метод connectCallback():

/

connectedCallback() {

  const
    shadow = this.attachShadow({ mode: 'closed' }),
    hwMsg = `Hello ${ this.name }`;

  // добавляем теневой DOM
  shadow.append(
    document.getElementById('hello-world').content.cloneNode(true)
  );

  // найти все слоты с классом hw-text
  Array.from( shadow.querySelectorAll('slot.hw-text') )

   // добавляем теневой DOM
    .forEach( n => n.assignedNodes()[0].textContent = hwMsg );

}

Кроме того, вы не можете напрямую стилизовать вставленные элементы, хотя вы можете ориентироваться на определенные слоты в вашем веб-компоненте:

<template id="hello-world">

  <style>
    slot[name="msgtext"] { color: green; }
  </style>

  <slot name="msgtext" class="hw-text"></slot>
  <slot></slot>

</template>

Слоты шаблонов немного необычны, но одно из преимуществ заключается в том, что ваш контент будет отображаться, если JavaScript не запустится. Этот код показывает заголовок и абзац по умолчанию, которые заменяются только при успешном выполнении класса веб-компонента:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Таким образом, вы можете внедрить какую-либо форму прогрессивного улучшения — даже если это просто сообщение «Вам нужен JavaScript»!

Декларативный Shadow DOM

В приведенных выше примерах создается Shadow DOM с использованием JavaScript. Это остается единственным вариантом, но для Chrome разрабатывается экспериментальный декларативный Shadow DOM. Это обеспечивает рендеринг на стороне сервера и позволяет избежать каких-либо сдвигов макета или вспышек нестилизованного содержимого.

<hello-world name="Craig">

  <template shadowroot="closed">
    <slot name="msgtext" class="hw-text"></slot>
    <slot></slot>
  </template>

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Эта функция недоступна ни в одном браузере, и нет гарантии, что она появится в Firefox или Safari. Вы можете узнать больше о декларативном Shadow DOM, а полифилл — это просто, но имейте в виду, что реализация может измениться.

Теневые события в DOM

Ваш веб-компонент может прикреплять события к любому элементу в Shadow DOM так же, как и в DOM страницы, например, для прослушивания событий кликов на всех внутренних дочерних элементах:

shadow.addEventListener('click', e => {

  // do something

});
Если вы не остановите распространение, событие всплывет в DOM страницы, но событие будет перенацелено. Следовательно, кажется, что он исходит из вашего пользовательского элемента, а не из элементов внутри него.

Использование веб-компонентов в других фреймворках

Любой созданный вами веб-компонент будет работать во всех средах JavaScript. Никто из них не знает и не заботится об HTML-элементах — ваш компонент <hello-world> будет обрабатываться так же, как <div> и помещаться в DOM, где будет инициализирован класс веб-компонент.

custom-elements-everywhere.com предоставляет список фреймворков и заметок о веб-компонентах. Большинство из них полностью совместимы, хотя у React.js есть некоторые проблемы. В JSX можно использовать <hello-world>:

import React from 'react';
import ReactDOM from 'react-dom';
import from './hello-world.js';

function MyPage() {

  return (
    <>
      <hello-world name="Craig"></hello-world> 
    </>
  );

}

ReactDOM.render(<MyPage />, document.getElementById('root'));

…но

1. React может передавать только примитивные типы данных в атрибуты HTML (не массивы или объекты)

2. React не может прослушивать события веб-компонентов, поэтому вы должны вручную прикрепить свои собственные обработчики.

Также в качестве примера переиспользования веб-компонентов вы можете ознакомиться с моими публикациями
1. Интегрируем wc-likes в wordpress и
2. Используем веб компонент wc-likes в vue.js\nuxt 

Критика и проблемы веб-компонентов

Веб-компоненты значительно улучшились, но некоторыми аспектами может быть сложно управлять.

Трудности стилизации

Стилизация веб-компонентов сопряжена с некоторыми проблемами, особенно если вы хотите переопределить стили области действия. Есть много решений:

  1. Избегайте использования Shadow DOM. Вы можете добавить содержимое непосредственно в свой пользовательский элемент, хотя любой другой JavaScript может случайно или злонамеренно изменить его.
  2. Используйте классы :host. Как мы видели выше, CSS с ограниченной областью действия может применять определенные стили, когда класс применяется к пользовательскому элементу.
  3. Ознакомьтесь с пользовательскими свойствами CSS (переменными). Пользовательские свойства передаются в веб-компоненты, поэтому, если ваш элемент использует var(--my-color), вы можете установить —my-color во внешнем контейнере (например, :root), и он будет использоваться.
  4. Воспользуйтесь преимуществами теневых частей. Новый селектор ::part() может стилизовать внутренний компонент с атрибутом part, т. е. <h1 part="heading"> внутри компонента <hello-world> можно стилизовать с помощью селектора hello-world::part(heading ).
  5. Передайте строку стилей. Вы можете передать ее как атрибут для вашего веб-компонента.

Ни один из них не идеален, и вам нужно спланировать, как другие пользователи смогут тщательно настроить ваш веб-компонент.

Игнорирование input элементов

Любые поля <input>, <textarea> или <select> в вашем Shadow DOM не связываются автоматически с содержащей их формой. Первые пользователи веб-компонентов добавляли скрытые поля в DOM страницы или использовали интерфейс FormData для обновления значений. Ни один из вариантов не является особенно практичным и нарушает инкапсуляцию веб-компонентов.

Новый интерфейс ElementInternals позволяет веб-компоненту подключаться к формам, чтобы можно было определить пользовательские значения и допустимость. Он реализован в Chrome, но полифилл доступен и для других браузеров. Читайте статью на нашем сайте ElementInternals и ассоциированные с формой пользовательские элементы

Для демонстрации вы создадим базовый компонент <input-age name="your-age"></input-age>. Класс должен иметь статическое значение formAssociated, установленное true и, необязательно, метод formAssociatedCallback() может быть вызван, когда внешняя форма связана:

// <input-age> web component
class InputAge extends HTMLElement {

  static formAssociated = true;

  formAssociatedCallback(form) {
    console.log('form associated:', form.id);
}

Теперь конструктор должен запустить метод attachInternals(), который позволяет компоненту взаимодействовать с формой и другим кодом JavaScript, который хочет проверить значение или проверку:

 constructor() {

    super();
    this.internals = this.attachInternals();
    this.setValue('');

  }

  // set form value

  setValue(v) {

    this.value = v;

    this.internals.setFormValue(v);

}

Метод setFormValue() ElementInternal устанавливает значение элемента для родительской формы, инициализированной пустой строкой здесь (ему также может быть передан объект FormData с несколькими парами имя/значение). Другие свойства и методы включают в себя:

  1. form: родительская форма
  2. label: массив элементов, которые маркируют компонент
  3. Constraint Validation API параметры API проверки ограничений, такие как willValidate, checkValidity и validationMessage.

Метод connectCallback() создает Shadow DOM, как и раньше, но также должен отслеживать изменения в поле, поэтому можно запустить setFormValue():

 connectedCallback() {

    const shadow = this.attachShadow({ mode: 'closed' });

    shadow.innerHTML = `
      <style>input { width: 4em; }</style>
      <input type="number" placeholder="age" min="18" max="120" />`;

    // monitor input values
    shadow.querySelector('input').addEventListener('input', e => {
      this.setValue(e.target.value);
    });

}

Теперь вы можете создать HTML-форму, используя этот веб-компонент, который действует аналогично другим полям формы:

<form id="myform">

  <input type="text" name="your-name" placeholder="name" />

  <input-age name="your-age"></input-age>

  <button>submit</button>

</form>

Резюме

Веб-компоненты изо всех сил пытались добиться согласия и принятия в то время, когда фреймворки JavaScript выросли в статусе и возможностях. Если вы переходите с React, Vue.js или Angular, веб-компоненты могут выглядеть сложными и неуклюжими, особенно если вам не хватает таких функций, как привязка данных и управление состоянием.

Есть недостатки, которые нужно сгладить, но будущее веб-компонентов светлое. Они не зависят от фреймворка, легковесны, быстры и могут реализовывать функции, которые были бы невозможны в одном только JavaScript.

Десять лет назад мало кто стал бы работать с сайтом без jQuery, но разработчики браузеров взяли отличные части и добавили нативные альтернативы (такие как querySelector). То же самое произойдет и с фреймворками JavaScript, и веб-компоненты — это первый пробный шаг.

У вас есть вопросы по использованию веб-компонентов? Давайте поговорим об этом в комментариях Или нашем телеграмм канале.

Если вам интересны веб-компоненты — рекомендую ознакомиться с нашим циклом статей про веб-компоненты. На данный момент это самый большой и актуальный список статей на русском по веб-компонентам в ру сегменте. Буду рад добавить и обновить цикл вашими статьями и примерами. Enjoy!)


Последняя редакция 2 марта, 2023 в 07:03