Это руководство представляет собой введение в стандартные веб-компоненты, написанные без использования инфраструктуры JavaScript. Вы узнаете, что это такое и как адаптировать их для своих собственных веб-проектов. Необходимо хорошее знание HTML5, CSS и JavaScript.
- Что такое веб-компоненты (Web Components)?
- Как работают веб-компоненты (Web Components)?
- Ваш первый веб-компонент
- Создать <word-count> компонент
- Конструктор WordCount
- Прикрепите теневой DOM
- Наблюдение за атрибутами WordCount
- WordCount Визуализация
- Раскрытие теневого DOM
- Объемный стиль
- Переопределение стилей теней
- События теневой DOM
- Использование слотов для шаблонов
- Декларативный теневой DOM
- Включающие входы
- Будущее за веб-компонентами?
Что такое веб-компоненты (Web Components)?
В идеале ваш проект разработки должен использовать простые независимые модули. У каждого должна быть четкая единственная ответственность. Код инкапсулирован: нужно только знать, что будет выводиться при заданном наборе входных параметров. Другим разработчикам не нужно проверять реализацию (если, конечно, нет ошибки).
Большинство языков поощряют использование модулей и повторно используемого кода, но разработка браузера требует сочетания HTML, CSS и JavaScript для отображения содержимого, стилей и функциональности. Связанный код может быть разделен на несколько файлов и может неожиданно конфликтовать.
Фреймворки и библиотеки JavaScript, такие как React, Vue, Svelte и Angular, облегчили некоторые головные боли, представив свои собственные компонентные методы. Связанные HTML, CSS и JavaScript можно объединить в один файл. К сожалению:
- другое дело учиться
- фреймворки развиваются, и обновления часто требуют рефакторинга кода или даже его переписывания.
- компонент, написанный в одном фреймворке, обычно не работает в другом, и
- фреймворки могут быть тяжелыми и ограниченными возможностями JavaScript.
Десять лет назад многие из понятий, введенных JQuery были добавлены в браузеры (такие как querySelector, ближайший, ClassList, и так далее). Сегодня поставщики внедряют веб-компоненты, которые работают в браузере без фреймворка.
На это ушло время. Алекс Рассел сделал первоначальное предложение в 2011 году. Полимерный фреймворк Google (polyfill) появился в 2013 году, но прошло три года, прежде чем его собственные реализации появились в Chrome и Safari. Были некоторые напряженные переговоры, но Firefox добавил поддержку в 2018 году, а затем Edge (версия Chromium) в 2020 году.
Как работают веб-компоненты (Web Components)?
Рассмотрим HTML5 элементы, которые позволяют пользователям воспроизводить, приостанавливать, перематывать и перематывать мультимедиа с помощью ряда внутренних кнопок и элементов управления. По умолчанию браузер обрабатывает макет, стиль и функциональность.
Веб-компоненты позволяют добавлять собственные пользовательские элементы HTML — например, <word-count>тег для подсчета количества слов на странице. Имя элемента должно содержать дефис ( -), чтобы гарантировать, что он никогда не будет конфликтовать с официальным элементом HTML.
Затем для вашего пользовательского элемента регистрируется класс JavaScript ES2015. Он может добавлять элементы DOM, такие как кнопки, заголовки, абзацы и т. Д. Чтобы они не конфликтовали с остальной частью страницы, вы можете прикрепить их к внутренней Shadow DOM, имеющей собственные стили с ограниченными областями. Вы можете думать об этом как о мини <iframe>, хотя свойства CSS, такие как шрифты и цвета, наследуются через каскад.
Наконец, вы можете добавлять контент в Shadow DOM с помощью повторно используемых HTML-шаблонов, которые предлагают некоторую конфигурацию с помощью <slot>тегов.
Стандартные веб-компоненты рудиментарны по сравнению с фреймворками. Они не включают в себя такие функции, как привязка данных и управление состоянием, но у веб-компонентов есть ряд неоспоримых преимуществ:
- они легкие и быстрые
- также они могут реализовать функциональность, невозможную только в JavaScript (например, Shadow DOM)
- они работают в любом фреймворке JavaScript
- они будут поддерживаться годами — если не десятилетиями
Ваш первый веб-компонент
Для начала добавьте <hello-world></hello-world> элемент на любую веб-страницу. (Закрывающий тег важен: вы не можете определить самозакрывающийся <hello-world />тег.)
Создайте файл сценария с именем hello-world.jsи загрузите его с той же HTML-страницы (модули ES автоматически откладываются, поэтому их можно разместить где угодно — но чем раньше на странице, тем лучше):
<script type="module" src="./hello-world.js"></script> <hello-world></hello-world>
Создайте HelloWorldкласс в своем файле сценария:
// <hello-world> Web Component class HelloWorld extends HTMLElement { connectedCallback() { this.textContent = 'Hello, World!'; } }
Веб-компоненты должны расширять интерфейс HTMLElement, который реализует свойства и методы по умолчанию для каждого элемента HTML.
Примечание. Firefox может расширять определенные элементы, такие как HTMLImageElementи HTMLButtonElement. Однако они не поддерживают Shadow DOM, и эта практика не поддерживается в других браузерах.
Браузер запускает connectedCallback()метод всякий раз, когда пользовательский элемент добавляется в документ. В этом случае изменяется внутренний текст. (Теневой DOM не используется.)
Класс должен быть зарегистрирован с вашим настраиваемым элементом в CustomElementRegistry :
// register <hello-world> with the HelloWorld class customElements.define( 'hello-world', HelloWorld );
Загрузите страницу, и появится надпись «Hello World». Новый элемент можно стилизовать в CSS с помощью hello-world {… }селектора:
Создать <word-count> компонент
<word-count> Компонент является более сложным. Этот пример может генерировать количество слов или количество минут, чтобы прочитать статью. Интернационализация API можно использовать для вывода цифр в правильном формате.
Могут быть добавлены следующие атрибуты элемента:
- round=»N»: округляет количество слов до ближайшего N (по умолчанию 10)
- minutes: показывает минуты чтения, а не количество слов (по умолчанию false)
- wpm=»M»: количество слов, которое человек может прочитать за минуту (по умолчанию 200)
- locale=»L»: языковой стандарт, например en-USили fr-FR(по умолчанию из <html> langатрибута или, en-USесли он недоступен)
На <word-count>страницу можно добавить любое количество элементов. Например:
<p> This article has <word-count round="100"></word-count> words, and takes <word-count minutes></word-count> minutes to read. </p>
Конструктор WordCount
Новый WordCountкласс создается в модуле JavaScript с именем word-count.js:
class WordCount extends HTMLElement { // cached word count static words = 0; constructor() { super(); // defaults this.locale = document.documentElement.getAttribute('lang') || 'en-US'; this.round = 10; this.wpm = 200; this.minutes = false; // attach shadow DOM this.shadow = this.attachShadow({ mode: 'open' }); }
Статическое wordsсвойство хранит количество слов на странице. Он рассчитывается один раз и используется повторно, поэтому другим <word-count>элементам не нужно повторять работу.
constructor()Функция выполняется при создании каждого объекта. Он должен вызвать super()метод для инициализации родителя, HTMLElementа затем при необходимости установить другие значения по умолчанию.
Прикрепите теневой DOM
Конструктор также определяет теневой DOM attachShadow()и сохраняет ссылку в shadowсвойстве, поэтому его можно использовать в любое время.
modeМожет быть установлен:
- «open»: JavaScript на внешней странице может получить доступ к Shadow DOM с помощью Element.shadowRoot или
- «closed»: Shadow DOM недоступен для внешней страницы
Этот компонент добавляет простой текст, и внешние изменения не критичны. Использование openдостаточно, чтобы другой JavaScript на странице мог запрашивать контент. Например:
const wordCount = document.querySelector('word-count').shadowRoot.textContent;
Наблюдение за атрибутами WordCount
К этому веб-компоненту можно добавить любое количество атрибутов, но он касается только четырех, перечисленных выше. Статическое observedAttributes()свойство возвращает массив свойств для наблюдения:
// component attributes static get observedAttributes() { return ['locale', 'round', 'minutes', 'wpm']; }
attributeChangedCallback() Метод вызывается, когда любой из этих атрибутов установлен в HTML — формате или в JavaScript .setAttribute()методе. Ему передается имя свойства, предыдущее значение и новое значение:
// attribute change attributeChangedCallback(property, oldValue, newValue) { // update property if (oldValue === newValue) return; this[property] = newValue || 1; // update existing if (WordCount.words) this.updateCount(); }
this.updateCount();Вызов делает компонент, поэтому он может быть повторно запущен, если атрибут изменен после того, как она отображается в первый раз.
WordCount Визуализация
connectedCallback()Метод вызывается, когда веб — компоненты добавляются к объектной модели документа. Он должен запустить любой требуемый рендеринг:
// connect component connectedCallback() { this.updateCount(); }
Две другие функции доступны в течение жизненного цикла веб-компонента, хотя здесь они не нужны:
- disconnectedCallback(): вызывается, когда веб-компонент удаляется из объектной модели документа. Он может очистить состояние, прервать запросы Ajax и т. Д.
- adoptedCallback(): вызывается, когда веб-компонент перемещается из одного документа в другой. Я никогда не находил ему применения!
updateCount()Метод вычисляет количество слов, если это не было сделано раньше. Он использует содержимое первого <main>тега или страницы, <body>когда оно недоступно:
// update count message updateCount() { if (!WordCount.words) { // get root <main> or </body> let element = document.getElementsByTagName('main'); element = element.length ? element[0] : document.body; // do word count WordCount.words = element.textContent.trim().replace(/\s+/g, ' ').split(' ').length; }
Затем он обновляет Shadow DOM счетчиком слов или минут (если minutesатрибут установлен):
// locale const localeNum = new Intl.NumberFormat( this.locale ); // output word or minute count this.shadow.textContent = localeNum.format( this.minutes ? Math.ceil( WordCount.words / this.wpm ) : Math.ceil( WordCount.words / this.round ) * this.round ); } } // end of class
Затем регистрируется класс веб-компонента:
// register component window.customElements.define( 'word-count', WordCount );
Раскрытие теневого DOM
Теневой DOM манипулируют, как и любым другим элементом DOM, но у него есть несколько секретов и подводных камней…
Объемный стиль
Стили, установленные в Shadow DOM, привязаны к веб-компоненту. Они не могут влиять на внешние элементы страницы или быть изменены извне (хотя некоторые свойства, такие как font-family, цвета и интервал, могут передаваться каскадом):
const shadow = this.attachShadow({ mode: 'closed' }); shadow.innerHTML = ` <style> p { font-size: 5em; text-align: center; font-weight: normal; color: red; } </style> <p>Hello!</p> `;
Вы можете настроить таргетинг на сам элемент веб-компонента с помощью :hostселектора:
:host { background-color: green; }
Стили также можно применять, когда веб-компоненту назначен определенный класс, например <my-component class=»nocolor»>:
:host(.nocolor) { background-color: transparent; }
Переопределение стилей теней
Переопределить стили с ограниченной областью видимости непросто, и, вероятно, этого не должно быть. Есть несколько вариантов, если вы хотите предложить уровень стиля потребителям вашего веб-компонента:
- :hostклассы. Как показано выше, CSS с областью видимости может применять стили, когда класс применяется к настраиваемому элементу. Это может быть полезно для предложения ограниченного выбора вариантов стиля.
- Пользовательские свойства CSS (переменные). Настраиваемые свойства каскадно переходят в веб-компоненты. Если ваш компонент использует var(—color1), вы можете установить —color1в любом родительском контейнере вплоть до :root. Возможно, вам придется избегать конфликтов имен, возможно, используя переменные пространства имен, такие как —my-component-color1.
- Используйте части тени. Селектор :: часть () может укладывать внутренний компонент с атрибутом части. Например, <h1 part=»head»>внутренний <my-component>компонент стилизован с использованием my-component::part(head) {… }.
- Передайте атрибуты стиля. Строка стилей может быть установлена в одном или нескольких атрибутах веб-компонента, которые могут применяться во время рендеринга. Он кажется немного грязным, но он сработает.
- Избегайте теневого DOM. Вы можете добавлять содержимое DOM непосредственно в свой настраиваемый элемент без использования Shadow DOM, хотя в этом случае он может конфликтовать с другими компонентами на странице или нарушать их работу.
События теневой DOM
Веб-компоненты могут присоединять события к любому элементу в теневой модели DOM так же, как и в модели страницы. Например, чтобы прослушать нажатие кнопки:
shadow.querySelector('button').addEventListener('click', e => { // do something });
Событие всплывет на внешнюю страницу DOM, если вы не предотвратите это с помощью e.stopPropagation(). Однако событие перенаправлено; внешняя страница будет знать, что это произошло с вашим настраиваемым элементом, но не знает, какой элемент Shadow DOM был целью.
Использование HTML-шаблонов
Определение HTML внутри вашего класса может быть непрактичным. Шаблоны HTML определяют фрагмент HTML на вашей странице, который может использоваться любым веб-компонентом:
- вы можете настроить HTML-код на клиенте или сервере без необходимости переписывать строки в классах JavaScript
- компоненты можно настраивать, не требуя отдельных классов JavaScript для каждого типа
HTML-шаблоны определяются внутри <template>тега. Идентификатор обычно определяется, поэтому вы можете ссылаться на него:
<template id="my-template"> <style> h1, p { text-align: center; } </style> <h1><h1> <p></p> </template>
Код класса может получить этот шаблон.contentи клонировать его, чтобы создать уникальный фрагмент DOM перед внесением изменений и обновлением Shadow DOM:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }), template = document.getElementById('my-template').content.cloneNode(true); template.querySelector('h1').textContent = 'heading'; template.querySelector('p').textContent = 'text'; shadow.append( template ); }
Использование слотов для шаблонов
Шаблоны можно настраивать с помощью <slot>. Например, вы можете создать следующий шаблон:
<template id="my-template"> <slot name="heading"></slot> <slot></slot> </template>
Затем определите компонент:
<my-component> <h1 slot="heading">default heading<h1> <p>default text</p> </my-component>
Элемент с slotатрибутом, установленным на «heading»( <h1>), вставляется в шаблон, где <slot name=»heading»>появляется. <p>Не имеет имя слота, но он используется в следующий доступный неназванный. Полученный шаблон выглядит так:
<template id="my-template"> <slot name="heading"> <h1 slot="heading">default heading<h1> </slot> <slot> <p>default text</p> </slot> </template>
На самом деле каждый <slot> указывает на свои вставленные элементы. Вы должны найти a <slot>в Shadow DOM, а затем использовать .assignedNodes()для возврата массива дочерних узлов, которые можно изменить. Например:
const shadow = this.attachShadow({ mode: 'closed' }), // append template with slots to shadow DOM shadow.append( document.getElementById('my-template').content.cloneNode(true) ); // update heading shadow.querySelector('slot[name="heading"]') .assignedNodes()[0] .textContent = 'My new heading';
Невозможно напрямую стилизовать вставленные элементы, хотя вы можете настроить таргетинг на слоты и каскадировать свойства CSS:
slot[name="heading"] { color: #123; }
Слоты немного громоздки, но содержимое отображается до запуска JavaScript вашего компонента. Их можно использовать для элементарного прогрессивного улучшения.
Декларативный теневой DOM
Теневая модель DOM не может быть построена до тех пор, пока не будет запущен ваш JavaScript. Declarative Shadow DOM — это новая экспериментальная функция, которая обнаруживает и отображает шаблон компонента на этапе синтаксического анализа HTML. Теневая модель DOM может быть объявлена на стороне сервера, и это помогает избежать сдвигов макета и мигания нестилизованного содержимого.
Компонент определяется с внутренним, <template>который имеет shadowrootнабор атрибутов, чтобы openили closedв случае необходимости:
<mycompnent> <template shadowroot="closed"> <slot name="heading"> <h1 slot="heading">default heading<h1> </slot> <slot> <p>default text</p> </slot> </template> <h1 slot="heading">default heading<h1> <p>default text</p> </my-component>
После этого Shadow DOM будет готов к запуску класса компонента; он может обновлять содержимое по мере необходимости.
Эта функция появится в браузерах на базе Chrome, но еще не готова, и нет гарантии, что она будет поддерживаться в Firefox или Safari (хотя она легко полифицируется). Для получения дополнительной информации см. Декларативную теневую DOM.
Включающие входы
<input>, <textarea>И <select>поле, используемое в тени DOM являются не связанно с содержащей формой. Некоторые авторы веб-компонентов добавляют скрытые поля к DOM внешней страницы или используют интерфейс FormData для обновления значений, но это нарушает инкапсуляцию.
Новый интерфейс ElementInternals позволяет веб-компонентам ассоциировать себя с формами. Он реализован в Chrome, но для других браузеров требуется полифил.
Допустим, вы создали <input-age name=»your-age»></input-age>компонент, который добавляет следующее поле в Shadow DOM:
<input type="number" placeholder="age" min="18" max="120" />
Класс веб-компонента должен определять статическое formAssociatedсвойство как trueи при необходимости может предоставлять formAssociatedCallback()метод, который вызывается, когда форма связана с элементом управления:
class InputAge extends HTMLElement { static formAssociated = true; formAssociatedCallback(form) { console.log('form associated:', form.id); }
Конструктор должен работать, this.attachInternals()чтобы компонент мог взаимодействовать с формой и другим кодом JavaScript. Метод ElementInternal setFormValue()устанавливает значение элемента (инициализируется пустой строкой):
constructor() { super(); this.internals = this.attachInternals(); this.setValue(''); } // set field value setValue(v) { this.value = v; this.internals.setFormValue(v); }
connectedCallback()Метод делает Shadow DOM, как и раньше, но он также должен смотреть на поле для изменения и обновления значения:
connectedCallback() { const shadow = this.attachShadow({ mode: 'closed' }); shadow.innerHTML = ` <style>input { width: 4em; }</style> <input type="number" placeholder="age" min="18" max="120" />`; // monitor age input shadow.querySelector('input').addEventListener('change', e => { this.setValue(e.target.value); }); }
ElementInternal также может предоставлять информацию о форме, метках и параметрах API проверки ограничений.
Будущее за веб-компонентами?
Если вы работаете с фреймворком JavaScript, веб-компоненты могут показаться низкоуровневыми и несколько громоздкими. Также потребовалось десятилетие, чтобы прийти к соглашению и достичь разумного уровня кросс-браузерной совместимости. Неудивительно, что разработчики не спешили их использовать.
Веб-компоненты не идеальны, но они будут развиваться, и их можно будет использовать уже сегодня. Они легкие, быстрые и предлагают функциональность, невозможную только в JavaScript. Более того, их можно использовать в любом фреймворке JavaScript.