1. Введение
Последнее обновление: 10.08.2021
Веб-компоненты
Веб-компоненты — это набор API веб-платформы, позволяющий создавать новые, многократно используемые, инкапсулированные HTML-теги для использования на веб-страницах и в веб-приложениях. Пользовательские компоненты и виджеты, созданные на основе стандартов веб-компонентов, будут работать во всех современных браузерах и могут использоваться с любой библиотекой или фреймворком JavaScript, работающим с HTML.
Что такое литература?
Lit — это простая библиотека для создания быстрых и легковесных веб-компонентов, работающих в любом фреймворке или без него. С помощью Lit вы можете создавать компоненты, приложения, дизайн-системы и многое другое, которыми можно делиться.
Lit предоставляет API для упрощения распространенных задач веб-компонентов, таких как управление свойствами, атрибутами и рендеринг.
Что вы узнаете
- Что такое веб-компонент?
- Концепции веб-компонентов
- Как создать веб-компонент
- Что такое lit-html и LitElement?
- Что делает Lit поверх веб-компонента
Что вы построите
- Простой веб-компонент для отображения лайков /дизлайков.
- Компонент веб-разработки, основанный на литературных принципах (лайк/дизлайк).
Что вам понадобится
- Любой современный браузер (Chrome, Safari, Firefox, Chromium Edge). Веб-компоненты работают во всех современных браузерах, а полифилы доступны для Microsoft Internet Explorer 11 и Microsoft Edge (не Chromium).
- Знание HTML, CSS, JavaScript и инструментов разработчика Chrome .
2. Подготовка и знакомство с игровой площадкой
Доступ к коду
На протяжении всего практического занятия будут встречаться ссылки на интерактивную площадку Lit Playground, подобные этим:
Playground — это песочница для кода, которая полностью работает в вашем браузере. Она может компилировать и запускать файлы TypeScript и JavaScript, а также автоматически разрешать импорты в модули Node.js. Например:
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://unpkg.com/lit?module';
Вы можете пройти весь учебный курс в Lit Playground, используя эти контрольные точки в качестве отправных точек. Если вы используете VS Code, вы можете использовать эти контрольные точки для загрузки начального кода для любого шага, а также для проверки своей работы.
Изучение пользовательского интерфейса освещенной игровой площадки

