От веб-компонента к освещенному элементу

1. Введение

Последнее обновление: 10 августа 2021 г.

Веб-компоненты

Веб-компоненты — это набор API-интерфейсов веб-платформы, которые позволяют создавать новые пользовательские, повторно используемые, инкапсулированные HTML-теги для использования на веб-страницах и в веб-приложениях. Пользовательские компоненты и виджеты, созданные на основе стандартов веб-компонентов, будут работать в современных браузерах и могут использоваться с любой библиотекой JavaScript или платформой, работающей с HTML.

Что такое горит

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

Lit предоставляет API для упрощения общих задач веб-компонентов, таких как управление свойствами, атрибутами и рендерингом.

Что вы узнаете

  • Что такое веб-компонент
  • Концепции веб-компонентов
  • Как создать веб-компонент
  • Что такоеlit-html и LitElement
  • Что Lit делает поверх веб-компонента

Что ты построишь

  • Ванильный веб-компонент «палец вверх/вниз»
  • Веб-компонент на основе Lit

Что вам понадобится

  • Любой обновленный современный браузер (Chrome, Safari, Firefox, Chromium Edge). Веб-компоненты работают во всех современных браузерах, а полифилы доступны для Microsoft Internet Explorer 11 и Microsoft Edge без Chrome.
  • Знание HTML, CSS, JavaScript и Chrome DevTools .

2. Настройка и изучение игровой площадки.

Доступ к коду

На протяжении всей кодовой лаборатории будут ссылки на игровую площадку Lit, например:

Игровая площадка — это песочница кода, которая полностью работает в вашем браузере. Он может компилировать и запускать файлы TypeScript и JavaScript, а также автоматически разрешать импорт в модули узлов. например

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

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

Изучение пользовательского интерфейса освещенной игровой площадки

Панель вкладок выбора файлов помечена как «Раздел 1», раздел редактирования кода — как «Раздел 2», предварительный просмотр вывода — как «Раздел 3», а кнопка перезагрузки предварительного просмотра — как «Раздел 4».

На снимке экрана пользовательского интерфейса освещенной игровой площадки выделены разделы, которые вы будете использовать в этой лаборатории кода.

  1. Селектор файлов. Обратите внимание на кнопку плюс...
  2. Редактор файлов.
  3. Предварительный просмотр кода.
  4. Кнопка перезагрузки.
  5. Кнопка загрузки.

Настройка VS Code (расширенная)

Вот преимущества использования этой настройки VS Code:

  • Проверка типа шаблона
  • IntelliSense и автозаполнение шаблонов

Если у вас уже установлен NPM, VS Code (с плагиномlit-plugin ) и вы знаете, как использовать эту среду, вы можете просто загрузить и запустить эти проекты, выполнив следующие действия:

  • Нажмите кнопку загрузки
  • Извлеките содержимое файла tar в каталог.
  • Установите сервер разработки, который может разрешать спецификаторы пустых модулей (команда Lit рекомендует @web/dev-server ).
  • Запустите сервер разработки и откройте браузер (если вы используете @web/dev-server вы можете использовать npx web-dev-server --node-resolve --watch --open )
    • Если вы используете пример package.json используйте npm run serve

3. Определите пользовательский элемент

Пользовательские элементы

Веб-компоненты — это набор из четырех собственных веб-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 с tagName 'rating-element' . Это означает, что каждый элемент вашего документа с именем <rating-element> будет связан с этим классом.

Поместите <rating-element> в тело документа и посмотрите, что отобразится.

index.html

<body>
 <rating-element></rating-element>
</body>

Теперь, посмотрев на результат, вы увидите, что ничего не отрисовано. Это ожидаемо, поскольку вы не указали браузеру, как отображать <rating-element> . Вы можете убедиться, что определение пользовательского элемента прошло успешно, выбрав <rating-element> в селекторе элементов Chrome Dev Tools и в консоли вызвав:

$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. Теневой ДОМ

Почему Тень ДОМ?

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

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>

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

Прикрепление теневого корня

Прикрепите теневой корень к элементу и визуализируйте 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);

Обновив страницу, вы заметите, что стили в основном документе больше не могут выбирать узлы внутри теневого корня.

Как ты это сделал? В connectedCallback вы вызвали this.attachShadow , который прикрепляет теневой корень к элементу. open режим означает, что теневое содержимое доступно для проверки, а также делает теневой корень доступным через this.shadowRoot . Взгляните также на веб-компонент в инспекторе Chrome:

Дерево dom в инспекторе Chrome. Существует <rating-element> с #shadow-root (open) в качестве дочернего элемента и предыдущим DOM внутри этого теневого корня.

