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

1. Введение

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

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

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

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

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

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

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

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

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

2. Лит против Реакция

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

Это маленький

Lit крошечный: он составляет около 5 КБ, минифицированный и заархивированный по сравнению с 40+ КБ React + ReactDOM .

Гистограмма размера пакета уменьшена и сжата в КБ. Горящая полоса имеет размер 5 КБ, а React + React DOM — 42,2 КБ.

Это быстро

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

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

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

Не требует сборки

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

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

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

Независимость от фреймворка

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

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

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

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

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

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

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

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

Снимок экрана IDE, показывающий предложения IntelliSense

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

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

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

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

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

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

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

Это низкий бай-ин

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

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

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

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

Есть два способа сделать эту кодовую лабораторию:

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

Доступ к коду

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Нажмите кнопку загрузки
  • Извлеките содержимое файла tar в каталог.
  • (Если TS) настройте быстрый 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 и шаблоны

В этом разделе вы познакомитесь с основами шаблонизации в Лит.

JSX и шаблоны Lit

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

Основной синтаксис

В React вы можете визуализировать приветственный мир JSX следующим образом:

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 для группировки нескольких элементов в своих шаблонах.

В Lit шаблоны обернуты шаблоном LIT eral с тегом html , от которого 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" )
  • Устанавливает значение по умолчанию

В Lit вы должны сделать следующее:

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

Освещенный синтаксис привязки реквизита

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
);

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

  • Запишите слово «щелчок» при нажатии на вход
  • Зарегистрируйте значение ввода, когда пользователь вводит символ

В Lit вы должны сделать следующее:

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. Состояние и хуки более подробно рассматриваются в последующих разделах.

Компоненты класса и LitElement

Эквивалентом компонента класса React в Lit является 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 TS, но изначально работает на JavaScript.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Это вызывается всякий раз, когда изменяется какое-либо реактивное свойство.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Это связывает имя тега элемента HTML с определением класса.
  • В соответствии со стандартом пользовательских элементов имя тега должно содержать дефис (-).
  • this в LitElement относится к экземпляру пользовательского элемента (в данном случае <welcome-banner> )
customElements.define('welcome-banner', WelcomeBanner);
  • Это JavaScript-эквивалент декоратора TS @customElement
<head>
  <script type="module" src="./index.js"></script>
</head>
  • Импортирует определение пользовательского элемента.
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • Добавляет пользовательский элемент на страницу
  • Устанавливает свойство name в 'Elliott'

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

Lit не имеет 1:1 интерпретации функционального компонента, поскольку не использует 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. Состояние и жизненный цикл

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

Состояние

Концепция «Реактивных свойств» Лита представляет собой смесь состояния и свойств 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 похож на комбинацию обратных вызовов жизненного цикла firstUpdated и connectedCallback от Lit.

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 ReactDidMount, изменения реактивных свойств в 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 (с использованием английского прошедшего времени «обновление»).
  • В отличие от React, updated также вызывается при первоначальном рендеринге.
  • По функциям аналогичен componentDidUpdate ReactDidUpdate.

componentWillUnmount

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

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • Горящий эквивалент аналогичен 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! Это так», а затем отображает время.
  • Каждую секунду он будет обновлять часы
  • При демонтаже очищает интервал вызова галочки

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

// 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 позволяют функциональным компонентам «подключаться» к состоянию. В этом есть несколько преимуществ.

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

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

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

Концепции хуков React в Lit

Как упоминалось в разделе «Компоненты и реквизиты» , 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 относится к ссылке на пользовательский элемент
  • Свойства класса теперь могут быть созданы как члены класса. Это очищает реализации на основе конструктора.

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

Основные концепции, лежащие в основе хуков, существуют в 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! Это так», а затем отображает время.
  • Каждую секунду он будет обновлять часы
  • При демонтаже очищает интервал вызова галочки

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

