Освещено для разработчиков React

1. Введение

Что такое литература?

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

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

Как перевести несколько концепций React на язык Lit, например:

  • JSX и шаблонизация
  • Компоненты и реквизит
  • Состояние и жизненный цикл
  • Крючки
  • Дети
  • Ссылки
  • Государство-посредник

Что вы построите

По завершении этого практического занятия вы сможете преобразовывать концепции компонентов React в их аналоги в Lit.

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

  • Последняя версия Chrome, Safari, Firefox или Edge.
  • Знание HTML, CSS, JavaScript и инструментов разработчика Chrome .
  • Знание React
  • (Продвинутый уровень) Для наилучшего опыта разработки скачайте VS Code . Вам также понадобится lit-plugin для VS Code и NPM .

2. Литературное против реакционного

Основные концепции и возможности Lit во многом схожи с React, но у Lit также есть некоторые ключевые отличия и особенности:

Оно маленькое

Lit очень компактен: в сжатом и минифицированном виде он занимает всего около 5 КБ, в то время как React + ReactDOM — более 40 КБ .

Гистограмма размера пакета в килобайтах (минифицированном и сжатом виде). ​​Размер минифицированной гистограммы составляет 5 кБ, а размер DOM-дерева React + React — 42,2 кБ.

Это быстро

В публичных тестах , сравнивающих систему шаблонов Lit, lit-html, с VDOM от React, lit-html оказывается на 8-10% быстрее React в худшем случае и более чем на 50% быстрее в более распространенных сценариях использования.

LitElement (базовый класс компонентов Lit) добавляет минимальные накладные расходы к lit-html, но превосходит производительность React на 16-30% при сравнении таких характеристик компонентов, как использование памяти, а также время взаимодействия и запуска.

Сгруппированная гистограмма, показывающая сравнение производительности lit и React в миллисекундах (чем меньше, тем лучше).

Сборка не требуется

Благодаря новым функциям браузера, таким как модули ES и помеченные шаблонные литералы, Lit не требует компиляции для запуска . Это означает, что среду разработки можно настроить с помощью тега <script> + браузера + сервера, и всё готово к работе.

Благодаря модулям ES и современным CDN-сетям, таким как Skypack или UNPKG , вам, возможно, даже не понадобится NPM для начала работы!

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

независимый от фреймворка

Компоненты Lit основаны на наборе веб-стандартов, называемых веб-компонентами. Это означает, что компонент, созданный в Lit, будет работать в существующих и будущих фреймворках . Если он поддерживает HTML-элементы, он поддерживает и веб-компоненты.

Единственные проблемы с совместимостью фреймворков возникают, когда фреймворки имеют ограниченную поддержку DOM. React — один из таких фреймворков, но он допускает обходные пути через Refs, а использование Refs в React не очень удобно для разработчиков.

Команда Lit работает над экспериментальным проектом под названием @lit-labs/react , который будет автоматически анализировать ваши компоненты Lit и генерировать обертку React, чтобы вам не приходилось использовать ссылки.

Кроме того, Custom Elements Everywhere покажет вам, какие фреймворки и библиотеки хорошо работают с пользовательскими элементами!

Первоклассная поддержка TypeScript

Хотя весь код Lit можно написать на JavaScript, Lit написан на TypeScript , и команда Lit рекомендует разработчикам также использовать TypeScript!

Команда Lit сотрудничает с сообществом Lit, чтобы поддерживать проекты, которые обеспечивают проверку типов TypeScript и автозаполнение кода в шаблонах Lit как на этапе разработки, так и на этапе сборки с помощью lit-analyzer и lit-plugin .

Скриншот IDE, демонстрирующий некорректную проверку типа при присвоении логическому значению с очерченной структурой числового значения.

Скриншот IDE, отображающей подсказки IntelliSense.

Инструменты разработчика встроены в браузер.

Компоненты Lit — это просто HTML-элементы в DOM . Это означает, что для проверки ваших компонентов вам не нужно устанавливать какие-либо инструменты или расширения для вашего браузера.

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

Изображение инструментов разработчика Chrome, показывающее, что $0 возвращает <mwc-textfield>, $0.value возвращает "hello world", $0.outlined возвращает true, а {$0} показывает расширение свойства.

Он разработан с учетом серверного рендеринга (SSR).

Lit 2 был разработан с учетом поддержки SSR (серверного рендеринга). На момент написания этого руководства команда Lit еще не выпустила инструменты SSR в стабильной форме, но уже развертывает компоненты с серверным рендерингом в продуктах Google и тестирует SSR в приложениях React. Команда Lit планирует в ближайшее время выпустить эти инструменты на GitHub.

А пока вы можете следить за прогрессом команды Lit здесь .

Это низкий уровень вовлеченности.

Для использования Lit не требуется значительных усилий! Вы можете создавать компоненты в Lit и добавлять их в существующий проект. Если они вам не понравятся, вам не придётся конвертировать всё приложение сразу, так как веб-компоненты работают и в других фреймворках!

Вы разработали целое приложение на Lit и хотите перейти на что-то другое? Тогда вы можете поместить ваше текущее приложение на Lit в новую среду разработки и перенести все необходимые компоненты в новую среду.

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

3. Подготовка и знакомство с игровой площадкой

Этот практический пример можно выполнить двумя способами:

  • Это можно сделать полностью онлайн, в браузере.
  • (Продвинутый уровень) Это можно сделать на локальном компьютере с помощью VS Code.

Доступ к коду

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

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

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

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

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

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

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

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

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

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

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

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

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

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

4. JSX и шаблонизация

В этом разделе вы изучите основы создания шаблонов в Lit.

Шаблоны JSX и Lit

JSX — это расширение синтаксиса JavaScript, позволяющее пользователям React легко писать шаблоны в своем JavaScript-коде. Шаблоны Lit служат аналогичной цели: выражают пользовательский интерфейс компонента как функцию его состояния.

Базовый синтаксис

В React вы бы отобразили JSX-запрос "Hello World" примерно так:

import 'react';
import ReactDOM from 'react-dom';

const name = 'Josh Perez';
const element = (
  <>
    <h1>Hello, {name}</h1>
    <div>How are you?</div>
  </>
);

ReactDOM.render(
  element,
  mountNode
);

В приведенном выше примере есть два элемента и включенная переменная "имя". В Lit это выглядело бы следующим образом:

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Обратите внимание, что для группировки нескольких элементов в шаблонах Lit не требуется React Fragment.

В Lit шаблоны заключаются в html тег `template LIT eral` , отсюда и название Lit!

Значения шаблона