На скриншоте пользовательского интерфейса Lit Playground выделены разделы, которые вы будете использовать в этом практическом задании.
- Средство выбора файла. Обратите внимание на кнопку «плюс»...
- Редактор файлов.
- Предварительный просмотр кода.
- Кнопка перезагрузки.
- Кнопка загрузки.
Настройка VS Code (расширенные параметры)
Вот преимущества использования такой конфигурации VS Code:
- Проверка типа шаблона
- Интеллектуальное автозаполнение шаблонов
Если у вас уже установлены NPM, VS Code (с плагином lit- plugin) и вы знаете, как использовать эту среду, вы можете просто загрузить и запустить эти проекты, выполнив следующие действия:
- Нажмите кнопку загрузки
- Распакуйте содержимое tar-архива в отдельную директорию.
- Установите сервер разработки, способный разрешать спецификаторы модулей без указания их местоположения (команда Lit рекомендует @web/dev-server ).
- Вот пример файла
package.json
- Вот пример файла
- Запустите сервер разработки и откройте браузер (если вы используете
@web/dev-serverвы можете использоватьnpx web-dev-server --node-resolve --watch --open).- Если вы используете пример
package.jsonвыполнитеnpm run serve
- Если вы используете пример
3. Определение пользовательского элемента
Пользовательские элементы
Веб-компоненты представляют собой набор из 4 нативных веб-API. Это:
- Модули ES
- Пользовательские элементы
- Теневой ДОМ
- HTML-шаблоны
Вы уже использовали спецификацию модулей ES, которая позволяет создавать модули JavaScript с импортом и экспортом, загружаемые на страницу с помощью <script type="module"> .
Определение пользовательского элемента
Спецификация Custom Elements позволяет пользователям определять собственные HTML-элементы с помощью JavaScript. Имена должны содержать дефис ( - ), чтобы отличать их от стандартных элементов браузера. Очистите файл index.js и определите класс пользовательского элемента:
index.js
class RatingElement extends HTMLElement {}
customElements.define('rating-element', RatingElement);
Пользовательский элемент определяется путем связывания класса, расширяющего HTMLElement с именем тега, состоящим из двух частей (дефиса). Вызов метода customElements.define указывает браузеру связать класс RatingElement с тегом 'rating-element' . Это означает, что каждый элемент в вашем документе с именем <rating-element> будет связан с этим классом.
Разместите элемент <rating-element> в теле документа и посмотрите, что отобразится.
index.html
<body>
<rating-element></rating-element>
</body>
Теперь, взглянув на результат, вы увидите, что ничего не отобразилось. Это ожидаемо, поскольку вы не указали браузеру, как отображать <rating-element> . Вы можете убедиться в успешности определения пользовательского элемента, выбрав <rating-element> в селекторе элементов в инструментах разработчика Chrome и выполнив в консоли следующую команду:
$0.constructor
В результате должно получиться следующее:
class RatingElement extends HTMLElement {}
Жизненный цикл пользовательского элемента
Пользовательские элементы поставляются с набором хуков жизненного цикла. К ним относятся:
-
constructor -
connectedCallback -
disconnectedCallback -
attributeChangedCallback -
adoptedCallback
constructor вызывается при первом создании элемента: например, путем вызова document.createElement('rating-element') или new RatingElement() . Конструктор — хорошее место для настройки элемента, но обычно считается плохой практикой выполнять манипуляции с DOM в конструкторе из-за соображений производительности при «запуске» элемента.
Функция connectedCallback вызывается при прикреплении пользовательского элемента к DOM. Обычно именно здесь происходят первоначальные манипуляции с DOM.
Функция disconnectedCallback вызывается после удаления пользовательского элемента из DOM.
Функция attributeChangedCallback(attrName, oldValue, newValue) вызывается при изменении любого из указанных пользователем атрибутов.
Функция adoptedCallback вызывается, когда пользовательский элемент переносится из другого documentFragment в основной документ с помощью adoptNode , например, в HTMLTemplateElement .
Отображение DOM
Теперь вернитесь к пользовательскому элементу и свяжите с ним некоторый DOM-элемент. Установите содержимое элемента после его привязки к DOM:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
this.innerHTML = `
<style>
rating-element {
display: inline-flex;
align-items: center;
}
rating-element button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
В constructor вы сохраняете свойство экземпляра с именем rating у элемента. В connectedCallback вы добавляете дочерние элементы DOM к <rating-element> , чтобы отобразить текущий рейтинг, а также кнопки "палец вверх" и "палец вниз".
4. Теневой DOM
Почему именно Shadow DOM?
На предыдущем шаге вы заметите, что селекторы в вставленном вами теге style выбирают любой элемент рейтинга на странице, а также любую кнопку. Это может привести к тому, что стили выйдут за пределы элемента и выберут другие узлы, которые вы, возможно, не собираетесь стилизовать. Кроме того, другие стили вне этого пользовательского элемента могут непреднамеренно стилизовать узлы внутри вашего пользовательского элемента. Например, попробуйте добавить тег style в заголовок основного документа:
index.html
<!DOCTYPE html>
<html>
<head>
<script src="./index.js" type="module"></script>
<style>
span {
border: 1px solid red;
}
</style>
</head>
<body>
<rating-element></rating-element>
</body>
</html>
В вашем выводе вокруг тега <span> с рейтингом должна быть красная рамка. Это тривиальный случай, но отсутствие инкапсуляции DOM может привести к более серьезным проблемам в сложных приложениях. Вот тут-то и пригодится Shadow DOM.
Прикрепление теневого корня
Прикрепите к элементу теневой корень (Shadow Root) и отобразите DOM внутри этого корня:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
После обновления страницы вы заметите, что стили в основном документе больше не могут выделять узлы внутри Shadow Root.
Как вы это сделали? В connectedCallback вы вызвали this.attachShadow , который прикрепляет теневой корень к элементу. open режим означает, что содержимое тени доступно для просмотра, а теневой корень также доступен через this.shadowRoot . Посмотрите также на компонент Web в инспекторе Chrome:

Теперь вы должны увидеть расширяемый теневой корень, содержащий содержимое. Всё, что находится внутри этого теневого корня, называется теневым DOM. Если вы выберете элемент rating в инструментах разработчика Chrome и вызовете $0.children , вы заметите, что он не возвращает никаких дочерних элементов. Это потому, что теневой DOM не считается частью того же дерева DOM, что и прямые дочерние элементы, а рассматривается как теневое дерево .
Световой DOM
Эксперимент: добавить узел в качестве непосредственного дочернего элемента элемента <rating-element> :
index.html
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
Обновите страницу, и вы увидите, что этот новый DOM-узел в светлом DOM этого пользовательского элемента не отображается на странице. Это происходит потому, что в теневом DOM есть функции, позволяющие управлять тем, как узлы светлого DOM проецируются в теневой DOM с помощью элементов <slot> .
5. HTML-шаблоны
Почему именно шаблоны?
Использование innerHTML и строковых литералов шаблонов без предварительной очистки может привести к проблемам безопасности при внедрении скриптов. В прошлом применялись методы с использованием DocumentFragment , но они также сопряжены с другими проблемами, такими как загрузка изображений и выполнение скриптов при определении шаблонов, а также создают препятствия для повторного использования. Именно здесь на помощь приходит элемент <template> ; шаблоны обеспечивают инертный DOM, высокопроизводительный метод клонирования узлов и возможность повторного использования шаблонов.
Использование шаблонов
Далее, переведите компонент на использование HTML-шаблонов:
index.html
<body>
<template id="rating-element-template">
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating"></span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
</template>
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
</body>
Здесь вы переместили содержимое DOM в тег шаблона в DOM основного документа. Теперь переработайте определение пользовательского элемента:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
const templateContent = document.getElementById('rating-element-template').content;
const clonedContent = templateContent.cloneNode(true);
shadowRoot.appendChild(clonedContent);
this.shadowRoot.querySelector('.rating').innerText = this.rating;
}
}
customElements.define('rating-element', RatingElement);
Чтобы использовать этот элемент шаблона, необходимо запросить шаблон, получить его содержимое и клонировать эти узлы с помощью templateContent.cloneNode , где аргумент true выполняет глубокое клонирование. Затем необходимо инициализировать DOM данными.
Поздравляем, у вас теперь есть веб-компонент! К сожалению, он пока ничего не делает, поэтому дальше нужно добавить некоторую функциональность.
6. Добавление функциональности
Связи собственности
В настоящее время единственный способ установить рейтинг для элемента rating-element — это создать элемент, присвоить объекту свойство rating , а затем разместить его на странице. К сожалению, так обычно не работают нативные HTML-элементы. Нативные HTML-элементы, как правило, обновляются при изменении как свойств, так и атрибутов.
Чтобы пользовательский элемент обновлял представление при изменении свойства rating , добавьте следующие строки:
index.js
constructor() {
super();
this._rating = 0;
}
set rating(value) {
this._rating = value;
if (!this.shadowRoot) {
return;
}
const ratingEl = this.shadowRoot.querySelector('.rating');
if (ratingEl) {
ratingEl.innerText = this._rating;
}
}
get rating() {
return this._rating;
}
Вы добавляете сеттер и геттер для свойства rating, а затем обновляете текст элемента rating, если он доступен. Это означает, что если вы установите свойство rating для элемента, представление обновится; проверьте это быстро в консоли инструментов разработчика!
Привязки атрибутов
Теперь обновляйте представление при изменении атрибута; это похоже на обновление представления поля ввода при установке <input value="newValue"> . К счастью, жизненный цикл веб-компонента включает в себя функцию attributeChangedCallback . Обновите рейтинг, добавив следующие строки:
index.js
static get observedAttributes() {
return ['rating'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
}
}
Для того чтобы сработала функция attributeChangedCallback , необходимо установить статический геттер для RatingElement.observedAttributes which defines the attributes to be observed for changes . Затем вы декларативно устанавливаете рейтинг в DOM. Попробуйте:
index.html
<rating-element rating="5"></rating-element>
Рейтинг теперь должен обновляться в декларативном режиме!
Функциональность кнопки
Теперь не хватает только функциональности кнопки. Поведение этого компонента должно позволять пользователю выставлять одну оценку «за» или «против» и предоставлять визуальную обратную связь. Это можно реализовать с помощью обработчиков событий и свойства отражения, но сначала обновите стили, чтобы добавить визуальную обратную связь, добавив следующие строки:
index.html
<style>
...
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
В Shadow DOM селектор :host указывает на узел или пользовательский элемент, к которому прикреплен Shadow Root. В данном случае, если атрибут vote имеет значение "up" кнопка "палец вверх" станет зеленой, а если vote - "down", then it will turn the thumb-down button red . Теперь реализуйте логику, создав для этого отражающее свойство/атрибут для vote аналогично тому, как вы реализовали rating . Начните с сеттера и геттера свойства:
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
}
get vote() {
return this._vote;
}
В constructor вы инициализируете свойство экземпляра _vote значением null , а в сеттере проверяете, отличается ли новое значение. Если да, то соответствующим образом корректируете рейтинг и, что важно, передаете атрибут vote обратно хосту с помощью this.setAttribute .
Далее настройте привязку атрибутов:
index.js
static get observedAttributes() {
return ['rating', 'vote'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
} else if (attributeName === 'vote') {
this.vote = newValue;
}
}
Опять же, это тот же процесс, что и при привязке атрибута rating : вы добавляете vote в observedAttributes и устанавливаете свойство vote в attributeChangedCallback . И наконец, добавьте несколько обработчиков событий клика, чтобы придать кнопкам функциональность!
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
this._boundOnUpClick = this._onUpClick.bind(this);
this._boundOnDownClick = this._onDownClick.bind(this);
}
connectedCallback() {
...
this.shadowRoot.querySelector('.thumb_up')
.addEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.addEventListener('click', this._boundOnDownClick);
}
disconnectedCallback() {
this.shadowRoot.querySelector('.thumb_up')
.removeEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.removeEventListener('click', this._boundOnDownClick);
}
_onUpClick() {
this.vote = 'up';
}
_onDownClick() {
this.vote = 'down';
}
В constructor вы привязываете к элементу несколько обработчиков кликов и сохраняете ссылки на них. В connectedCallback вы отслеживаете события клика по кнопкам. В disconnectedCallback вы очищаете эти обработчики, а для самих обработчиков кликов устанавливаете соответствующее значение vote .
Поздравляем, теперь у вас есть полнофункциональный веб-компонент; попробуйте нажать на несколько кнопок! Проблема в том, что мой JS-файл теперь занимает 96 строк, HTML-файл — 43 строки, и код довольно многословный и императивный для такого простого компонента. Вот тут-то и пригодится проект Lit от Google!
7. Lit-html
Контрольная точка кода
Почему lit-html
В первую очередь, тег <template> полезен и эффективен, но он не интегрирован в логику компонента, что затрудняет распространение шаблона вместе с остальной логикой. Кроме того, способ использования элементов шаблона по своей природе способствует императивному коду, что во многих случаях приводит к менее читаемому коду по сравнению с декларативными шаблонами кодирования.
Вот тут-то и пригодится lit-html! Lit html — это система рендеринга Lit, которая позволяет писать HTML-шаблоны на JavaScript, а затем эффективно отображать и перерендеривать эти шаблоны вместе с данными для создания и обновления DOM. Она похожа на популярные библиотеки JSX и VDOM, но работает непосредственно в браузере и во многих случаях гораздо эффективнее.
Использование Lit HTML
Далее, переведите rating-element компонента Web Component на использование шаблонов Lit, которые используют помеченные шаблонные литералы — функции, принимающие в качестве аргументов строковые шаблоны со специальным синтаксисом. Lit использует шаблонные элементы для обеспечения быстрой отрисовки, а также предоставляет некоторые функции очистки данных для обеспечения безопасности. Начните с переноса элемента <template> в index.html в шаблон Lit, добавив метод render() к веб-компоненту:
index.js
// Dont forget to import from Lit!
import {render, html} from 'lit';
class RatingElement extends HTMLElement {
...
render() {
if (!this.shadowRoot) {
return;
}
const template = html`
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
<button class="thumb_down">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
render(template, this.shadowRoot);
}
}
Вы также можете удалить свой шаблон из index.html . В этом методе render вы определяете переменную с именем template и вызываете функцию шаблонного литерала, помеченную тегом html . Вы также заметите, что выполнили простую привязку данных внутри элемента span.rating , используя синтаксис интерполяции шаблонных литералов ${...} . Это означает, что в конечном итоге вам больше не потребуется императивно обновлять этот узел. Кроме того, вы вызываете метод render lit, который синхронно отображает шаблон в теневой корень.
Переход к декларативному синтаксису
Теперь, когда вы избавились от элемента <template> , перепишите код так, чтобы вместо него вызывался новый метод render . Для начала можно использовать привязку обработчика событий lit, чтобы упростить код обработчика:
index.js
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
...
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
В шаблонах Lit можно добавить обработчик событий к узлу с помощью синтаксиса привязки @EVENT_NAME , где в данном случае свойство vote обновляется каждый раз при нажатии этих кнопок.
Далее, упорядочьте код инициализации обработчика событий в constructor , а также в методах connectedCallback и disconnectedCallback :
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
connectedCallback() {
this.attachShadow({mode: 'open'});
this.render();
}
// remove disonnectedCallback and _onUpClick and _onDownClick
Вам удалось удалить логику обработки кликов из всех трех коллбэков и даже полностью убрать disconnectedCallback ! Также удалось удалить весь код инициализации DOM из connectedCallback , что сделало его гораздо более элегантным. Это также означает, что вы можете избавиться от методов обработки кликов _onUpClick и _onDownClick !
Наконец, обновите методы установки свойств, чтобы они использовали метод render , позволяющий DOM обновляться при изменении свойств или атрибутов:
index.js
set rating(value) {
this._rating = value;
this.render();
}
...
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
// add render method
this.render();
}
Здесь вы смогли удалить логику обновления DOM из сеттера rating и добавили вызов render из сеттера vote . Теперь шаблон стал гораздо более читабельным, поскольку вы можете видеть, где применяются привязки и обработчики событий.
Обновите страницу, и у вас должна появиться работающая кнопка оценки, которая при нажатии на кнопку «Проголосовать за» должна выглядеть вот так!