Сначала начните с объявления класса компонента и добавьте функцию 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 , но использование класса с constructor , который может принимать интерфейс ReactiveControllerHost , а также любые другие свойства, необходимые для инициализации контроллера, — это шаблон, который команда Lit предпочитает. использовать для большинства основных случаев.

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

  • componentDidMount
    • К connectedCallback LitElement
    • К hostConnected контроллераПодключено
  • ComponentWillUnmount
    • Для disconnectedCallback LitElement
    • К 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. Дети

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

Слоты и Дети

Слоты обеспечивают композицию, позволяя вкладывать компоненты.

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

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

Имейте в виду, что props.children — это компоненты React, а не элементы HTML.

В Lit дочерние элементы компонуются в функции рендеринга с элементами слота . Обратите внимание, что дети не наследуются так же, как React. В Lit дочерние элементы — это элементы HTML, прикрепленные к слотам. Это вложение называется Проекция .

@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 транслируется в серию вызовов функций, которые при вызове создают виртуальный DOM. Этот виртуальный DOM интерпретируется ReactDOM и отображает HTMLElements.

В React ссылки — это пространство в памяти для хранения сгенерированного 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 живет рядом с браузером и создает очень тонкую абстракцию над собственными функциями браузера.

Эквивалентом refs в Lit в React является 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 соответственно. Это JavaScript-эквивалент @query('input') inputEl!: HTMLInputElement;

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

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

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

10. Посредническое государство

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

Многоразовые компоненты

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

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 .
  • Отображает кнопку с меткой +step или -step.
  • Обновляет родительский компонент, вызывая props.addToCounter с props.step в качестве аргумента при щелчке.

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

@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>
    `;
  }
}

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

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

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

Компоненты с отслеживанием состояния

В 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 для step обновления count при каждом щелчке мыши.

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

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

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>
    `;
  }
}

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

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

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

...

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

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

стильКарта

Директива 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 :

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

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

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

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

Теги стиля

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

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

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

JSX и шаблоны

Освещенный и виртуальный DOM

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

Вот пример буквального шаблона:

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

В приведенном выше примере тег - это функция tag , а функция f возвращает вызов буквального шаблона.

Многое из магии производительности в горе происходит от того факта, что строковые массивы, передаваемые в функцию тега, имеют одинаковый указатель (как показано во второй console.log ). Браузер не воссоздает новый массив strings на каждом вызове функции тега, потому что он использует один и тот же буквальный буквальный (т.е. в одном и том же месте в AST). Таким образом, связывание, синтаксическое и шаблон LIT LIT может воспользоваться этими функциями без особых накладных расходов во время выполнения.

Это встроенное поведение браузера Tagged Template Leatrals дает горит довольно преимущество в производительности. Большинство обычных виртуальных DOMS выполняют большую часть своей работы в JavaScript. Тем не менее, Tagged Template Literals делают большую часть своего различия в C ++ браузера.

Если вы хотите начать использовать Tagged Tagged Literals HTML с React или Preact, команда Lit рекомендует библиотеку htm .

Хотя, как и в случае с сайтом Google CodeLabs и несколькими редакторами онлайн -кода, вы заметите, что выделение литерального синтаксиса с меткой шаблона не очень распространено. Некоторые идентификаторы и текстовые редакторы поддерживают их по умолчанию, такие как atom и github's Codeblock Highlighter. Команда Lit также очень тесно сотрудничает с сообществом, чтобы поддерживать такие проекты, как lit-plugin , который представляет собой плагин VS-кода, который добавит синтаксис-выделение, проверку типов и Intellisense к вашим Lit Projects.

Lit & JSX + React Dom

JSX не работает в браузере и вместо этого использует препроцессор для преобразования вызовов функции JSX в JAVAScript (обычно через Babel).

Например, Babel преобразует это:

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

Lit-HTML использует Tagged Template Literals, которые могут работать в браузере без транспиляции или препроцессора. Это означает, что для того, чтобы начать работу с Lit, все, что вам нужно, это HTML -файл, скрипт модуля ES и сервер. Вот полностью управляемый браузером сценарий:

<!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>

Кроме того, поскольку система шаблона LIT Lit-HTML не использует обычный виртуальный DOM, а скорее использует DOM API напрямую, размер Lit 2 находится под 5 кб-мБ и GZED по сравнению с React (2,8 КБ) + React-Dom (39,4 КБ). 40 КБ заслуженно и заморожено.