Шаблоны Lit могут принимать другие шаблоны Lit, известные как TemplateResult . Например, заключите name в теги курсивом ( <i> ) и заключите его в шаблонный литерал с тегами. Примечание: используйте обратную кавычку ( ` ), а не одинарную кавычку ' ).

import {html, render} from 'lit';

const name = html`<i>Josh Perez</i>`;
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Объекты Lit TemplateResult могут принимать массивы, строки, другие объекты TemplateResult , а также директивы.

В качестве упражнения попробуйте преобразовать следующий код React в Lit:

const itemsToBuy = [
  <li>Bananas</li>,
  <li>oranges</li>,
  <li>apples</li>,
  <li>grapes</li>
];
const element = (
  <>
    <h1>Things to buy:</h1>
    <ol>
      {itemsToBuy}
    </ol>
  </>);

ReactDOM.render(
  element,
  mountNode
);

Отвечать:

import {html, render} from 'lit';

const itemsToBuy = [
  html`<li>Bananas</li>`,
  html`<li>oranges</li>`,
  html`<li>apples</li>`,
  html`<li>grapes</li>`
];
const element = html`
  <h1>Things to buy:</h1>
  <ol>
    ${itemsToBuy}
  </ol>`;

render(
  element,
  mountNode
);

Передача и расстановка реквизита

Одно из главных отличий синтаксиса JSX от Lit заключается в синтаксисе привязки данных. Например, рассмотрим этот элемент ввода React с привязками:

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      disabled={disabled}
      className={`static-class ${myClass}`}
      defaultValue={value}/>;

ReactDOM.render(
  element,
  mountNode
);

В приведенном выше примере определен входной параметр, который выполняет следующие действия:

  • Присваивает значение переменной, определяющей статус «отключено» (в данном случае — false).
  • Устанавливает для класса значение static-class плюс переменную (в данном случае "static-class my-class" ).
  • Устанавливает значение по умолчанию

В литературном кружке вы бы сделали следующее:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      ?disabled=${disabled}
      class="static-class ${myClass}"
      .value=${value}>`;

render(
  element,
  mountNode
);

В примере Lit добавлена ​​логическая привязка для переключения атрибута disabled .

Далее, привязка осуществляется непосредственно к атрибуту class , а не к className . К атрибуту class можно добавить несколько привязок, если только вы не используете директиву classMap , которая является декларативным вспомогательным средством для переключения классов.

Наконец, свойство value устанавливается для элемента `input`. В отличие от React, это не устанавливает элемент `input` в режим только для чтения, поскольку соответствует нативной реализации и поведению элемента `input`.

Синтаксис привязки свойств Lit

html`<my-element ?attribute-name=${booleanVar}>`;
  • Префикс " ? — это синтаксис привязки для переключения атрибута элемента.
  • Эквивалентно inputRef.toggleAttribute('attribute-name', booleanVar)
  • Это полезно для элементов, использующих disabled , поскольку disabled="false" всё равно воспринимается DOM как `true`, потому что inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • Префикс . — это синтаксис привязки для установки свойства элемента.
  • Эквивалентно inputRef.propertyName = anyVar
  • Подходит для передачи сложных данных, таких как объекты, массивы или классы.
html`<my-element attribute-name=${stringVar}>`;
  • Привязывается к атрибуту элемента.
  • Эквивалентно inputRef.setAttribute('attribute-name', stringVar)
  • Подходит для базовых значений, селекторов правил стиля и селекторов запросов.

обработчики передач

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      onClick={() => console.log('click')}
      onChange={e => console.log(e.target.value)} />;

ReactDOM.render(
  element,
  mountNode
);

В приведенном выше примере определен входной параметр, который выполняет следующие действия:

  • Регистрировать слово "click" при нажатии на поле ввода.
  • Выводить значение поля ввода в лог, когда пользователь вводит символ.

В литературном кружке вы бы сделали следующее:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      @click=${() => console.log('click')}
      @input=${e => console.log(e.target.value)}>`;

render(
  element,
  mountNode
);

В примере Lit к событию click добавлен обработчик с помощью @click .

Далее, вместо использования onChange , используется привязка к собственному событию input <input> , поскольку собственное событие change срабатывает только при blur (React абстрагируется от этих событий).

Синтаксис обработчика событий Lit

html`<my-element @event-name=${() => {...}}></my-element>`;
  • Префикс @ — это синтаксис привязки для обработчика событий.
  • Аналогично inputRef.addEventListener('event-name', ...)
  • Использует собственные имена событий DOM.

5. Компоненты и реквизит

В этом разделе вы узнаете о компонентах и ​​функциях класса Lit. Состояние и хуки более подробно рассматриваются в последующих разделах.

Компоненты класса и литеральный элемент

В Lit аналогом классового компонента React является LitElement, а концепция «реактивных свойств» в Lit представляет собой комбинацию свойств и состояния React. Например:

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''};
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

В приведенном выше примере есть компонент React, который:

  • Отображает name
  • Устанавливает значение по умолчанию для name в виде пустой строки ( "" )
  • Переназначает name "Elliott"

Вот как это делается в LitElement.

В TypeScript:

import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  @property({type: String})
  name = '';

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

На JavaScript:

import {LitElement, html} from 'lit';

class WelcomeBanner extends LitElement {
  static get properties() {
    return {
      name: {type: String}
    }
  }

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

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

customElements.define('welcome-banner', WelcomeBanner);

А в HTML-файле:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

Рассмотрим, что происходит в приведенном выше примере:

@property({type: String})
name = '';
  • Определяет общедоступное реактивное свойство — часть публичного API вашего компонента.
  • Предоставляет доступ к атрибуту (по умолчанию), а также к свойству вашего компонента.
  • Определяет способ преобразования атрибутов компонента (которые представляют собой строки) в значение.
static get properties() {
  return {
    name: {type: String}
  }
}
  • Этот декоратор выполняет ту же функцию, что и декоратор @property в TypeScript, но работает непосредственно в JavaScript.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Эта функция вызывается всякий раз, когда изменяется какое-либо реактивное свойство.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Это связывает имя тега HTML-элемента с определением класса.
  • В соответствии со стандартом Custom Elements, имя тега должно содержать дефис (-).
  • В LitElement this относится к экземпляру пользовательского элемента (в данном случае <welcome-banner> ).
customElements.define('welcome-banner', WelcomeBanner);
  • Это JavaScript-эквивалент декоратора TypeScript @customElement
<head>
  <script type="module" src="./index.js"></script>
</head>
  • Импортирует определение пользовательского элемента.
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • Добавляет пользовательский элемент на страницу.
  • Устанавливает свойство name в значение 'Elliott'

Функциональные компоненты

В Lit отсутствует прямое соответствие между функциональными компонентами, поскольку он не использует JSX или препроцессор. Тем не менее, довольно просто составить функцию, которая принимает свойства и отображает DOM на основе этих свойств. Например:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

В литературе это звучало бы так:

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'Elliott'}),
  document.body.querySelector('#root')
);

6. Состояние и жизненный цикл

В этом разделе вы узнаете о состоянии и жизненном цикле литературы.

Состояние

Концепция «реактивных свойств» Лита представляет собой сочетание состояния и свойств React. При изменении реактивные свойства могут запускать жизненный цикл компонента. Реактивные свойства бывают двух видов:

Общедоступные реактивные свойства

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.name !== nextProps.name) {
      this.setState({name: nextProps.name})
    }
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';

class MyEl extends LitElement {
  @property() name = 'there';
}
  • Определено @property
  • Аналогично свойствам и состоянию в React, но с возможностью изменения.
  • Публичный API, к которому обращаются и который устанавливают потребители компонента.

Внутреннее реактивное состояние

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';

class MyEl extends LitElement {
  @state() name = 'there';
}
  • Определено @state
  • Аналогично состоянию React, но с возможностью изменения.
  • Приватное внутреннее состояние, доступ к которому обычно осуществляется изнутри компонента или подклассов.

Жизненный цикл

Жизненный цикл Lit довольно похож на жизненный цикл React, но есть некоторые существенные различия.

constructor

// React (js)
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this._privateProp = 'private';
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) counter = 0;
  private _privateProp = 'private';
}

// Lit (js)
class MyEl extends LitElement {
  static get properties() {
    return { counter: {type: Number} }
  }
  constructor() {
    this.counter = 0;
    this._privateProp = 'private';
  }
}
  • В литературе эквивалентом также является constructor
  • Нет необходимости передавать что-либо в супервызов.
  • Вызывается (список не является исчерпывающим):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Если на странице присутствует необновлённое имя тега, а определение загружено и зарегистрировано с помощью @customElement или customElements.define
  • По своей функции похож на constructor React.

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Лит эквивалентом также является render
  • Может возвращать любой отображаемый результат, например, TemplateResult , string и т. д.
  • Подобно React, render() должен быть чистой функцией.
  • Рендеринг будет выполняться в тот узел, который вернет createRenderRoot() (по умолчанию — ShadowRoot ).

componentDidMount

componentDidMount похож на комбинацию двух коллбэков жизненного цикла Lit: firstUpdated и connectedCallback .

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
  this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • Вызывается при первом рендеринге шаблона компонента в корневом каталоге компонента.
  • Вызов будет произведен только в том случае, если элемент подключен, например, метод document.createElement('my-component') не будет вызван до тех пор, пока этот узел не будет добавлен в DOM-дерево.
  • Это подходящее место для настройки компонента, требующего отображения DOM-дерева, созданного компонентом.
  • В отличие от componentDidMount в React, изменения реактивных свойств в firstUpdated приведут к повторной отрисовке, хотя браузер обычно объединяет изменения в один и тот же фрейм. Если эти изменения не требуют доступа к DOM корневого элемента, то их обычно следует помещать в willUpdate

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • Вызывается всякий раз, когда пользовательский элемент вставляется в DOM-дерево.
  • В отличие от компонентов React, при отсоединении пользовательских элементов от DOM они не уничтожаются и, следовательно, могут быть «подключены» несколько раз.
    • firstUpdated больше вызываться не будет.
  • Полезно для повторной инициализации DOM или повторного подключения обработчиков событий, которые были удалены при разрыве соединения.
  • Примечание: connectedCallback может быть вызвана до firstUpdated , поэтому при первом вызове DOM может быть недоступен.

componentDidUpdate

// React
componentDidUpdate(prevProps) {
  if (this.props.title !== prevProps.title) {
    this._chart.setTitle(this.props.title);
  }
}

// Lit (ts)
updated(prevProps: PropertyValues<this>) {
  if (prevProps.has('title')) {
    this._chart.setTitle(this.title);
  }
}
  • В литературе используется updated эквивалент (с применением английского прошедшего времени слова "update").
  • В отличие от React, updated вызывается также при первоначальном рендеринге.
  • По функциональности аналогичен componentDidUpdate в React.

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • Аналогом Lit является disconnectedCallback
  • В отличие от компонентов React, при отсоединении пользовательских элементов от DOM компонент не уничтожается.
  • В отличие от componentWillUnmount , disconnectedCallback вызывается после удаления элемента из дерева.
  • DOM внутри корневого элемента по-прежнему связан с поддеревом этого элемента.
  • Полезно для очистки обработчиков событий и проблем с утечками ссылок, чтобы браузер мог выполнить сборку мусора для компонента.

Упражнение

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

В приведенном выше примере используются простые часы, выполняющие следующие функции:

  • Программа выводит сообщение "Hello World! It is", а затем отображает время.
  • Каждую секунду часы будут обновляться.
  • При спешивании устройство очищает интервал, вызывая тик.

Для начала начнём с объявления класса компонента:

// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
}

// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
}

customElements.define('lit-clock', LitClock);

Далее, инициализируйте date и объявите её внутренним реактивным свойством с помощью @state поскольку пользователи компонента не будут устанавливать date напрямую.

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state() // declares internal reactive prop
  private date = new Date(); // initialization
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      // declares internal reactive prop
      date: {state: true}
    }
  }

  constructor() {
    super();
    // initialization
    this.date = new Date();
  }
}

customElements.define('lit-clock', LitClock);

Далее выполните рендеринг шаблона.

// Lit (JS & TS)
render() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

Теперь реализуйте метод "тик".

tick() {
  this.date = new Date();
}

Далее следует реализация componentDidMount . Аналог в Lit — это смесь firstUpdated и connectedCallback . В случае этого компонента вызов tick с setInterval не требует доступа к DOM внутри корневого элемента. Кроме того, интервал будет очищен при удалении элемента из дерева документа, поэтому, если он будет повторно добавлен, интервал нужно будет начинать заново. Таким образом, connectedCallback здесь — лучший выбор.

// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  // initialize timerId for TS
  private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  ...
}

// Lit (JS)
constructor() {
  super();
  // initialization
  this.date = new Date();
  this.timerId = -1; // initialize timerId for JS
}

connectedCallback() {
  super.connectedCallback();
  this.timerId = setInterval(
    () => this.tick(),
    1000
  );
}

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

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

В итоге всё должно выглядеть примерно так:

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      date: {state: true}
    }
  }

  constructor() {
    super();
    this.date = new Date();
  }

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

customElements.define('lit-clock', LitClock);

7. Крючки

В этом разделе вы узнаете, как перевести концепции React Hook на язык Lit.

Понятия хуков React

React Hooks предоставляют функциональным компонентам способ "подключаться" к состоянию. Это дает ряд преимуществ.

  • Они упрощают повторное использование логики с сохранением состояния.
  • Помогите разбить компонент на более мелкие функции.

Кроме того, акцент на компонентах, основанных на функциях, позволил решить некоторые проблемы синтаксиса React, основанного на классах, такие как:

  • Необходимо передавать props от constructor к super
  • Неаккуратная инициализация свойств в constructor
    • Это была причина, указанная командой React в то время, но она была решена в ES2019.
  • Проблемы, вызванные тем, this больше не относится к компоненту,

Концепции React Hooks в литературе

Как упоминалось в разделе «Компоненты и свойства» , Lit не предоставляет возможности создания пользовательских элементов из функции, но LitElement решает большинство основных проблем, связанных с классовыми компонентами React. Например:

// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';

class MyEl extends React.Component {
  constructor(props) {
    super(props); // Leaky implementation
    this.state = {count: 0};
    this._chart = null; // Deemed messy
  }

  render() {
    return (
      <>
        <div>Num times clicked {count}</div>
        <button onClick={this.clickCallback}>click me</button>
      </>
    );
  }

  clickCallback() {
    // Errors because `this` no longer refers to the component
    this.setState({count: this.count + 1});
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) count = 0; // No need for constructor to set state
  private _chart = null; // Public class fields introduced to JS in 2019

  render() {
    return html`
        <div>Num times clicked ${count}</div>
        <button @click=${this.clickCallback}>click me</button>`;
  }

  private clickCallback() {
    // No error because `this` refers to component
    this.count++;
  }
}

Как литература решает эти проблемы?

  • constructor не принимает аргументов.
  • Все привязки @event автоматически привязываются к this
  • В подавляющем большинстве случаев this относится к ссылке на пользовательский элемент.
  • Теперь свойства класса можно создавать как члены класса. Это упрощает реализацию, основанную на конструкторах.

Реактивные контроллеры

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

Реактивный контроллер — это объектный интерфейс, который может подключаться к циклу обновления хоста контроллера, например, LitElement.

Жизненный цикл ReactiveController и reactiveControllerHost выглядит следующим образом:

interface ReactiveController {
  hostConnected(): void;
  hostUpdate(): void;
  hostUpdated(): void;
  hostDisconnected(): void;
}
interface ReactiveControllerHost {
  addController(controller: ReactiveController): void;
  removeController(controller: ReactiveController): void;
  requestUpdate(): void;
  readonly updateComplete: Promise<boolean>;
}

Создав реактивный контроллер и прикрепив его к хосту с помощью addController , вы вызовете жизненный цикл контроллера одновременно с жизненным циклом хоста. Например, вспомним пример с часами из раздела «Состояние и жизненный цикл» :

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

В приведенном выше примере используются простые часы, которые выполняют следующие действия:

  • Программа выводит сообщение "Hello World! It is", а затем отображает время.
  • Каждую секунду часы будут обновляться.
  • При спешивании устройство очищает интервал, вызывая тик.

Создание каркаса компонентов

Для начала объявите класс компонента и добавьте функцию render .

// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';

class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

Создание контроллера

Теперь перейдем к clock.ts , создадим класс для ClockController и настроим constructor :

// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';

export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  private tick() {
  }

  hostDisconnected() {
  }
}

// Lit (JS) - clock.js
export class ClockController {
  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  tick() {
  }

  hostDisconnected() {
  }
}

Реактивный контроллер можно построить любым способом, если он использует интерфейс ReactiveController , но команда Lit предпочитает использовать для большинства простых случаев класс с constructor , который может принимать интерфейс ReactiveControllerHost , а также любые другие свойства, необходимые для инициализации контроллера.

Теперь вам нужно преобразовать коллбэки жизненного цикла React в коллбэки контроллера. Вкратце:

  • componentDidMount
    • В connectedCallback LitElement
    • К хосту контроллера hostConnected
  • ComponentWillUnmount
    • Для LitElement's disconnectedCallback
    • К хосту контроллера hostDisconnected

Для получения дополнительной информации о преобразовании жизненного цикла React в жизненный цикл Lit см. раздел «Состояние и жизненный цикл» .

Далее реализуйте метод обратного вызова hostConnected и метод tick , а также очистите интервал в hostDisconnected как это сделано в примере в разделе «Состояние и жизненный цикл» .

// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;
  private interval = 0 as unknown as ReturnType<typeof setTimeout>;
  date = new Date();

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  private tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

// Lit (JS) - clock.js
export class ClockController {
  interval = 0;
  host;
  date = new Date();

  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

Использование контроллера

Для использования контроллера часов импортируйте контроллер и обновите компонент в index.ts или index.js .

// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';

@customElement('my-element')
class MyElement extends LitElement {
  private readonly clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';

class MyElement extends LitElement {
  clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

Для использования контроллера необходимо создать его экземпляр, передав ссылку на хост контроллера (которым является компонент <my-element> ), а затем использовать контроллер в методе render .

Запуск повторной отрисовки в контроллере

Обратите внимание, что время отображается, но не обновляется. Это происходит потому, что контроллер устанавливает дату каждую секунду, но хост не обновляет её. Это связано с тем, что date изменяется в классе ClockController , а не в компоненте. Это означает, что после установки date в контроллере хосту необходимо дать команду на запуск цикла обновления с помощью host.requestUpdate() .

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

Теперь время поджимает!

Для более подробного сравнения распространенных сценариев использования хуков, пожалуйста, обратитесь к разделу «Расширенные темы — Хуки» .

8. Дети

В этом разделе вы узнаете, как использовать слоты для управления дочерними элементами в Lit.

Игровые автоматы и дети

Слоты позволяют осуществлять композицию, предоставляя возможность вкладывать компоненты друг в друга.

В React дочерние элементы наследуются через props. Слот по умолчанию — props.children , а функция render определяет, где этот слот находится. Например:

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

Учтите, что props.children — это компоненты React, а не HTML-элементы.

В Lit дочерние элементы создаются в функции render с помощью элементов слотов . Обратите внимание, что дочерние элементы наследуются не так, как в React. В Lit дочерние элементы представляют собой HTMLElements, прикрепленные к слотам. Это прикрепление называется проекцией (Projection) .

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

Несколько слотов

В React добавление нескольких слотов по сути эквивалентно наследованию большего количества свойств.

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

Аналогично, добавление большего количества элементов <slot> создает больше слотов в Lit. Несколько слотов определяются с помощью атрибута name : <slot name="slot-name"> . Это позволяет дочерним элементам указывать, к какому слоту они будут привязаны.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <header>
          <slot name="headerChildren"></slot>
        </header>
        <section>
          <slot name="sectionChildren"></slot>
        </section>
      </article>
   `;
  }
}