8. LitElement
Почему именно LitElement?
В коде всё ещё присутствуют некоторые проблемы. Во-первых, изменение свойства или атрибута vote может изменить свойство rating , что приведёт к двойному вызову функции render . Несмотря на то, что повторные вызовы render по сути ничего не делают и эффективны, виртуальная машина JavaScript всё равно тратит время на двойной синхронный вызов этой функции. Во-вторых, добавление новых свойств и атрибутов утомительно, так как требует большого количества шаблонного кода. Вот тут-то и пригодится LitElement !
LitElement — это базовый класс Lit для создания быстрых и легковесных веб-компонентов, которые можно использовать в разных фреймворках и средах. Далее рассмотрим, что LitElement может сделать для нас в rating-element , изменив реализацию для его использования!
Использование LitElement
Для начала импортируйте и создайте подкласс базового класса LitElement из пакета lit :
index.js
import {LitElement, html, css} from 'lit';
class RatingElement extends LitElement {
// remove connectedCallback()
...
Вы импортируете LitElement , который является новым базовым классом для rating-element . Затем вы сохраняете импорт html и, наконец, css , который позволяет нам определять шаблонные литералы с тегами CSS для математических выражений CSS, шаблонизации и других функций.
Далее переместите стили из метода render в статическую таблицу стилей Lit:
index.js
class RatingElement extends LitElement {
static get styles() {
return css`
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
`;
}
...
Здесь хранится большинство стилей в Lit. Lit берет эти стили и использует функции браузеров, такие как Constructable Stylesheets, для обеспечения более быстрой отрисовки, а также, при необходимости, передает их через полифилл Web Components в более старых браузерах.
Жизненный цикл
Lit вводит набор методов обратного вызова жизненного цикла рендеринга поверх стандартных методов обратного вызова веб-компонентов. Эти методы срабатывают при изменении объявленных свойств Lit.
Для использования этой функции необходимо статически указать, какие свойства будут запускать жизненный цикл рендеринга.
index.js
static get properties() {
return {
rating: {
type: Number,
},
vote: {
type: String,
reflect: true,
}
};
}
// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()
Здесь вы определяете, что rating и vote будут запускать жизненный цикл рендеринга LitElement, а также задаете типы, которые будут использоваться для преобразования строковых атрибутов в свойства.
<user-profile .name=${this.user.name} .age=${this.user.age}>
${this.user.family.map(member => html`
<family-member
.name=${member.name}
.relation=${member.relation}>
</family-member>`)}
</user-profile>
Кроме того, флаг reflect в свойстве vote автоматически обновит атрибут vote элемента-хоста, который вы вручную активировали в методе установки значения vote .
Теперь, когда у вас есть блок статических свойств, вы можете удалить всю логику обновления атрибутов и свойств. Это означает, что вы можете удалить следующие методы:
-
connectedCallback -
observedAttributes -
attributeChangedCallback -
rating(связующие и принимающие) -
vote(с использованием сеттеров и геттеров, но с сохранением логики изменения из сеттера)
Вы сохраняете constructor , а также добавляете новый метод жизненного цикла willUpdate :
index.js
constructor() {
super();
this.rating = 0;
this.vote = null;
}
willUpdate(changedProps) {
if (changedProps.has('vote')) {
const newValue = this.vote;
const oldValue = changedProps.get('vote');
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
}
}
// remove set vote() and get vote()
Здесь вы просто инициализируете rating и vote , а логику установки vote переносите в метод жизненного цикла willUpdate . Метод willUpdate вызывается перед render всякий раз, когда изменяется какое-либо обновляемое свойство, поскольку LitElement объединяет изменения свойств в пакеты и делает рендеринг асинхронным. Изменения реактивных свойств (например, this.rating ) в willUpdate не вызовут ненужных вызовов жизненного цикла render .
Наконец, render — это метод жизненного цикла LitElement, который требует от нас возврата шаблона Lit:
index.js
render() {
return html`
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
}
Вам больше не нужно проверять наличие теневого корня, и вам больше не нужно вызывать функцию render ранее импортированную из пакета 'lit' .
Теперь ваш элемент должен отобразиться в предварительном просмотре; нажмите на него!
9. Поздравляем!
Поздравляем, вы успешно создали веб-компонент с нуля и превратили его в LitElement!
Lit невероятно компактен (< 5 КБ в минифицированном и сжатом виде), очень быстр и с ним действительно интересно программировать! Вы можете создавать компоненты для использования другими фреймворками или разрабатывать с его помощью полноценные приложения!
Теперь вы знаете, что такое веб-компонент, как его создать и как Lit упрощает этот процесс!
Контрольная точка кода
Хотите сравнить свой итоговый код с нашим? Сравните его здесь .
Что дальше?
Посмотрите и другие примеры кода!
- Lit for React Developers
- Создайте средство просмотра кирпичей с помощью lit-element.
- Создайте компонент «Истории» с помощью lit-element.
Дополнительная информация
- Интерактивный учебник по литературе
- Литературные врачи
- Open Web Components — сообщество, управляемое самими пользователями, которое предоставляет рекомендации и инструменты.
- WebComponents.dev — Создание веб-компонентов во всех известных фреймворках
Сообщество
- Lit and Friends Slack — крупнейшее сообщество пользователей веб-компонентов.
- @buildWithLit в Твиттере — аккаунт в Твиттере команды, создавшей Lit.
- Web Components SF — встреча любителей веб-компонентов в Сан-Франциско.