События

React использует синтетическую систему событий. Это означает, что React-Dom должен определять каждое событие, которое будет использоваться на каждом компоненте, и предоставлять слушатель Camelcase Event, эквивалентный для каждого типа узла. В результате JSX не имеет метода для определения слушателя событий для пользовательского события, и разработчики должны использовать ref , а затем применить слушателя. Это создает опыт разработчика подразделения при интеграции библиотек, которые не имеют в виду, что приводит к тому, что приводит к тому, что необходимость писать специфичную для реагирования обертку.

Lit-HTML непосредственно обращается к DOM и использует нативные события, поэтому добавление слушателей событий так же просто, как @event-name=${eventNameListener} . Это означает, что меньше разбора времени выполнения делается для добавления слушателей событий, а также для стрельбы.

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

React Components и пользовательские элементы

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

Некоторые преимущества пользовательские элементы имеют в качестве компонентной системы:

  • Уроженец браузера и не требует никаких инструментов
  • Вписать в каждый API браузера от innerHTML и document.createElement к querySelector
  • Обычно можно использовать через рамки
  • Может быть лениво зарегистрироваться в customElements.define и «Hydrate» dom

Некоторые недостатки пользовательских элементов сравнивают с компонентами реагирования:

  • Не могу создать пользовательский элемент без определения класса (таким образом, нет JSX-подобных функциональных компонентов)
  • Должен содержать закрывающий тег
    • Примечание. Несмотря на то, что поставщики браузеров разработчика, как правило, сожалеют о спецификации самостоятельного тега, поэтому новые спецификации, как правило, не включают самозакрывающие теги
  • Представляет дополнительный узел дерева DOM, который может вызвать проблемы с макетом
  • Должен быть зарегистрирован через JavaScript

Lits пошел с пользовательскими элементами по модернизированной системе элементов, потому что пользовательские элементы встроены в браузер, и команда Lit считает, что преимущества перекрестной рамы перевешивают преимущества, предоставляемые слоем абстракции компонентов. Фактически, усилия Lit Team в пространстве Lit-SSR преодолели основные проблемы с регистрацией JavaScript. Кроме того, некоторые компании, такие как GitHub, используют пользовательскую личную регистрацию Lazy, чтобы постепенно улучшить страницы с помощью дополнительного таланта.

Передача данных в пользовательские элементы

Распространенным заблуждением с пользовательскими элементами является то, что данные могут передаваться только как строки. Это заблуждение, вероятно, исходит из того факта, что атрибуты элементов могут быть написаны только как строки. Хотя это правда, что LIT будет сдавать атрибуты строк к их определенным типам, пользовательские элементы также могут принимать сложные данные в качестве свойств.

Например, учитывая следующее определение литлета:

// 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>`;
  }
}

num примитивное реактивное свойство, которое будет преобразовать строковое значение атрибута в number , а затем введена сложная структура данных с attribute:false , который деактивирует обработку атрибутов Lit.

Это как передавать данные в этот пользовательский элемент:

<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>

Государство и жизненный цикл

Другие обратные вызовы жизни реагируют

static getDerivedStateFromProps

Нет эквивалента, как реквизит, а состояние - это одно и то же классовое свойства

shouldComponentUpdate

  • Горит эквивалент shouldUpdate
  • Призвал к первым визуализациям в отличие от React
  • Аналогично по функции, чтобы реагировать shouldComponentUpdate

getSnapshotBeforeUpdate

В Lit, getSnapshotBeforeUpdate аналогична как update , так и willUpdate

willUpdate

  • Вызвано до update
  • В отличие от getSnapshotBeforeUpdate , willUpdate вызывается перед render
  • Изменения в реактивных свойствах в willUpdate не повторно запускают цикл обновления
  • Хорошее место для вычисления значений свойств, которые зависят от других свойств и используются в остальной части процесса обновления
  • Этот метод вызывается на сервере в SSR, поэтому доступ к DOM здесь не рекомендуется