Содержимое слота по умолчанию

Если в слот не спроецированы узлы, слот будет отображать свое поддерево. Если в слот спроецированы узлы, этот слот не будет отображать свое поддерево, а вместо этого будет отображать спроецированные узлы.

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot name="slotWithDefault">
            <p>
             This message will not be rendered when children are attached to this slot!
            <p>
          </slot>
        </div>
      </section>
   `;
  }
}

Распределить детей по группам

В React дочерние элементы назначаются слотам через свойства компонента. В приведенном ниже примере элементы React передаются в свойства headerChildren и sectionChildren .

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

В Lit дочерние элементы назначаются в слоты с помощью атрибута slot .

@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
  render() {
    return html`
      <my-article>
        <h3 slot="headerChildren">
          Extry, Extry! Read all about it!
        </h3>
        <p slot="sectionChildren">
          Children are composed with slots in Lit!
        </p>
      </my-article>
   `;
  }
}

Если отсутствует слот по умолчанию (например, <slot> ) и нет слота, у которого атрибут name (например <slot name="foo"> ) соответствует атрибуту slot дочерних элементов пользовательского элемента (например <div slot="foo"> ), то этот узел не будет проецироваться и не будет отображаться.

9. Ссылки

Иногда разработчику может потребоваться доступ к API элемента HTMLElement.

В этом разделе вы узнаете, как получать ссылки на элементы в Lit.

Ссылки на React

Компонент React преобразуется в серию вызовов функций, которые при вызове создают виртуальный DOM. Этот виртуальный DOM интерпретируется ReactDOM и отображает HTMLElements.

В React ссылки (Refs) — это место в памяти, предназначенное для размещения сгенерированного HTMLElement.

const RefsExample = (props) => {
 const inputRef = React.useRef(null);
 const onButtonClick = React.useCallback(() => {
   inputRef.current?.focus();
 }, [inputRef]);

 return (
   <div>
     <input type={"text"} ref={inputRef} />
     <br />
     <button onClick={onButtonClick}>
       Click to focus on the input above!
     </button>
   </div>
 );
};

В приведенном выше примере компонент React выполнит следующие действия:

  • Отобразить пустое текстовое поле и кнопку с текстом.
  • Фокусировка ввода при нажатии кнопки.

После первоначальной отрисовки React установит inputRef.current равным сгенерированному HTMLInputElement через атрибут ref .

Опубликовано в разделе «Ссылки» с помощью @query

Lit работает в непосредственной близости от браузера и создает очень тонкую абстракцию над встроенными функциями браузера.

В React аналогом refs в Lit является HTMLElement, возвращаемый декораторами @query и @queryAll .

@customElement("my-element")
export class MyElement extends LitElement {
  @query('input') // Define the query
  inputEl!: HTMLInputElement; // Declare the prop

  // Declare the click event listener
  onButtonClick() {
    // Use the query to focus
    this.inputEl.focus();
  }

  render() {
    return html`
      <input type="text">
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