Теперь вы должны увидеть расширяемый теневой корень, в котором хранится содержимое. Все внутри этого теневого корня называется Shadow DOM. Если вы выберете элемент рейтинга в Chrome Dev Tools и вызовете $0.children , вы заметите, что он не возвращает дочерних элементов. Это связано с тем, что Shadow DOM не считается частью того же дерева DOM, что и прямые дочерние элементы, а скорее Shadow Tree .

Светлый ДОМ

Эксперимент: добавьте узел как прямой дочерний элемент <rating-element> :

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

Обновите страницу, и вы увидите, что этот новый узел DOM в Light DOM этого пользовательского элемента не отображается на странице. Это связано с тем, что Shadow DOM имеет функции для управления тем, как узлы Light 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 для объекта и затем поместить его на страницу. К сожалению, нативные элементы 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;
}

Вы добавляете метод установки и получения для свойства рейтинга, а затем обновляете текст элемента рейтинга, если он доступен. Это означает, что если вы установили свойство рейтинга для элемента, представление обновится; быстро протестируйте его в консоли Dev Tools!

Привязки атрибутов

Теперь обновите представление при изменении атрибута; это похоже на ввод, обновляющий свое представление, когда вы устанавливаете <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 относится к узлу или пользовательскому элементу, к которому прикреплен теневой корень. В этом случае, если атрибут 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;
}

Вы инициализируете свойство экземпляра _vote значением null в constructor , а в установщике проверяете, отличается ли новое значение. Если это так, вы соответствующим образом корректируете рейтинг и, что немаловажно, возвращаете атрибут 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 строки, а код довольно многословен и обязателен для такого простого компонента. Именно здесь на помощь приходит проект Google Lit!

7. Лит-html

Контрольная точка кода

Почему лит-html

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

Вот тут-то и приходит на помощьlit-html! Lit html — это система рендеринга Lit, которая позволяет писать шаблоны HTML на Javascript, а затем эффективно отображать и повторно отображать эти шаблоны вместе с данными для создания и обновления DOM. Она похожа на популярные библиотеки JSX и VDOM, но работает в браузере изначально и во многих случаях гораздо эффективнее.

Использование Lit HTML

Затем перенесите собственный rating-element веб-компонента на использование шаблона 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 . В этом методе рендеринга вы определяете переменную с именем template и вызываете литеральную функцию шаблона с тегом html . Вы также заметите, что выполнили простую привязку данных внутри элемента span.rating , используя синтаксис интерполяции шаблонных литералов ${...} . Это означает, что в конечном итоге вам больше не понадобится принудительно обновлять этот узел. Кроме того, вы вызываете метод render освещенный, который синхронно отображает шаблон в теневой корень.

Переход на декларативный синтаксис

Теперь, когда вы избавились от элемента <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 . Теперь шаблон стал гораздо более читаемым, поскольку теперь вы можете видеть, где применяются привязки и прослушиватели событий.

Обновите страницу, и у вас должна появиться работающая кнопка рейтинга, которая должна выглядеть так, когда нажата кнопка «за»!

Ползунок рейтинга «Большой палец вверх и вниз» со значением 6 и большим пальцем вверх зеленого цвета.

8. ЛитЭлемент

Почему ЛитЭлемент

Некоторые проблемы с кодом все еще присутствуют. Во-первых, если вы измените свойство или атрибут vote , это может изменить свойство rating , что приведет к двойному вызову render . Несмотря на то, что повторные вызовы рендеринга по сути являются бездействующими и эффективными, виртуальная машина javascript все еще тратит время на двойной синхронный вызов этой функции. Во-вторых, добавлять новые свойства и атрибуты утомительно, поскольку для этого требуется много шаблонного кода. Вот тут-то и приходит на помощь LitElement !

LitElement — это базовый класс Lit для создания быстрых и легких веб-компонентов, которые можно использовать в разных платформах и средах. Далее посмотрим, что LitElement может сделать для нас в rating-element , изменив реализацию для его использования!

Использование ЛитЭлемент

Начните с импорта и создания подкласса базового класса LitElement из lit :

index.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

Вы импортируете LitElement , который является новым базовым классом для rating-element . Затем вы сохраняете импорт html и, наконец, css , который позволяет нам определять литералы шаблона с тегами CSS для математики CSS, шаблонов и других функций «под капотом».

Затем переместите стили из метода рендеринга в статическую таблицу стилей 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 представляет набор методов обратного вызова жизненного цикла рендеринга поверх собственных обратных вызовов веб-компонентов. Эти обратные вызовы запускаются при изменении объявленных свойств 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 упрощает их создание!

Контрольная точка кода

Хотите сравнить свой окончательный код с нашим? Сравните здесь .

Что дальше?

Ознакомьтесь с некоторыми другими лабораториями кода!

Дальнейшее чтение

Сообщество