update

  • Вызвано после willUpdate
  • В отличие от getSnapshotBeforeUpdate , update вызывается перед render
  • Изменения в реактивных свойствах в update не повторно запускайте цикл обновления, если изменяется перед вызовом super.update
  • Хорошее место для захвата информации из DOM, окружающего компонент до того, как отображаемый вывод
  • Этот метод не вызывается на сервере в SSR

Другие обратные вызовы Lit Lifecycle

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

attributeChangedCallback

Это вызывается, когда изменяется один из элементов observedAttributes . Как observedAttributes , так и attributeChangedCallback являются частью спецификации пользовательских элементов и реализованы Lit под капюшоном, чтобы обеспечить API атрибута для освещенных элементов.

adoptedCallback

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

Другие методы и свойства жизненного цикла

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

updateComplete

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

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

getUpdateComplete

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

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

performUpdate

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

hasUpdated

Это свойство true , если компонент обновил хотя бы один раз.

isConnected

Часть спецификации пользовательских элементов, это свойство будет true , если элемент в настоящее время прикреплен к основному дереву документов.

Обновление обновления визуализация жизненного цикла

В жизненном цикле обновления есть 3 части:

  • Pre-update
  • Обновлять
  • После обновления

Pre-update

Направленный ациклический график узлов с именами обратного вызова. Конструктор для запроса update. @Property to Setter. attributeChangeDcallback к установке свойств. Собственность свойства в HASCHANGED. привлечен к запросу. requestUpdate указывает на следующее, обновить график жизненного цикла.

После requestUpdate ожидается запланированное обновление.

Обновлять

Направленный ациклический график узлов с именами обратного вызова. Стрелка из предыдущего изображения точек жизненного цикла Pre-Update для выполнения. Выполнить посадку в WORDUPDATE. Должен быть указан как на «полное обновление, если неверно», так и Willupdate. Willupdate для обновления. Обновление как для рендеринга, так и для следующего, пост-разумного жизненного цикла жизненного цикла. рендерин также указывает на следующий график жизненного цикла после обновления.

После обновления

Направленный ациклический график узлов с именами обратного вызова. Стрелка из предыдущего изображения обновления жизненного цикла указывает на FirstUpdated. Первоначально, чтобы обновить. Обновлено до UpdateCoplete.

Крючки

Почему крючки

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

API -запросы крючки и контроллеры

Обычно писать крючок, который запрашивает данные из API. Например, возьмите этот компонент функции React , который выполняет следующее:

  • index.tsx
    • Образует текст
    • Отвечает ответу useAPI
      • Идентификатор пользователя + имя пользователя
      • Сообщение об ошибке
        • 404 при достижении пользователя 11 (по дизайну)
        • Ошибка прерывания, если API Fetch прерван
      • Загрузка сообщения
    • Образует кнопку действия
      • Следующий пользователь: который получает API для следующего пользователя
      • Отмена: который прерывает API и отображает ошибку
  • useApi.tsx
    • Определяет индивидуальный крючок useApi
    • Async извлечет пользовательский объект из API
    • Излучает:
      • Имя пользователя
      • Загружается ли выборка
      • Любые сообщения об ошибках
      • Обратный вызов, чтобы прервать выборку
    • Прерывает избрать в процессе

Вот реализация реагирующего контроллера Lit + .

Вынос:

  • Реактивные контроллеры больше всего похожи на индивидуальные крючки
  • Передача неразличиваемых данных между обратными вызовами и эффектами
    • React использует useRef для передачи данных между useEffect и useCallback
    • LIT использует собственность частного класса
    • Реакция по существу имитирует поведение собственности частного класса

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

Дети

Слот по умолчанию

Когда HTML -элементы не получают атрибут slot , они присваиваются неназванному слоту по умолчанию. В приведенном ниже примере MyApp вставит один абзац в именованный слот. Другой абзац по умолчанию в неназванный слот ".