В приведенном выше примере компонент Lit выполняет следующие действия:

  • Определяет свойство элемента MyElement с помощью декоратора @query (создавая геттер для HTMLInputElement ).
  • Объявляет и прикрепляет функцию обратного вызова для события клика с именем onButtonClick .
  • Фокусирует ввод при нажатии кнопки

В JavaScript декораторы @query и @queryAll выполняют querySelector и querySelectorAll соответственно. Это эквивалент ` @query('input') inputEl!: HTMLInputElement; в JavaScript.

get inputEl() {
  return this.renderRoot.querySelector('input');
}

После того, как компонент Lit зафиксирует шаблон метода render в корневом my-element , декоратор @query позволит inputEl возвращать первый найденный элемент input в корневом элементе render. Он вернет null если @query не сможет найти указанный элемент.

Если в корневом элементе рендеринга присутствует несколько элементов input , @queryAll вернет список узлов.

10. Государство-посредник

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

Компоненты многоразового использования

React имитирует функциональный рендеринг с нисходящим потоком данных. Родительские элементы передают состояние дочерним элементам через свойства (props). Дочерние элементы взаимодействуют с родительскими элементами через колбэки, находящиеся в свойствах.

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;


  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

В приведенном выше примере компонент React выполняет следующие действия:

  • Создает метку на основе значения props.step .
  • Отображает кнопку с меткой "+шаг" или "-шаг".
  • Обновляет родительский компонент, вызывая метод props.addToCounter с аргументом props.step при щелчке.

Хотя в Lit можно передавать коллбэки, общепринятые шаблоны отличаются. Компонент React из приведенного выше примера можно было бы переписать как компонент Lit в приведенном ниже примере:

@customElement('counter-button')
export class CounterButton extends LitElement {
  @property({type: Number}) step: number = 0;

  onClick() {
    const event = new CustomEvent('update-counter', {
      bubbles: true,
      detail: {
        step: this.step,
      }
    });

    this.dispatchEvent(event);
  }

  render() {
    const label = this.step < 0
      ? `- ${-1 * this.step}`  // "- 1"
      : `+ ${this.step}`;      // "+ 1"

    return html`
      <button @click=${this.onClick}>${label}</button>
    `;
  }
}

В приведенном выше примере компонент Lit выполнит следующие действия:

  • Создайте step реактивного свойства
  • При щелчке по элементу отправляется пользовательское событие с именем update-counter передающее значение step элемента.

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

Компоненты с сохранением состояния

В React для управления состоянием обычно используются хуки. Компонент MyCounter можно создать, повторно используя компонент CounterButton . Обратите внимание, как addToCounter передается обоим экземплярам CounterButton .

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

Приведенный выше пример выполняет следующее:

  • Создает состояние count .
  • Создает функцию обратного вызова, которая добавляет число к состоянию count .
  • CounterButton использует addToCounter для обновления count step значения при каждом клике.

Аналогичная реализация MyCounter может быть выполнена в Lit. Обратите внимание, что addToCounter не передается в counter-button . Вместо этого, функция обратного вызова привязывается в качестве обработчика событий к событию @update-counter на родительском элементе.

@customElement("my-counter")
export class MyCounter extends LitElement {
  @property({type: Number}) count = 0;

  addToCounter(e: CustomEvent<{step: number}>) {
    // Get step from detail of event or via @query
    this.count += e.detail.step;
  }

  render() {
    return html`
      <div @update-counter="${this.addToCounter}">
        <h3>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

Приведенный выше пример выполняет следующее:

  • Создает реактивное свойство с именем count , которое будет обновлять компонент при изменении его значения.
  • Привязывает функцию обратного вызова addToCounter к обработчику событий @update-counter
  • Количество обновлений count путем добавления значения, найденного в detail.step события update-counter
  • Устанавливает значение step counter-button с помощью атрибута step

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

Этот подход соответствует передовым практикам и отвечает цели Lit по обеспечению кроссплатформенной поддержки веб-компонентов.

11. Стиль

В этом разделе вы узнаете о стилизации в литературе.

Стиль

Lit предлагает множество способов оформления элементов, а также встроенное решение.

Встроенные стили

Lit поддерживает как встроенные стили, так и привязку к ним.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1 style="color:orange;">This text is orange</h1>
        <h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

В приведенном выше примере есть 2 заголовка, каждый из которых имеет строчный стиль.

Теперь импортируйте и привяжите рамку из border-color.js к оранжевому тексту:

...
import borderColor from './border-color.js';

...

html`
  ...
  <h1 style="color:orange;${borderColor}">This text is orange</h1>
  ...`

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

styleMap

Директива styleMap упрощает использование JavaScript для установки встроенных стилей. Например:

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';

@customElement('my-element')
class MyElement extends LitElement {
  @property({type: String})
  color = '#000'