@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>
   `;
  }
}

Обновления слотов

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

Ссылки

Справочный поколение

Горит и реагировать оба выставьте ссылку на HTMlelement после того, как их функции render были вызваны. Но стоит просмотреть, как React and Lit составьте DOM, который позже возвращается через маленький декоратор @query или справочник React.

React - это функциональный трубопровод, который создает компоненты React, а не HTMlelements. Поскольку рефери объявляется до того, как будет отображаться HTMlelement, распределяется пространство в памяти. Вот почему вы видите null как начальное значение рефери, потому что фактический элемент DOM еще не был создан (или отображается), т. Е. useRef(null) .

После того, как Reactdom преобразует реагированный компонент в HTMlelement, он ищет атрибут, называемый ref в ReactComponent. Если доступно, Reactdom помещает ссылку HTMleLement на ref.current .

LiteLement использует функцию метки шаблона html от Lit-HTML для составления элемента шаблона под капотом. Litelement отмечает содержимое шаблона в теневую DOM пользовательского элемента после рендеринга. The Shadow Dom - это дерево Dom, инкапсулированное корнем тени. Затем декоратор @query создает Getter для собственности, которая по сути выполняет this.shadowRoot.querySelector на корне смельку.

Запросить несколько элементов

В приведенном ниже примере декоратор @queryAll вернет два абзаца в корне Shadow в качестве 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>
   `;
  }
}

По сути, @queryAll создает Getter для paragraphs , которые возвращают результаты this.shadowRoot.querySelectorAll() . В JavaScript можно объявить, что выполнять ту же цель:

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

Элементы изменения запроса

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

В приведенном ниже примере @queryAsync найдет первый элемент абзаца. Тем не менее, элемент абзаца будет отображаться только тогда, когда renderParagraph случайным образом генерирует нечетное число. Директива @queryAsync вернет обещание, которое будет решено, когда будет доступен первый абзац.

@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()}
   `;
  }
}

Посреднивание состояния

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

Внешнее состояние

Можно использовать Redux, Mobx или любую другую библиотеку управления государством, вместе с Lit.

Lit Components создаются в сфере браузера. Таким образом, любая библиотека, которая также существует в сфере охвата браузера, доступна для Lit. Многие удивительные библиотеки были построены для использования существующих систем управления государством в лит.

Вот серия Ваадина, объясняющая, как использовать Redux в освещенном компоненте.

Взгляните на Lit-Mobx от Adobe, чтобы увидеть, как крупномасштабный сайт может использовать MOBX в голени.

Кроме того, проверьте элементы Apollo , чтобы увидеть, как разработчики включают GraphQL в свои веб -компоненты.

Lit Works с собственными функциями браузера и большинством решений по управлению состоянием в сфере охвата браузера можно использовать в освещенном компоненте.

Стиль

Тень ДОМ

Чтобы изначально инкапсулировать стили и DOM в пользовательском элементе, LIT использует Shadow Dom . Корни тени генерируют теневое дерево отдельно от основного дерева документов. Это означает, что большинство стилей подключены к этому документу. Некоторые стили протекают, такие как цвет и другие стили, связанные с шрифтом.

Shadow Dom также вводит новые концепции и селекторы для спецификации CSS:

: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.
   */
}

Обмен стилями

LIT позволяет легко делиться стилями между компонентами в виде CSSTemplateResults через теги шаблонов css . Например:

// 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 DOM, - это выявить стиль API с помощью пользовательских свойств CSS . Например, это шаблон, который использует дизайн материала:

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

Пользователь затем изменит тему сайта, применяя пользовательские значения свойств:

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

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

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

Т.е. 11

Если вам нужно поддерживать более старые браузеры, такие как IE 11, вам придется загрузить несколько полифиллов, которые выходят примерно на 33 КБ. Больше информации можно найти здесь .

Условные пучки

Горит команда рекомендует подавать две разные пачки, одну для IE 11 и одну для современных браузеров. Есть несколько преимуществ этого:

  • Обслуживание ES 6 быстрее и будет обслуживать большинство ваших клиентов
  • Транспилированный ES 5 значительно увеличивает размер пакета
  • Условные пучки дают вам лучшее из обоих миров
    • Т.е. 11 поддержка
    • Нет замедления современных браузеров

Более подробную информацию о том, как построить кондиционерный пакет, можно найти на нашем сайте документации здесь .