  render() {
    // Define the styleMap
    const headerStyle = styleMap({
      'border-color': this.color,
    });

    return html`
      <div>
        <h1
          style="border-style:solid;
          <!-- Use the styleMap -->
          border-width:2px;${headerStyle}">
          This div has a border color of ${this.color}
        </h1>
        <input
          type="color"
          @input=${e => (this.color = e.target.value)}
          value="#000">
      </div>
    `;
  }
}

Приведенный выше пример выполняет следующие действия:

  • Отображает заголовок h1 с рамкой и палитрой цветов.
  • Изменяет border-color на значение, выбранное в палитре цветов.

Кроме того, существует styleMap , который используется для установки стилей элемента h1 . Синтаксис styleMap аналогичен синтаксису привязки атрибутов style в React.

CSSResult

Рекомендуемый способ оформления компонентов — использование шаблонных литералов, помеченных css тегами.

import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';

const ORANGE = css`orange`;

@customElement('my-element')
class MyElement extends LitElement {
  static styles = [
    css`
      #orange {
        color: ${ORANGE};
      }

      #purple {
        color: rebeccapurple;
      }
    `
  ];

  render() {
    return html`
      <div>
        <h1 id="orange">This text is orange</h1>
        <h1 id="purple">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

Приведенный выше пример выполняет следующие действия:

  • Объявляет шаблонный литерал с тегом CSS и привязкой.
  • Задает цвета для двух заголовков h1 с идентификаторами.

Преимущества использования тега шаблона css :

  • Анализ выполняется один раз для каждого класса, а не для каждого экземпляра.
  • Разработано с учетом возможности повторного использования модулей.
  • Стили легко можно разделить на отдельные файлы.
  • Совместимо с полифиллом CSS Custom Properties.

Кроме того, обратите внимание на тег <style> в index.html :

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

Lit ограничивает область видимости стилей ваших компонентов их корневыми элементами. Это означает, что стили не будут просачиваться внутрь или наружу. Для передачи стилей компонентам команда Lit рекомендует использовать пользовательские свойства CSS , поскольку они могут выходить за рамки области видимости стилей Lit.

Стильные теги

Также можно просто встраивать теги <style> в шаблоны. Браузер удалит дубликаты этих тегов style, но, размещая их в шаблонах, вы обеспечите их разбор для каждого экземпляра компонента, а не для каждого класса, как это происходит с шаблонами, помеченными тегами css . Кроме того, удаление дубликатов CSSResult происходит гораздо быстрее.

Использование тега <link rel="stylesheet"> в шаблоне также возможно для стилей, но это тоже не рекомендуется, поскольку может вызвать первоначальное мерцание нестилизованного контента (FOUC).

12. Расширенные темы (необязательно)

JSX и шаблонизация

Lit & Virtual DOM

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

Вот пример шаблонного литерала:

const str = 'string';
console.log(`This is a template literal ${str}`);

Вот пример помеченного шаблонного литерала:

const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true

In the above example, the tag is the tag function and the f function returns an invocation of a tagged template literal.

A lot of the performance magic in Lit comes from the fact that the string arrays passed into the tag function have the same pointer (as shown in the second console.log ). The browser does not recreate a new strings array on each tag function invocation, because it is using the same template literal (ie in the same location in the AST). So Lit's binding, parsing, and template caching can take advantage of these features without much runtime diffing overhead.

This built-in browser behavior of tagged template literals gives Lit quite a performance advantage. Most conventional Virtual DOMs do the majority of their work in JavaScript. However, tagged template literals do most of their diffing in the browser's C++.

If you'd like to get started using HTML tagged template literals with React or Preact, the Lit team recommends the htm library .

Though, as is the case with the Google Codelabs site and several online code editors, you will notice that tagged template literal syntax highlighting is not very common. Some IDEs and text editors support them by default such as Atom and GitHub's codeblock highlighter. The Lit team also works very closely with the community to maintain projects such as the lit-plugin which is a VS Code plugin that will add syntax highlighting, type checking, and intellisense to your Lit projects.

Lit & JSX + React DOM

JSX does not run in the browser and instead uses a preprocessor to convert JSX to JavaScript function calls (typically via Babel).

For example, Babel will transform this:

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

в это:

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

React DOM then takes the React output and translates it to actual DOM – properties, attributes, event listeners, and all.

Lit-html uses tagged template literals which can run in the browser without transpilation or a preprocessor. This means that in order to get started with Lit, all you need is an HTML file, an ES module script, and a server. Here's a completely browser-runnable script:

<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      import {html, render} from 'https://cdn.skypack.dev/lit';

      render(
        html`<div>Hello World!</div>`,
        document.querySelector('.root')
      )
    </script>
  </head>
  <body>
    <div class="root"></div>
  </body>
</html>

Additionally, since Lit's templating system, lit-html, does not use a conventional Virtual DOM but rather uses the DOM API directly, Lit 2's size is under 5kb minified and gzipped compared to React (2.8kb) + react-dom's (39.4kb) 40kb minified and gizipped.

События

React uses a synthetic event system. This means that react-dom must define every event that will be used on every component and provide a camelCase event listener equivalent for each type of node. As a result, JSX does not have a method to define an event listener for a custom event and developers must use a ref and then imperatively apply a listener. This creates a sub-par developer experience when integrating libraries that don't have React in mind thus resulting in having to write a React-specific wrapper.

Lit-html directly accesses the DOM and uses native events, so adding event listeners is as easy as @event-name=${eventNameListener} . This means that less runtime parsing is done for adding event listeners as well as firing events.

Компоненты и реквизит

React components & custom elements

Under the hood, LitElement uses custom elements to package its components. Custom elements introduce some tradeoffs between React components when it comes to componentization (state and lifecycle is discussed further in the State & Lifecycle section).

Some advantages Custom Elements have as a component system:

  • Native to the browser and do not require any tooling
  • Fit into every browser API from innerHTML and document.createElement to querySelector
  • Can typically be used across frameworks
  • Can be lazily registered with customElements.define and "hydrate" DOM

Some disadvantages Custom Elements have compared to React components:

  • Cannot create a custom element without defining a class (thus no JSX-like functional components)
  • Must contain a closing tag
    • Note: despite the developer convenience browser vendors tend to regret the self-closing tag spec which is why newer specs tend to not include self-closing tags
  • Introduces an extra node to the DOM tree which may cause layout issues
  • Must be registered via JavaScript

Lit has gone with custom elements over a bespoke element system because the custom elements are built into the browser, and the Lit team believes that the cross-framework benefits outweigh the benefits provided by a component abstraction layer. In fact, the Lit team's efforts in the lit-ssr space have overcome the main issues with JavaScript registration. Additionally, some companies such as GitHub take advantage of custom element lazy registration to progressively enhance pages with optional flair.

Passing data to custom elements

A common misconception with custom elements is that data can only be passed in as strings. This misconception likely comes from the fact that element attributes can only be written as strings. Though it is true that Lit will cast string attributes to their defined types, custom elements can also accept complex data as properties.

For example – given the following LitElement definition:

// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('data-test')
class DataTest extends LitElement {
  @property({type: Number})
  num = 0;

  @property({attribute: false})
  data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}

  render() {
    return html`
      <div>num + 1 = ${this.num + 1}</div>
      <div>data.a = ${this.data.a}</div>
      <div>data.b = ${this.data.b}</div>
      <div>data.c = ${this.data.c}</div>`;
  }
}

A primitive reactive property num is defined which will convert an attribute's string value into a number , and then complex data structure is introduced with attribute:false which deactivates Lit's attribute handling.

This is how to pass data to this custom element:

<head>
  <script type="module">
    import './data-test.js'; // loads element definition
    import {html} from './data-test.js';

    const el = document.querySelector('data-test');
    el.data = {
      a: 5,
      b: null,
      c: [html`<div>foo</div>`,html`<div>bar</div>`]
    };
  </script>
</head>
<body>
  <data-test num="5"></data-test>
</body>

Состояние и жизненный цикл

Other React Lifecycle Callbacks

static getDerivedStateFromProps

There is no equivalent in Lit as props and state are both the same class properties

shouldComponentUpdate

  • Lit equivalent is shouldUpdate
  • Called on first render unlike React
  • Similar in function to React's shouldComponentUpdate

getSnapshotBeforeUpdate

In Lit, getSnapshotBeforeUpdate is similar to both update and willUpdate

willUpdate

  • Called before update
  • Unlike getSnapshotBeforeUpdate , willUpdate is called before render
  • Changes to reactive properties in willUpdate do not re-trigger the update cycle
  • Good place to compute property values that depend on other properties and are used in the rest of the update process
  • This method is called on the server in SSR, so accessing the DOM is not advised here

update

  • Called after willUpdate
  • Unlike getSnapshotBeforeUpdate , update is called before render
  • Changes to reactive properties in update do not re-trigger the update cycle if changed before calling super.update
  • Good place to capture information from the DOM surrounding the component before the rendered output is committed to the DOM
  • This method is not called on the server in SSR

Other Lit Lifecycle Callbacks

There are several lifecycle callbacks that were not mentioned in the previous section because there is no analog to them in React. They are:

attributeChangedCallback

It is invoked when one of the element's observedAttributes changes. Both observedAttributes and attributeChangedCallback are part of the custom elements spec and implemented by Lit under the hood to provide an attribute API for Lit elements.

adoptedCallback

Invoked when the component is moved to a new document eg from an HTMLTemplateElement 's documentFragment to the main document . This callback is also a part of the custom elements spec and should only be used for advanced use cases when the component changes documents.

Other lifecycle methods and properties

These methods and properties are class members you can call, override, or await to help manipulate the lifecycle process.

updateComplete

This is a Promise that resolves when the element has finished updating as the update and render lifecycles are asynchronous. An example:

async nextButtonClicked() {
  this.step++;
  // Wait for the next "step" state to render
  await this.updateComplete;
  this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

This is a method that should be overridden to customize when updateComplete resolves. This is common when a component is rendering a child component and their render cycles must be in sync. eg,

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

This method is what calls the update lifecycle callbacks. This should generally not be needed except for rare cases where updating must be done synchronously or for custom scheduling.

hasUpdated

This property is true if the component has updated at least once.

isConnected

A part of the custom elements spec, this property will be true if the element is currently attached to the main document tree.

Lit Update Lifecycle Visualization

There are 3 parts to the update lifecycle:

  • Pre-update
  • Обновлять
  • Post-update

Предварительное обновление

A directed acyclic graph of nodes with callback names. constructor to requestUpdate. @property to Property Setter. attributeChangedCallback to Property Setter. Property Setter to hasChanged. hasChanged to requestUpdate. requestUpdate points out to the next, update lifecycle graph.

After requestUpdate , a scheduled update is awaited.

Обновлять

A directed acyclic graph of nodes with callback names. Arrow from previous image of pre-update lifecycle points to performUpdate. performUpdate to shouldUpdate. shouldUpdate points to both ‘complete update if false’ as well as willUpdate. willUpdate to update. update to both render as well as to the next, post-update lifecycle graph. render also points to the next, post-update lifecycle graph.

Post-Update

A directed acyclic graph of nodes with callback names. Arrow from previous image of update lifecycle points to firstUpdated. firstUpdated to updated. updated to updateComplete.

Крючки

Why hooks

Hooks were introduced into React for simple function component use cases that required state. In many simple cases function components with hooks tend to be much simpler and more readable than their class component counterparts. Though, when introducing asynchonous state updates as well as passing data between hooks or effects, the hooks pattern tends to not suffice, and a class-based solution like reactive controllers tend to shine.

API request hooks & controllers

It is common to write a hook that requests data from an API. For example, take this React function component that does the following:

  • index.tsx
    • Renders text
    • Renders useAPI 's response
      • User ID + User name
      • Сообщение об ошибке
        • 404 when reaches user 11 (by design)
        • Abort error if API fetch is aborted
      • Сообщение о загрузке
    • Renders an action button
      • Next user: which fetches the API for the next user
      • Cancel: which aborts the API fetch and displays an error
  • useApi.tsx
    • Defines a useApi custom hook
    • Will async fetch a user object from an api
    • Emits:
      • Имя пользователя
      • Whether the fetch is loading
      • Любые сообщения об ошибках
      • A callback to abort the fetch
    • Aborts fetches in progress if dismounted

Here is the Lit + Reactive Controller implementation .

Основные выводы:

  • Reactive Controllers are most like custom hooks
  • Passing non-renderable data between callbacks and effects
    • React uses useRef to pass data between useEffect and useCallback
    • Lit uses a private class property
    • React is essentially mimicking the behavior of a private class property

Additionally, if you really like the React function component syntax with hooks but the same buildless environment of Lit, the Lit team highly recommends the Haunted library.

Дети

Default Slot

When HTML elements are not given a slot attribute, they are assigned to the default unnamed slot. In the example below, MyApp will slot one paragraph into a named slot. The other paragraph will default to the unnamed slot".

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot></slot>
        </div>
        <div>
          <slot name="custom-slot"></slot>
        </div>
      </section>
   `;
  }
}

@customElement("my-app")
export class MyApp extends LitElement {
  render() {
    return html`
      <my-element>
        <p slot="custom-slot">
          This paragraph will be placed in the custom-slot!
        </p>
        <p>
          This paragraph will be placed in the unnamed default slot!
        </p>
      </my-element>
   `;
  }
}

Slot Updates

When the structure of slot descendants change, a slotchange event is fired. A Lit component can bind an event-listener to a slotchange event. In the example below, the first slot found in the shadowRoot will have their assignedNodes logged to the console on slotchange .

@customElement("my-element")
export class MyElement extends LitElement {
  onSlotChange(e: Event) {
    const slot = this.shadowRoot.querySelector('slot');
    console.log(slot.assignedNodes({flatten: true}));
  }

  render() {
    return html`
      <section>
        <div>
          <slot @slotchange="{this.onSlotChange}"></slot>
        </div>
      </section>
   `;
  }
}

Ссылки

Генерация эталонных данных

Lit and React both expose a reference to an HTMLElement after their render functions have been called. But it's worth reviewing how React and Lit compose the DOM that is later returned through a Lit @query decorator or a React Reference.

React is a functional pipeline that creates React Components not HTMLElements. Because a Ref is declared before an HTMLElement is rendered, a space in memory is allocated. This is why you see null as the initial value of a Ref, because the actual DOM element hasn't yet been created (or rendered) ie useRef(null) .

After ReactDOM converts a React Component into an HTMLElement, it looks for an attribute called ref in the ReactComponent. If available, ReactDOM places the HTMLElement's reference to ref.current .

LitElement uses the html template tag function from lit-html to compose a Template Element under the hood. LitElement stamps the template's contents to a custom element's shadow DOM after render. The shadow DOM is a scoped DOM tree encapsulated by a shadow root. The @query decorator then creates a getter for the property which essentially performs a this.shadowRoot.querySelector on the scoped root.

Query Multiple Elements

In the example below, the @queryAll decorator will return the two paragraphs in the shadow root as a NodeList .

@customElement("my-element")
export class MyElement extends LitElement {
  @queryAll('p')
  paragraphs!: NodeList;

  render() {
    return html`
      <p>Hello, world!</p>
      <p>How are you?</p>
   `;
  }
}

Essentially, @queryAll creates a getter for paragraphs that returns the results of this.shadowRoot.querySelectorAll() . In JavaScript, a getter can be declared to perform the same purpose:

get paragraphs() {
  return this.renderRoot.querySelectorAll('p');
}

Query Changing Elements

The @queryAsync decorator is better suited to handle a node that can change based on the state of another element property.

In the example below, @queryAsync will find the first paragraph element. However, a paragraph element will only be rendered when renderParagraph randomly generates an odd number. The @queryAsync directive will return a promise that will resolve when the first paragraph is available.

@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
  @queryAsync('p')
  paragraph!: Promise<HTMLElement>;

  renderParagraph() {
    const randomNumber = Math.floor(Math.random() * 10)
    if (randomNumber % 2 === 0) {
      return "";
    }

    return html`<p>This checkbox is checked!`
  }

  render() {
    return html`
      ${this.renderParagraph()}
   `;
  }
}

Mediating State

In React, convention is to use callbacks because state is mediated by React itself. React does it's best to not rely on state provided by elements. The DOM is simply an effect of the rendering process.

External State

It's possible to use Redux, MobX, or any other state management library alongside Lit.

Lit components are created in browser scope. So any library that also exists in browser scope is available to Lit. Many amazing libraries have been built to utilize existing state management systems in Lit.

Here is a series by Vaadin explaining how to leverage Redux in a Lit component.

Take a look at lit-mobx from Adobe to see how a large scale site can leverage MobX in Lit.

Also, check out Apollo Elements to see how developers are including GraphQL in their web components.

Lit works with native browser features and most state management solutions in browser scope can be used in a Lit component.

Стиль

Теневой ДОМ

To natively encapsulate styles and DOM within a Custom Element, Lit uses Shadow DOM . Shadow Roots generate a shadow tree separate from the main document tree. This means that most styles are scoped to this document. Certain styles do leak through such as color, and other font-related styles.

Shadow DOM also introduces new concepts and selectors to the CSS spec:

:host, :host(:hover), :host([hover]) {
  /* Styles the element in which the shadow root is attached to */
}

slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
  /*
   * Styles the elements projected into a slot element. NOTE: the spec only allows
   * styling the direcly slotted elements. Children of those elements are not stylable.
   */
}

Sharing Styles

Lit makes it easy to share styles between components in the form of CSSTemplateResults via css template tags. For example:

// typography.ts
export const body1 = css`
  .body1 {
    ...
  }
`;

// my-el.ts
import {body1} from './typography.ts';

@customElement('my-el')
class MyEl Extends {
  static get styles = [
    body1,
    css`/* local styles come after so they will override bod1 */`
  ]

  render() {
    return html`<div class="body1">...</div>`
  }
}

Тематическое оформление

Shadow roots present a bit of a challenge to conventional theming which typically are top-down style tag approaches. The conventional way to tackle theming with Web Components that use Shadow DOM is to expose a style API via CSS Custom Properties . For example, this is a pattern that Material Design uses:

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

The user would then change the theme of the site by applying custom property values:

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

If top-down theming is a must and you are unable to expose styles, it is always possible to disable Shadow DOM by overriding createRenderRoot to return this which will then render your components' template to the custom element itself rather than to a shadow root attached to the custom element. With this you will lose: style encapsulation, DOM encapsulation, and slots.

Производство

IE 11

If you need to support older browsers like IE 11, you will have to load some polyfills which come out to about another 33kb. More information can be found here .

Conditional Bundles

The Lit team recommends serving two different bundles, one for IE 11 and one for modern browsers. There are several benefits to this:

  • Serving ES 6 is faster and will serve most of your clients
  • Transpiled ES 5 significantly increases bundle size
  • Conditional bundles give you the best of both worlds
    • IE 11 support
    • No slowdown on modern browsers

More info on how to build a conditionally served bundle can be found on our documentation site here .