Lit for React Developers

1. Wprowadzenie

Co to jest Lit

Lit to prosta biblioteka do tworzenia szybkich i lekkich komponentów internetowych, które działają w dowolnej platformie lub bez niej. Za pomocą Lit możesz tworzyć komponenty, aplikacje, systemy projektowe i inne elementy, które można udostępniać.

Czego się nauczysz

Jak przetłumaczyć na Lit kilka pojęć z Reacta, takich jak:

  • JSX i szablony
  • Komponenty i rekwizyty
  • Stan i cykl życia
  • Elementy przykuwające uwagę
  • Dzieci
  • Odsyłacze
  • Stan pośredniczenia

Co utworzysz

Po ukończeniu tego laboratorium dowiesz się, jak przekształcać koncepcje komponentów React na ich odpowiedniki w Lit.

Czego potrzebujesz

  • najnowsza wersja przeglądarki Chrome, Safari, Firefox lub Edge;
  • Znajomość HTML-a, CSS-a, JavaScriptu i Narzędzi deweloperskich w Chrome.
  • Znajomość React
  • (Zaawansowane) Jeśli chcesz mieć jak najlepsze środowisko programistyczne, pobierz VS Code. Potrzebujesz też lit-plugin do VS Code i NPM.

2. Lit a React

Pod wieloma względami podstawowe koncepcje i możliwości Lit są podobne do Reacta, ale Lit ma też kilka kluczowych różnic i wyróżników:

Jest mały

Lit jest niewielki: po zminimalizowaniu i skompresowaniu ma około 5 KB, a React + ReactDOM – ponad 40 KB.

Wykres słupkowy przedstawiający rozmiar pakietu po zminimalizowaniu i skompresowaniu w kilobajtach. Lit bar ma 5 KB, a React + React DOM – 42,2 KB.

Szybko

W publicznych testach porównawczych, w których system szablonów Lit, lit-html, jest porównywany z wirtualnym DOM Reacta, lit-html jest o 8–10% szybszy od Reacta w najgorszym przypadku i o ponad 50%szybszy w bardziej typowych zastosowaniach.

LitElement (podstawowa klasa bazowa komponentów Lit) dodaje minimalny narzut do lit-html, ale przewyższa wydajność Reacta o 16–30%, jeśli chodzi o funkcje komponentów, takie jak wykorzystanie pamięci oraz czas interakcji i uruchamiania.

pogrupowany wykres słupkowy porównujący wydajność biblioteki Lit i React w milisekundach (im mniejsza wartość, tym lepiej);

Nie wymaga kompilacji

Dzięki nowym funkcjom przeglądarki, takim jak moduły ES i oznaczone literały szablonów, Lit nie wymaga kompilacji do działania. Oznacza to, że środowiska deweloperskie można skonfigurować za pomocą tagu skryptu, przeglądarki i serwera, a potem od razu zacząć pracę.

W przypadku modułów ES i nowoczesnych sieci CDN, takich jak Skypack czy UNPKG, możesz nawet nie potrzebować NPM, aby zacząć!

Jeśli jednak chcesz, możesz nadal tworzyć i optymalizować kod Lit. Ostatnie ujednolicenie środowiska deweloperskiego wokół natywnych modułów ES było korzystne dla Lit – Lit to po prostu zwykły JavaScript, więc nie ma potrzeby używania interfejsów CLI specyficznych dla frameworka ani obsługi kompilacji.

Niezależne od platformy

Komponenty Lit są oparte na zestawie standardów internetowych o nazwie Web Components. Oznacza to, że komponent utworzony w Lit będzie działać w obecnych i przyszłych frameworkach. Jeśli obsługuje elementy HTML, obsługuje też komponenty internetowe.

Jedynym problemem z współdziałaniem platform jest ograniczone wsparcie dla DOM. React to jeden z takich frameworków, ale umożliwia on stosowanie obejść za pomocą referencji. Referencje w React nie są jednak wygodne dla programistów.

Zespół Lit pracuje nad eksperymentalnym projektem o nazwie @lit-labs/react, który będzie automatycznie analizować komponenty Lit i generować otoczkę React, dzięki czemu nie będziesz musiał używać odwołań.

Dodatkowo na stronie Custom Elements Everywhere znajdziesz informacje o tym, które frameworki i biblioteki dobrze współpracują z elementami niestandardowymi.

Pełna obsługa TypeScriptu

Chociaż cały kod Lit można napisać w JavaScript, Lit jest napisany w TypeScript i zespół Lit zaleca, aby deweloperzy również używali TypeScriptu.

Zespół Lit współpracuje ze społecznością Lit, aby pomagać w utrzymywaniu projektów, które zapewniają sprawdzanie typów TypeScript i funkcję IntelliSense w szablonach Lit zarówno w czasie programowania, jak i czasie kompilacji za pomocą lit-analyzerlit-plugin.

Zrzut ekranu IDE pokazujący nieprawidłowe sprawdzanie typu podczas ustawiania wartości logicznej na liczbę

Zrzut ekranu IDE z sugestiami funkcji IntelliSense

Narzędzia dla programistów są wbudowane w przeglądarkę

Komponenty Lit to po prostu elementy HTML w DOM. Oznacza to, że aby sprawdzić komponenty, nie musisz instalować żadnych narzędzi ani rozszerzeń w przeglądarce.

Wystarczy otworzyć narzędzia deweloperskie, wybrać element i sprawdzić jego właściwości lub stan.

Obraz narzędzi deweloperskich w Chrome pokazujący, że $0 zwraca <mwc-textfield>, $0.value zwraca hello world, $0.outlined zwraca true, a {$0} pokazuje rozwinięcie właściwości

Został zaprojektowany z myślą o renderowaniu po stronie serwera (SSR).

Biblioteka Lit 2 została zaprojektowana z myślą o obsłudze renderowania po stronie serwera. W momencie pisania tego przewodnika zespół Lit nie udostępnił jeszcze narzędzi SSR w stabilnej wersji, ale już wdraża komponenty renderowane po stronie serwera w usługach Google i testuje SSR w aplikacjach React. Zespół Lit planuje wkrótce udostępnić te narzędzia w usłudze GitHub.

Postępy zespołu Lit możesz śledzić tutaj.

Wymaga niewielkich nakładów

Korzystanie z Lit nie wymaga dużego zaangażowania. Możesz tworzyć komponenty w Lit i dodawać je do istniejącego projektu. Jeśli Ci się nie spodobają, nie musisz od razu przekształcać całej aplikacji, ponieważ komponenty internetowe działają w innych platformach.

Czy masz już całą aplikację w Lit i chcesz przejść na inną platformę? W takim przypadku możesz umieścić obecną aplikację Lit w nowych ramach i przenieść do komponentów nowych ram wszystko, co chcesz.

Wiele nowoczesnych platform obsługuje też dane wyjściowe w komponentach internetowych, co oznacza, że zwykle mogą one mieścić się w elemencie Lit.

3. Konfigurowanie i poznawanie Playgroundu

Ten codelab możesz przejść na 2 sposoby:

  • Możesz to zrobić w całości online w przeglądarce.
  • (Zaawansowane) Możesz to zrobić na komputerze lokalnym za pomocą VS Code.

Dostęp do kodu

W tym przewodniku znajdziesz linki do platformy Lit Playground, np. takie:

Plac zabaw to piaskownica kodu, która działa w całości w przeglądarce. Może kompilować i uruchamiać pliki TypeScript i JavaScript, a także automatycznie rozpoznawać importy do modułów węzła, np.

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

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

Cały samouczek możesz przejść w środowisku Lit Playground, używając tych punktów kontrolnych jako punktów wyjścia. Jeśli używasz VS Code, możesz pobrać kod początkowy dla dowolnego kroku, a także sprawdzić swoją pracę za pomocą tych punktów kontrolnych.

Poznawanie interfejsu podświetlonego placu zabaw

Pasek kart selektora plików jest oznaczony jako Sekcja 1, sekcja edycji kodu jako Sekcja 2, podgląd danych wyjściowych jako Sekcja 3, a przycisk ponownego wczytywania podglądu jako Sekcja 4.

Zrzut ekranu interfejsu Lit Playground z wyróżnionymi sekcjami, których będziesz używać w tym module.

  1. Selektor plików. Zwróć uwagę na przycisk plusa...
  2. Edytor plików.
  3. Podgląd kodu.
  4. Przycisk Załaduj ponownie.
  5. Przycisk pobierania.

Konfiguracja VS Code (zaawansowana)

Oto zalety korzystania z tej konfiguracji VS Code:

  • Sprawdzanie typu szablonu
  • Inteligentne podpowiedzi i autouzupełnianie w szablonach

Jeśli masz już zainstalowane NPM i VS Code (z wtyczką lit-plugin) i wiesz, jak korzystać z tego środowiska, możesz po prostu pobrać te projekty i je uruchomić, wykonując te czynności:

  • Naciśnij przycisk pobierania
  • Wyodrębnij zawartość pliku tar do katalogu.
  • (Jeśli TS) skonfiguruj szybki plik tsconfig, który generuje moduły ES i ES2015+
  • Zainstaluj serwer deweloperski, który może rozwiązywać specyfikatory modułów podstawowych (zespół Lit zaleca @web/dev-server).
  • Uruchom serwer deweloperski i otwórz przeglądarkę (jeśli używasz @web/dev-server, możesz użyć npx web-dev-server --node-resolve --watch --open).
    • Jeśli używasz przykładu package.json, użyj npm run dev

4. JSX i szablony

W tej sekcji poznasz podstawy tworzenia szablonów w Lit.

Szablony JSX i Lit

JSX to rozszerzenie składni JavaScript, które umożliwia użytkownikom Reacta łatwe pisanie szablonów w kodzie JavaScript. Szablony Lit służą podobnemu celowi: wyrażaniu interfejsu komponentu jako funkcji jego stanu.

Podstawowa składnia

W Reactcie kod JSX „hello world” renderuje się w ten sposób:

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

W przykładzie powyżej znajdują się 2 elementy i zmienna „name”. W Lit wykonasz te czynności:

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

Zwróć uwagę, że szablony Lit nie wymagają fragmentu React do grupowania wielu elementów w szablonach.

W Lit szablony są opakowane w html oznaczony szablon LITeral, od którego pochodzi nazwa Lit.

Wartości szablonu

Szablony Lit mogą akceptować inne szablony Lit, zwane TemplateResult. Na przykład umieść name w tagach kursywy (<i>) i otocz go oznaczonym szablonem literału N.B. Pamiętaj, aby użyć znaku grawisu (`), a nie apostrofu (').

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 TemplateResults może akceptować tablice, ciągi znaków, inne TemplateResults oraz dyrektywy.

W ramach ćwiczenia spróbuj przekonwertować poniższy kod React na 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
);

Odpowiedź:

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

Przekazywanie i ustawianie właściwości

Jedną z największych różnic między składnią JSX i Lit jest składnia wiązania danych. Weźmy na przykład to pole wejściowe React z powiązaniami:

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

W powyższym przykładzie zdefiniowano dane wejściowe, które wykonują te czynności:

  • Ustawia wartość zmiennej „disabled” (w tym przypadku „false”).
  • Ustawia klasę na static-class plus zmienna (w tym przypadku "static-class my-class")
  • Ustawia wartość domyślną

W Lit wykonasz te czynności:

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

W przykładzie Lit dodano powiązanie logiczne, aby przełączać atrybut disabled.

Następnie następuje powiązanie bezpośrednio z atrybutem class, a nie z className. Do atrybutu class można dodać wiele powiązań, chyba że używasz dyrektywy classMap, która jest deklaratywnym pomocnikiem do przełączania klas.

Na koniec wejściu przypisywana jest właściwość value. W przeciwieństwie do Reacta nie spowoduje to ustawienia elementu wejściowego jako tylko do odczytu, ponieważ jest zgodne z natywną implementacją i działaniem elementu wejściowego.

Składnia wiązania lit prop

html`<my-element ?attribute-name=${booleanVar}>`;
  • Prefiks ? to składnia wiązania służąca do przełączania atrybutu w elemencie.
  • Równowartość ceny wynoszącej inputRef.toggleAttribute('attribute-name', booleanVar)
  • Przydatne w przypadku elementów, które używają disabled, ponieważ disabled="false" jest nadal odczytywane przez interfejs DOM jako wartość „true” (prawda), ponieważ inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • Prefiks . to składnia wiązania służąca do ustawiania właściwości elementu.
  • Równowartość ceny wynoszącej inputRef.propertyName = anyVar
  • Dobre rozwiązanie do przekazywania złożonych danych, takich jak obiekty, tablice lub klasy.
html`<my-element attribute-name=${stringVar}>`;
  • Wiązanie z atrybutem elementu
  • Równowartość ceny wynoszącej inputRef.setAttribute('attribute-name', stringVar)
  • Dobrze sprawdza się w przypadku wartości podstawowych, selektorów reguł stylu i metody querySelector

Przekazywanie modułów obsługi

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

W powyższym przykładzie zdefiniowano dane wejściowe, które wykonują te czynności:

  • Rejestrowanie słowa „click” po kliknięciu pola
  • Rejestrowanie wartości danych wejściowych, gdy użytkownik wpisuje znak

W Lit wykonasz te czynności:

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

W przykładzie Lit do zdarzenia click dodano detektor z funkcją @click.

Następnie zamiast onChange używane jest powiązanie z natywnym input zdarzeniem elementu <input>, ponieważ natywne change zdarzenie jest wywoływane tylko w przypadku elementu blur (React abstrahuje te zdarzenia).

Składnia modułu obsługi zdarzeń Lit

html`<my-element @event-name=${() => {...}}></my-element>`;
  • Prefiks @ to składnia wiązania detektora zdarzeń.
  • Równowartość ceny wynoszącej inputRef.addEventListener('event-name', ...)
  • Używa natywnych nazw zdarzeń DOM

5. Komponenty i rekwizyty

W tej sekcji dowiesz się więcej o komponentach i funkcjach klas Lit. Stan i hooki omówimy bardziej szczegółowo w dalszej części tego artykułu.

Komponenty klasowe i LitElement

Odpowiednikiem komponentu klasy React w Lit jest LitElement, a koncepcja „właściwości reaktywnych” w Lit to połączenie właściwości i stanu w React. Na przykład:

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

W przykładzie powyżej znajduje się komponent React, który:

  • Renderuje name
  • Ustawia domyślną wartość name na pusty ciąg znaków ("").
  • Ponownie przypisuje name do "Elliott"

Tak to wygląda w LitElement

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

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

W pliku HTML:

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

Omówienie tego, co się dzieje w przykładzie powyżej:

@property({type: String})
name = '';
  • Definiuje publiczną właściwość reaktywną – część publicznego interfejsu API komponentu.
  • Udostępnia atrybut (domyślnie) oraz właściwość w komponencie.
  • Określa, jak przetłumaczyć atrybut komponentu (który jest ciągiem znaków) na wartość.
static get properties() {
  return {
    name: {type: String}
  }
}
  • Spełnia tę samą funkcję co dekorator @property TS, ale działa natywnie w JavaScript.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Jest ona wywoływana za każdym razem, gdy zmieni się dowolna właściwość reaktywna.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Powiązanie nazwy tagu elementu HTML z definicją klasy
  • Zgodnie ze standardem Custom Elements nazwa tagu musi zawierać łącznik (-).
  • this w LitElement odnosi się do instancji elementu niestandardowego (w tym przypadku <welcome-banner>).
customElements.define('welcome-banner', WelcomeBanner);
  • Jest to odpowiednik dekoratora @customElement TS w JavaScript.
<head>
  <script type="module" src="./index.js"></script>
</head>
  • Importuje definicję elementu niestandardowego.
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • Dodaje element niestandardowy do strony.
  • Ustawia właściwość name na 'Elliott'.

Komponenty funkcyjne

Lit nie ma odpowiednika komponentu funkcyjnego, ponieważ nie używa JSX ani preprocesora. Można jednak łatwo utworzyć funkcję, która przyjmuje właściwości i na ich podstawie renderuje DOM. Na przykład:

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

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

W języku litewskim będzie to:

import {html, render} from 'lit';

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

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

6. Stan i cykl życia

W tej sekcji dowiesz się więcej o stanie i cyklu życia komponentu Lit.

Stan

Koncepcja „właściwości reaktywnych” w Lit to połączenie stanu i właściwości Reacta. Właściwości reaktywne po zmianie mogą wywoływać cykl życia komponentu. Właściwości reaktywne występują w 2 wariantach:

Publiczne właściwości reaktywne

// 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';
}
  • Zdefiniowane przez @property
  • Podobne do właściwości i stanu w React, ale modyfikowalne
  • Publiczny interfejs API, do którego dostęp mają użytkownicy komponentu i który jest przez nich ustawiany.

Wewnętrzny stan reaktywny

// 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';
}
  • Zdefiniowane przez @state
  • Podobny do stanu w React, ale modyfikowalny
  • Prywatny stan wewnętrzny, do którego zwykle uzyskuje się dostęp z poziomu komponentu lub podklas.

Lifecycle

Cykl życia Lit jest dość podobny do cyklu życia React, ale są między nimi pewne istotne różnice.

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';
  }
}
  • Odpowiednik w litrach to również constructor.
  • Nie musisz niczego przekazywać do superwywołania.
  • Wywoływane przez (niepełna lista):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Jeśli na stronie znajduje się nazwa nieuaktualnionego tagu, a definicja jest wczytana i zarejestrowana za pomocą funkcji @customElement lub customElements.define.
  • Podobny w działaniu do funkcji constructor w React

render

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

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Odpowiednik w litrach to również render.
  • Może zwracać dowolny wynik renderowania, np. TemplateResult lub string itp.
  • Podobnie jak w przypadku Reacta, render() powinna być funkcją czystą.
  • Renderowanie w węźle, który zwróci createRenderRoot() (domyślnie ShadowRoot).

componentDidMount

componentDidMount jest podobny do połączenia wywołań zwrotnych cyklu życia firstUpdatedconnectedCallback w bibliotece Lit.

firstUpdated

import Chart from 'chart.js';

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

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • Wywoływana po raz pierwszy, gdy szablon komponentu jest renderowany w jego elemencie głównym.
  • Funkcja zostanie wywołana tylko wtedy, gdy element jest połączony, np.nie zostanie wywołana za pomocą document.createElement('my-component'), dopóki węzeł nie zostanie dołączony do drzewa DOM.
  • To dobre miejsce na skonfigurowanie komponentu, które wymaga renderowania DOM przez komponent.
  • W przeciwieństwie do Reacta zmiany we właściwościach reaktywnych w firstUpdated powodują ponowne renderowanie, chociaż przeglądarka zwykle łączy zmiany w tej samej ramce.componentDidMount Jeśli te zmiany nie wymagają dostępu do DOM elementu głównego, zwykle powinny być umieszczane w willUpdate

connectedCallback

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

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • Wywoływana za każdym razem, gdy element niestandardowy jest wstawiany do drzewa DOM.
  • W przeciwieństwie do komponentów React, gdy elementy niestandardowe są odłączane od DOM, nie są niszczone, dlatego można je „łączyć” wielokrotnie.
    • firstUpdated nie zostanie ponownie wywołana.
  • Przydatne do ponownego inicjowania DOM lub ponownego dołączania detektorów zdarzeń, które zostały wyczyszczone po odłączeniu.
  • Uwaga: funkcja connectedCallback może być wywoływana przed firstUpdated, więc podczas pierwszego wywołania DOM może być niedostępny.

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);
  }
}
  • Odpowiednik w języku polskim to updated.
  • W przeciwieństwie do Reacta funkcja updated jest wywoływana również podczas początkowego renderowania.
  • Podobny do funkcji componentDidUpdate w React

componentWillUnmount

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

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • Równowartość w litrach jest podobna do disconnectedCallback
  • W przeciwieństwie do komponentów React, gdy elementy niestandardowe są odłączane od DOM, komponent nie jest niszczony.
  • W przeciwieństwie do componentWillUnmount, disconnectedCallback jest wywoływane po usunięciu elementu z drzewa.
  • DOM w jednostce głównej jest nadal dołączony do poddrzewa jednostki głównej.
  • Przydatne do usuwania detektorów zdarzeń i odwołań powodujących wycieki pamięci, aby przeglądarka mogła zebrać nieużywane elementy.

Ćwiczenia

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

W powyższym przykładzie mamy prosty zegar, który wykonuje te czynności:

  • Wyświetla „Hello World! Jest" i wyświetla godzinę.
  • Co sekundę będzie aktualizować zegar
  • Po odłączeniu usuwa interwał wywołujący tick.

Najpierw zadeklaruj klasę komponentu:

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

Następnie zainicjuj zmienną date i zadeklaruj ją jako wewnętrzną właściwość reaktywną za pomocą @state, ponieważ użytkownicy komponentu nie będą ustawiać zmiennej date bezpośrednio.

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

Następnie wyrenderuj szablon.

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

Teraz zastosuj metodę zaznaczania.

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

Następnie należy wdrożyć componentDidMount. Analog Lit to mieszanina firstUpdatedconnectedCallback. W przypadku tego komponentu wywołanie tick z parametrem setInterval nie wymaga dostępu do DOM w elemencie głównym. Dodatkowo interwał zostanie wyczyszczony, gdy element zostanie usunięty z drzewa dokumentu, więc jeśli zostanie ponownie dołączony, interwał będzie musiał się rozpocząć od nowa. Dlatego w tym przypadku lepszym wyborem jest 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
  );
}

Na koniec wyczyść interwał, aby nie wykonywał działania po odłączeniu elementu od drzewa dokumentu.

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

Po połączeniu wszystkich elementów powinno to wyglądać tak:

// 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. Elementy przykuwające uwagę

W tej sekcji dowiesz się, jak przełożyć koncepcje React Hook na Lit.

Pojęcia dotyczące hooków React

Hooki Reacta umożliwiają komponentom funkcyjnym „podłączanie się” do stanu. Ma to kilka zalet.

  • Ułatwiają ponowne używanie logiki stanowej.
  • Pomoc w podzieleniu komponentu na mniejsze funkcje

Dodatkowo skupienie się na komponentach opartych na funkcjach rozwiązało pewne problemy z syntaksą opartą na klasach w React, takie jak:

  • Musisz przejść z constructor do super przez props
  • Nieuporządkowana inicjalizacja właściwości w constructor
    • Był to powód podany przez zespół Reacta, ale został rozwiązany przez ES2019.
  • Problemy spowodowane tym, że this nie odnosi się już do komponentu

Koncepcje dotyczące hooków React w Lit

Jak wspomnieliśmy w sekcji Komponenty i właściwości, Lit nie umożliwia tworzenia elementów niestandardowych z funkcji, ale LitElement rozwiązuje większość głównych problemów z komponentami klasowymi React. Na przykład:

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

Jak Lit rozwiązuje te problemy?

  • constructor nie przyjmuje argumentów
  • Wszystkie powiązania @event są automatycznie powiązane z this
  • this w większości przypadków odnosi się do odwołania do elementu niestandardowego.
  • Właściwości klasy można teraz tworzyć jako elementy klasy. Upraszcza to implementacje oparte na konstruktorach.

Kontrolery reaktywne

Podstawowe koncepcje związane z funkcjami Hooks w Lit są realizowane przez reaktywne kontrolery. Wzorce kontrolera reaktywnego umożliwiają udostępnianie logiki stanowej, dzielenie komponentów na mniejsze, bardziej modułowe części, a także podłączanie się do cyklu aktualizacji elementu.

Kontroler reaktywny to interfejs obiektu, który można podłączyć do cyklu życia aktualizacji hosta kontrolera, takiego jak LitElement.

Cykl życia ReactiveControllerreactiveControllerHost wygląda tak:

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

Jeśli utworzysz kontroler reaktywny i dołączysz go do hosta za pomocą addController, cykl życia kontrolera będzie wywoływany równolegle z cyklem życia hosta. Przypomnij sobie na przykład zegar z sekcji Stan i cykl życia:

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

W powyższym przykładzie znajduje się prosty zegar, który wykonuje te czynności:

  • Wyświetla „Hello World! Jest" i wyświetla godzinę.
  • Co sekundę będzie aktualizować zegar
  • Po odłączeniu usuwa interwał wywołujący tick.

Tworzenie szkieletu komponentu

Najpierw zacznij od deklaracji klasy komponentu i dodaj funkcję 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);

Tworzenie kontrolera

Teraz przełącz się na clock.ts i utwórz zajęcia dla ClockController oraz skonfiguruj 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() {
  }
}

Kontroler reaktywny można utworzyć w dowolny sposób, o ile udostępnia interfejs ReactiveController, ale zespół Lit w większości podstawowych przypadków woli używać klasy z interfejsem constructor, który może przyjmować interfejs ReactiveControllerHost, a także inne właściwości potrzebne do zainicjowania kontrolera.

Teraz musisz przetłumaczyć wywołania zwrotne cyklu życia React na wywołania zwrotne kontrolera. W skrócie:

  • componentDidMount
    • do connectedCallback LitElement
    • Do kontrolera hostConnected
  • ComponentWillUnmount
    • do disconnectedCallback LitElement
    • Do kontrolera hostDisconnected

Więcej informacji o przekładaniu cyklu życia React na cykl życia Lit znajdziesz w sekcji Stan i cykl życia.

Następnie zaimplementuj wywołanie zwrotne hostConnected i metody tick oraz wyczyść interwał w hostDisconnected, tak jak w przykładzie w sekcji Stan i cykl życia.

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

Korzystanie z kontrolera

Aby użyć kontrolera zegara, zaimportuj go i zaktualizuj komponent w index.ts lub 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);

Aby użyć kontrolera, musisz utworzyć jego instancję, przekazując odwołanie do hosta kontrolera (czyli komponentu <my-element>), a następnie użyć kontrolera w metodzie render.

Wywoływanie ponownego renderowania w kontrolerze

Zwróć uwagę, że wyświetla się godzina, ale nie jest ona aktualizowana. Dzieje się tak, ponieważ kontroler ustawia datę co sekundę, ale host jej nie aktualizuje. Dzieje się tak, ponieważ date zmienia się w klasie ClockController, a nie w komponencie. Oznacza to, że po ustawieniu wartości date na kontrolerze host musi otrzymać polecenie uruchomienia cyklu aktualizacji z wartością host.requestUpdate().

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

Zegar powinien teraz tykać.

Bardziej szczegółowe porównanie typowych przypadków użycia z hakami znajdziesz w sekcji Tematy zaawansowane – haki.

8. Dzieci

W tej sekcji dowiesz się, jak używać slotów do zarządzania elementami podrzędnymi w Lit.

Automaty i dzieci

Sloty umożliwiają kompozycję, ponieważ pozwalają zagnieżdżać komponenty.

W React komponenty podrzędne są dziedziczone przez właściwości. Domyślny slot to props.children, a funkcja render określa jego położenie. Na przykład:

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

Pamiętaj, że props.children to komponenty React, a nie elementy HTML.

W Lit komponenty podrzędne są tworzone w funkcji renderowania za pomocą elementów slot. Zauważ, że elementy podrzędne nie są dziedziczone w taki sam sposób jak w przypadku React. W Lit elementy podrzędne to elementy HTML dołączone do slotów. Ten załącznik nazywa się Prognoza.

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

Wiele przedziałów

W React dodanie wielu slotów jest w zasadzie tym samym co odziedziczenie większej liczby właściwości.

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

Podobnie dodanie większej liczby elementów <slot> tworzy więcej miejsc w Lit. Zdefiniowano wiele miejsc docelowych z atrybutem name: <slot name="slot-name">. Dzięki temu dzieci mogą zadeklarować, które miejsce zostanie im przypisane.

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

Treści domyślne w miejscu docelowym

Gniazda będą wyświetlać swoje poddrzewo, gdy nie będą do nich rzutowane żadne węzły. Gdy węzły są rzutowane na gniazdo, gniazdo nie wyświetla poddrzewa, tylko rzutowane węzły.

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

Przypisywanie dzieci do miejsc

W React elementy podrzędne są przypisywane do miejsc za pomocą właściwości komponentu. W przykładzie poniżej elementy React są przekazywane do właściwości headerChildren i sectionChildren.

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

W Lit dzieci są przypisywane do slotów za pomocą atrybutu 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>
   `;
  }
}

Jeśli nie ma domyślnego gniazda (np. <slot>) ani gniazda z atrybutem name (np. <slot name="foo">), który pasuje do atrybutu slot elementów podrzędnych elementu niestandardowego (np. <div slot="foo">), węzeł nie zostanie wyświetlony.

9. Odsyłacze

Czasami deweloper może potrzebować dostępu do interfejsu API elementu HTMLElement.

W tej sekcji dowiesz się, jak uzyskiwać odwołania do elementów w Lit.

Odwołania w React

Komponent React jest transpilowany do serii wywołań funkcji, które po wywołaniu tworzą wirtualny DOM. Wirtualny DOM jest interpretowany przez ReactDOM i renderuje elementy HTML.

W React odwołania to miejsce w pamięci, w którym znajduje się wygenerowany element 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>
 );
};

W przykładzie powyżej komponent React wykona te czynności:

  • Wyświetl puste pole tekstowe i przycisk z tekstem.
  • Ustawianie fokusu na polu wprowadzania po kliknięciu przycisku

Po początkowym renderowaniu React ustawi wartość inputRef.current na wygenerowaną wartość HTMLInputElement za pomocą atrybutu ref.

Lit „References” with @query

Lit działa blisko przeglądarki i tworzy bardzo cienką warstwę abstrakcji nad natywnymi funkcjami przeglądarki.

Odpowiednikiem elementu refs w bibliotece React w bibliotece Lit jest element HTMLElement zwracany przez dekoratory @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>
   `;
  }
}

W przykładzie powyżej komponent Lit wykonuje te czynności:

  • Definiuje właściwość w MyElement za pomocą dekoratora @query (tworząc getter dla HTMLInputElement).
  • Deklaruje i dołącza wywołanie zwrotne zdarzenia kliknięcia o nazwie onButtonClick.
  • Ustawia fokus na polu wejściowym po kliknięciu przycisku.

W JavaScript dekoratory @query@queryAll wykonują odpowiednio działania querySelectorquerySelectorAll. Jest to odpowiednik JavaScriptu dla @query('input') inputEl!: HTMLInputElement;

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

Gdy komponent Lit zatwierdzi szablon metody render w korzeniu my-element, dekorator @query umożliwi inputEl zwrócenie pierwszego elementu input znalezionego w korzeniu renderowania. Jeśli @query nie może znaleźć określonego elementu, zwraca wartość null.

Jeśli w korzeniu renderowania było kilka elementów input, funkcja @queryAll zwróci listę węzłów.

10. Stan pośredniczenia

Z tej sekcji dowiesz się, jak zapośredniczać stan między komponentami w Lit.

Komponenty wielokrotnego użytku

React naśladuje funkcjonalne potoki renderowania z przepływem danych od góry do dołu. Rodzice przekazują stan dzieciom za pomocą rekwizytów. Komponenty podrzędne komunikują się z komponentami nadrzędnymi za pomocą wywołań zwrotnych znajdujących się w propsach.

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


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

W przykładzie powyżej komponent React wykonuje te czynności:

  • Tworzy etykietę na podstawie wartości props.step.
  • Renderuje przycisk z etykietą +step lub -step.
  • Aktualizuje komponent nadrzędny, wywołując funkcję props.addToCounter z argumentem props.step po kliknięciu.

Chociaż w Lit można przekazywać wywołania zwrotne, konwencjonalne wzorce są inne. Komponent React z przykładu powyżej można zapisać jako komponent Lit w przykładzie poniżej:

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

W przykładzie powyżej komponent Lit wykona te czynności:

  • Utwórz właściwość reaktywną step
  • Wysyłanie zdarzenia niestandardowego o nazwie update-counter, które po kliknięciu zawiera wartość elementu step.

Zdarzenia przeglądarki są przekazywane z elementów podrzędnych do elementów nadrzędnych. Zdarzenia umożliwiają dzieciom transmitowanie zdarzeń interakcji i zmian stanu. React przekazuje stan w przeciwnym kierunku, więc komponenty React rzadko wysyłają i nasłuchują zdarzeń w taki sam sposób jak komponenty Lit.

Komponenty stanowe

W React często używa się hooków do zarządzania stanem. MyCounter Komponent można utworzyć, ponownie wykorzystując CounterButton Komponent. Zwróć uwagę, że wartość addToCounter jest przekazywana do obu instancji 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>
 );
};

Powyższy przykład wykonuje te czynności:

  • Tworzy stan count.
  • Tworzy wywołanie zwrotne, które dodaje liczbę do count stanu.
  • CounterButton używa addToCounter, aby aktualizować countstep przy każdym kliknięciu.

Podobną implementację MyCounter można uzyskać w Lit. Zwróć uwagę, że parametr addToCounter nie jest przekazywany do funkcji counter-button. Zamiast tego wywołanie zwrotne jest powiązane jako detektor zdarzeń ze zdarzeniem @update-counter w elemencie nadrzędnym.

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

Powyższy przykład wykonuje te czynności:

  • Tworzy właściwość reaktywną o nazwie count, która będzie aktualizować komponent, gdy zmieni się jej wartość.
  • Wiąże wywołanie zwrotne addToCounter z detektorem zdarzeń @update-counter.
  • Aktualizuje count, dodając wartość znalezioną w detail.step zdarzenia update-counter.
  • Ustawia wartość counter-button step za pomocą atrybutu step.

W Lit bardziej konwencjonalne jest używanie właściwości reaktywnych do przekazywania zmian z elementów nadrzędnych do podrzędnych. Podobnie warto używać systemu zdarzeń przeglądarki, aby przekazywać szczegóły od dołu do góry.

To podejście jest zgodne ze sprawdzonymi metodami i z celem biblioteki Lit, jakim jest zapewnienie obsługi komponentów internetowych na wielu platformach.

11. Styl

W tej sekcji dowiesz się więcej o stylach w Lit.

Styl

Lit oferuje wiele sposobów stylizowania elementów, a także wbudowane rozwiązanie.

Style wbudowane

Lit obsługuje style wbudowane, a także powiązanie z nimi.

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

W powyższym przykładzie znajdują się 2 nagłówki, z których każdy ma styl wbudowany.

Teraz zaimportuj i powiąż obramowanie z grupy border-color.js z pomarańczowym tekstem:

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

...

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

Obliczanie ciągu stylów za każdym razem może być nieco uciążliwe, dlatego Lit oferuje dyrektywę, która w tym pomaga.

styleMap

styleMap Dyrektywa ułatwia używanie JavaScriptu do ustawiania stylów wbudowanych. Na przykład:

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

Powyższy przykład wykonuje te czynności:

  • Wyświetla ikonę h1 z obramowaniem i selektorem kolorów.
  • Zmienia border-color na wartość z selektora kolorów.

Dodatkowo istnieje element styleMap, który służy do ustawiania stylów elementu h1. styleMap ma składnię podobną do składni wiązania atrybutów style w React.

CSSResult

Zalecany sposób stylizowania komponentów to użycie css oznaczonego literału szablonu.

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

Powyższy przykład wykonuje te czynności:

  • Deklaruje literał szablonu otagowanego CSS z powiązaniem.
  • Ustawia kolory 2 h1 z identyfikatorami.

Zalety używania tagu szablonu css:

  • Analizowanie raz na klasę a raz na instancję
  • Wdrożony z myślą o ponownym wykorzystaniu modułów
  • Możesz łatwo rozdzielić style na osobne pliki.
  • Zgodność z kodem polyfill niestandardowych właściwości CSS

Zwróć też uwagę na tag <style>index.html:

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

Lit ograniczy style komponentów do ich elementów głównych. Oznacza to, że style nie będą się przenikać. Aby przekazywać style do komponentów, zespół Lit zaleca używanie niestandardowych właściwości CSS, ponieważ mogą one przenikać zakres stylów Lit.

Tagi stylu

Możesz też po prostu wstawić tagi <style> w szablonach. Przeglądarka usunie duplikaty tych tagów stylu, ale umieszczenie ich w szablonach spowoduje, że będą one analizowane w przypadku każdej instancji komponentu, a nie w przypadku każdej klasy, jak w przypadku szablonu oznaczonego tagiem css. Dodatkowo deduplikacja w przeglądarce jest znacznie szybsza.CSSResult

W przypadku stylów możesz też użyć elementu <link rel="stylesheet"> w szablonie, ale nie jest to zalecane, ponieważ może powodować początkowe miganie niestylizowanych treści (FOUC).

12. Tematy zaawansowane (opcjonalnie)

JSX i szablony

Lit i wirtualny DOM

Lit-html nie zawiera konwencjonalnego wirtualnego DOM, który porównuje każdy węzeł. Zamiast tego wykorzystuje funkcje wydajnościowe wbudowane w specyfikację oznaczonych literałów szablonu ES2015. Oznaczone literały szablonu to ciągi literałów szablonu z dołączonymi do nich funkcjami tagów.

Oto przykład literału szablonu:

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

Oto przykład literału szablonu z tagiem:

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

W powyższym przykładzie tagiem jest funkcja tag, a funkcja f zwraca wywołanie oznaczonego szablonu literału.

Wiele funkcji Lit związanych z wydajnością wynika z faktu, że tablice ciągów znaków przekazywane do funkcji tagu mają ten sam wskaźnik (jak pokazano w drugim console.log). Przeglądarka nie tworzy nowej tablicy strings przy każdym wywołaniu funkcji tagu, ponieważ używa tego samego literału szablonu (czyli w tym samym miejscu w AST). Dzięki temu wiązanie, parsowanie i pamięć podręczna szablonów w Lit mogą korzystać z tych funkcji bez dużego obciążenia związanego z różnicami w czasie działania.

To wbudowane w przeglądarkę działanie oznaczonych literałów szablonu daje bibliotece Lit znaczną przewagę pod względem wydajności. Większość tradycyjnych wirtualnych DOM-ów wykonuje większość pracy w JavaScript. Większość porównań w przypadku oznaczonych literałów szablonu odbywa się jednak w C++ przeglądarki.

Jeśli chcesz zacząć używać literałów szablonu oznaczonych tagami HTML w React lub Preact, zespół Lit zaleca bibliotekę htm.

Jednak, jak w przypadku witryny Google Codelabs i kilku edytorów kodu online, zauważysz, że podświetlanie składni oznaczonych literałów szablonu nie jest zbyt powszechne. Niektóre środowiska IDE i edytory tekstu obsługują je domyślnie, np. Atom i wyróżnianie bloków kodu w GitHubie. Zespół Lit ściśle współpracuje ze społecznością, aby utrzymywać projekty takie jak lit-plugin, czyli wtyczka do VS Code, która dodaje do projektów Lit wyróżnianie składni, sprawdzanie typów i funkcję IntelliSense.

Lit & JSX + React DOM

JSX nie działa w przeglądarce, ale korzysta z preprocesora, który przekształca JSX w wywołania funkcji JavaScript (zwykle za pomocą Babel).

Na przykład Babel przekształci ten kod:

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

w ten sposób:

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

React DOM pobiera dane wyjściowe Reacta i przekształca je w rzeczywisty DOM – właściwości, atrybuty, detektory zdarzeń i wszystko inne.

Lit-html używa oznaczonych literałów szablonu, które można uruchamiać w przeglądarce bez transpilacji ani preprocesora. Oznacza to, że aby rozpocząć pracę z Lit, wystarczy plik HTML, skrypt modułu ES i serwer. Oto skrypt, który można w całości uruchomić w przeglądarce:

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

Dodatkowo system szablonów Lit, lit-html, nie używa konwencjonalnego wirtualnego DOM, ale bezpośrednio korzysta z interfejsu DOM API. Rozmiar Lit 2 po zminimalizowaniu i skompresowaniu wynosi poniżej 5 KB, a w przypadku Reacta (2,8 KB) i react-dom (39,4 KB) jest to 40 KB.

Wydarzenia

React używa systemu zdarzeń syntetycznych. Oznacza to, że react-dom musi zdefiniować każde zdarzenie, które będzie używane w każdym komponencie, i zapewnić odpowiednik w formie detektora zdarzeń w notacji camelCase dla każdego typu węzła. W rezultacie JSX nie ma metody definiowania detektora zdarzeń niestandardowych, więc programiści muszą używać ref, a potem imperatywnie stosować detektor. W przypadku integracji bibliotek, które nie są przeznaczone do Reacta, powoduje to gorsze wrażenia deweloperskie, co zmusza do pisania specjalnych otoczek dla Reacta.

Lit-html ma bezpośredni dostęp do DOM i używa natywnych zdarzeń, więc dodawanie detektorów zdarzeń jest tak proste, jak @event-name=${eventNameListener}. Oznacza to, że podczas dodawania odbiorników zdarzeń i wywoływania zdarzeń wykonywane jest mniej analizowania w czasie działania.

Komponenty i rekwizyty

Komponenty React i elementy niestandardowe

LitElement używa elementów niestandardowych do pakowania komponentów. Elementy niestandardowe wprowadzają pewne kompromisy między komponentami React, jeśli chodzi o komponentyzację (stan i cykl życia są omówione w sekcji Stan i cykl życia).

Oto niektóre zalety elementów niestandardowych jako systemu komponentów:

  • Są one natywne dla przeglądarki i nie wymagają żadnych narzędzi.
  • Dopasowanie do każdego interfejsu API przeglądarki, od innerHTMLdocument.createElement po querySelector
  • Zwykle można ich używać w różnych platformach.
  • Może być leniwie rejestrowany za pomocą customElements.define i „wypełniać” DOM.

Oto niektóre wady elementów niestandardowych w porównaniu z komponentami React:

  • Nie można utworzyć elementu niestandardowego bez zdefiniowania klasy (a tym samym komponentów funkcyjnych podobnych do JSX).
  • Musi zawierać tag zamykający
    • Uwaga: mimo że jest to wygodne dla deweloperów, producenci przeglądarek zwykle żałują specyfikacji tagów zamykających się samodzielnie, dlatego nowsze specyfikacje zwykle ich nie zawierają.
  • Wprowadza dodatkowy węzeł do drzewa DOM, co może powodować problemy z układem.
  • Musi być zarejestrowany za pomocą JavaScriptu.

Zespół Lit zdecydował się na elementy niestandardowe zamiast na system elementów dostosowanych do potrzeb, ponieważ elementy niestandardowe są wbudowane w przeglądarkę. Zespół Lit uważa, że korzyści wynikające z używania różnych platform przeważają nad korzyściami wynikającymi z warstwy abstrakcji komponentów. W rzeczywistości zespół Lit w ramach projektu lit-ssr rozwiązał główne problemy z rejestracją JavaScriptu. Niektóre firmy, np. GitHub, korzystają też z leniwej rejestracji elementów niestandardowych, aby stopniowo ulepszać strony o opcjonalne funkcje.

Przekazywanie danych do elementów niestandardowych

Częstym nieporozumieniem dotyczącym elementów niestandardowych jest to, że dane można przekazywać tylko jako ciągi znaków. To błędne przekonanie wynika prawdopodobnie z faktu, że atrybuty elementów można zapisywać tylko jako ciągi znaków. Chociaż Lit przekształca atrybuty ciągu znaków na zdefiniowane typy, elementy niestandardowe mogą też akceptować złożone dane jako właściwości.

Na przykład przy tej definicji elementu LitElement:

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

Zdefiniowana jest podstawowa właściwość reaktywna num, która przekształca wartość ciągu atrybutu w number, a następnie wprowadzana jest złożona struktura danych z attribute:false, która dezaktywuje obsługę atrybutów Lit.

Aby przekazać dane do tego elementu niestandardowego:

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

Stan i cykl życia

Inne wywołania zwrotne cyklu życia React

static getDerivedStateFromProps

W Lit nie ma odpowiednika, ponieważ właściwości i stan są właściwościami tej samej klasy.

shouldComponentUpdate

  • Równowartość w litrach: shouldUpdate
  • Wywoływany przy pierwszym renderowaniu, w przeciwieństwie do Reacta
  • Podobny do funkcji shouldComponentUpdate w React

getSnapshotBeforeUpdate

W przypadku Lit getSnapshotBeforeUpdate jest podobny zarówno do update, jak i willUpdate.

willUpdate

  • Połączenie zakończone przed update
  • W przeciwieństwie do funkcji getSnapshotBeforeUpdate funkcja willUpdate jest wywoływana przed funkcją render.
  • Zmiany właściwości reaktywnych w willUpdate nie powodują ponownego uruchomienia cyklu aktualizacji.
  • Dobre miejsce do obliczania wartości właściwości, które zależą od innych właściwości i są używane w pozostałej części procesu aktualizacji.
  • Ta metoda jest wywoływana na serwerze w przypadku renderowania po stronie serwera, więc nie zaleca się w niej dostępu do DOM.

update

  • Połączenie wykonane po willUpdate
  • W przeciwieństwie do funkcji getSnapshotBeforeUpdate funkcja update jest wywoływana przed funkcją render.
  • Zmiany właściwości reaktywnych w update nie powodują ponownego uruchomienia cyklu aktualizacji, jeśli zostaną wprowadzone przed wywołaniem super.update.
  • Dobre miejsce do przechwytywania informacji z interfejsu DOM otaczającego komponent przed zatwierdzeniem wyrenderowanych danych wyjściowych w interfejsie DOM.
  • Ta metoda nie jest wywoływana na serwerze w przypadku renderowania po stronie serwera.

Inne wywołania zwrotne cyklu życia Lit

W poprzedniej sekcji nie wspomnieliśmy o kilku wywołaniach zwrotnych cyklu życia, ponieważ nie mają one odpowiedników w React. Są to:

attributeChangedCallback

Jest wywoływana, gdy zmieni się jeden z atrybutów observedAttributes elementu. Zarówno observedAttributes, jak i attributeChangedCallback są częścią specyfikacji elementów niestandardowych i są implementowane przez Lit w tle, aby zapewnić interfejs API atrybutów dla elementów Lit.

adoptedCallback

Wywoływana, gdy komponent zostanie przeniesiony do nowego dokumentu, np. z HTMLTemplateElementdocumentFragment do głównego document. Ta funkcja zwrotna jest też częścią specyfikacji elementów niestandardowych i powinna być używana tylko w zaawansowanych przypadkach, gdy komponent zmienia dokumenty.

Inne metody i właściwości cyklu życia

Te metody i właściwości są elementami klasy, które możesz wywoływać, zastępować lub na które możesz oczekiwać, aby manipulować procesem cyklu życia.

updateComplete

Jest to Promise, która jest rozwiązywana, gdy element zakończy aktualizację, ponieważ cykle życia aktualizacji i renderowania są asynchroniczne. Przykład:

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

getUpdateComplete

Jest to metoda, którą należy zastąpić, aby dostosować moment, w którym updateComplete zostanie rozwiązany. Jest to powszechne, gdy komponent renderuje komponent podrzędny, a ich cykle renderowania muszą być zsynchronizowane, np.

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

performUpdate

Ta metoda wywołuje wywołania zwrotne cyklu życia aktualizacji. Zwykle nie jest to potrzebne, z wyjątkiem rzadkich przypadków, w których aktualizacja musi być przeprowadzona synchronicznie lub w ramach niestandardowego harmonogramu.

hasUpdated

Ta właściwość ma wartość true, jeśli komponent został zaktualizowany co najmniej raz.

isConnected

Ta właściwość, która jest częścią specyfikacji elementów niestandardowych, będzie miała wartość true, jeśli element jest obecnie dołączony do głównego drzewa dokumentu.

Wizualizacja cyklu życia aktualizacji literatury

Cykl życia aktualizacji składa się z 3 etapów:

  • Przed aktualizacją
  • Aktualizuj
  • Po aktualizacji

Przed aktualizacją

Skierowany graf acykliczny węzłów z nazwami wywołań zwrotnych. Konstruktor do requestUpdate. @property do Property Setter. attributeChangedCallback do Property Setter. Funkcja ustawiająca właściwość hasChanged. Funkcja hasChanged do requestUpdate. Funkcja requestUpdate wskazuje następny wykres cyklu życia aktualizacji.

Po requestUpdate oczekiwana jest zaplanowana aktualizacja.

Aktualizuj

Skierowany graf acykliczny węzłów z nazwami wywołań zwrotnych. Strzałka z poprzedniego obrazu punktów cyklu życia przed aktualizacją do performUpdate. performUpdate do shouldUpdate. shouldUpdate do „complete update if false” i willUpdate. willUpdate do update. update do render i do następnego wykresu cyklu życia po aktualizacji. render również do następnego wykresu cyklu życia po aktualizacji.

Po aktualizacji

Skierowany graf acykliczny węzłów z nazwami wywołań zwrotnych. Strzałka od poprzedniego obrazu punktów cyklu życia aktualizacji wskazuje firstUpdated. Od firstUpdated do updated. Od updated do updateComplete.

Elementy przykuwające uwagę

Dlaczego warto używać elementów przyciągających uwagę

W React wprowadzono komponenty funkcyjne, które umożliwiają proste przypadki użycia wymagające stanu. W wielu prostych przypadkach komponenty funkcyjne z hookami są znacznie prostsze i czytelniejsze niż ich odpowiedniki w postaci komponentów klasowych. Jednak w przypadku wprowadzania asynchronicznych aktualizacji stanu oraz przekazywania danych między hookami lub efektami wzorzec hooków zwykle nie wystarcza, a rozwiązanie oparte na klasach, takie jak kontrolery reaktywne, sprawdza się lepiej.

Haczyki i kontrolery żądań do interfejsu API

Często pisze się hooka, który wysyła żądanie danych do interfejsu API. Weźmy na przykład ten komponent funkcji React, który wykonuje te czynności:

  • index.tsx
    • renderuje tekst,
    • Wyświetla odpowiedź useAPI
      • Identyfikator użytkownika + nazwa użytkownika
      • Komunikat o błędzie
        • 404, gdy dociera do użytkownika 11 (zgodnie z założeniami)
        • Błąd przerwania, jeśli pobieranie z interfejsu API zostanie przerwane
      • Wczytywanie wiadomości
    • Renderuje przycisk polecenia.
      • Następny użytkownik: pobiera interfejs API dla następnego użytkownika.
      • Anuluj: przerywa pobieranie danych z interfejsu API i wyświetla błąd.
  • useApi.tsx
    • Definiuje useApi niestandardowy hook
    • Asynchroniczne pobieranie obiektu użytkownika z interfejsu API
    • Emituje:
      • Nazwa użytkownika
      • Czy pobieranie jest w toku
      • wszelkie komunikaty o błędach;
      • Wywołanie zwrotne, które umożliwia przerwanie pobierania.
    • Przerywa trwające pobieranie danych, jeśli komponent zostanie odmontowany.

Oto implementacja kontrolera Lit + Reactive.

Wnioski:

  • Kontrolery reaktywne są najbardziej podobne do niestandardowych hooków.
  • Przekazywanie danych, które nie podlegają renderowaniu, między wywołaniami zwrotnymi a efektami
    • React używa useRef do przekazywania danych między useEffect a useCallback.
    • Lit używa prywatnej właściwości klasy
    • React w zasadzie naśladuje zachowanie prywatnej właściwości klasy.

Jeśli podoba Ci się składnia komponentów funkcyjnych React z hookami, ale chcesz korzystać z tego samego środowiska bez kompilacji co w przypadku Lit, zespół Lit gorąco poleca bibliotekę Haunted.

Dzieci

Domyślny slot

Jeśli elementy HTML nie mają atrybutu slot, są przypisywane do domyślnego nienazwanego przedziału. W przykładzie poniżej funkcja MyApp umieści jeden akapit w nazwanym slocie. Drugi akapit zostanie domyślnie umieszczony w nieokreślonym slocie.

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

Aktualizacje przedziałów

Gdy zmieni się struktura elementów podrzędnych slotu, zostanie wywołane zdarzenie slotchange. Komponent Lit może powiązać detektor zdarzeń ze zdarzeniem slotchange. W przykładzie poniżej w przypadku pierwszego slotu znalezionego w shadowRoot wartość assignedNodes zostanie zarejestrowana w konsoli w 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>
   `;
  }
}

Odsyłacze

Generowanie odwołań

Zarówno Lit, jak i React udostępniają odwołanie do elementu HTMLElement po wywołaniu funkcji render. Warto jednak sprawdzić, jak React i Lit tworzą DOM, który jest później zwracany za pomocą dekoratora Lit @query lub odwołania React.

React to funkcjonalny potok, który tworzy komponenty React, a nie elementy HTML. Ponieważ element Ref jest deklarowany przed wyrenderowaniem elementu HTMLElement, w pamięci jest przydzielane miejsce. Dlatego początkową wartością elementu Ref jest null, ponieważ rzeczywisty element DOM nie został jeszcze utworzony (ani wyrenderowany), czyli useRef(null).

Gdy ReactDOM przekształci komponent Reacta w HTMLElement, wyszuka w nim atrybut o nazwie ref. Jeśli jest dostępny, ReactDOM umieszcza odwołanie do HTMLElement w ref.current.

LitElement używa funkcji tagu szablonu html z lit-html do tworzenia elementu szablonu. Po renderowaniu LitElement umieszcza zawartość szablonu w modelu Shadow DOM elementu niestandardowego. Shadow DOM to drzewo DOM o określonym zakresie, hermetyzowane przez element shadow root. Dekorator @query tworzy następnie getter właściwości, który zasadniczo wykonuje operację this.shadowRoot.querySelector na poziomie głównym w zakresie.

Wysyłanie zapytań do wielu elementów

W przykładzie poniżej dekorator @queryAll zwróci 2 akapi w głównym elemencie podrzędnym jako 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>
   `;
  }
}

W zasadzie @queryAll tworzy funkcję pobierającą dla paragraphs, która zwraca wyniki this.shadowRoot.querySelectorAll(). W JavaScript można zadeklarować funkcję pobierającą, która będzie pełnić tę samą rolę:

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

Elementy zmieniające zapytanie

Dekorator @queryAsync lepiej nadaje się do obsługi węzła, który może się zmieniać w zależności od stanu właściwości innego elementu.

W przykładzie poniżej @queryAsync znajdzie pierwszy element akapitu. Element akapitu będzie jednak renderowany tylko wtedy, gdy funkcja renderParagraph wygeneruje losowo liczbę nieparzystą. Dyrektywa @queryAsync zwróci obietnicę, która zostanie spełniona, gdy pierwszy akapit będzie dostępny.

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

Stan pośredniczenia

W React przyjęło się używać wywołań zwrotnych, ponieważ stan jest zarządzany przez samą bibliotekę React. React stara się nie polegać na stanie dostarczanym przez elementy. DOM jest po prostu efektem procesu renderowania.

Stan zewnętrzny

Możesz używać Redux, MobX lub dowolnej innej biblioteki do zarządzania stanem razem z Lit.

Komponenty Lit są tworzone w zakresie przeglądarki. Dlatego każda biblioteka, która istnieje również w zakresie przeglądarki, jest dostępna dla Lit. W Lit powstało wiele wspaniałych bibliotek, które wykorzystują istniejące systemy zarządzania stanem.

Oto seria artykułów od Vaadin, w której wyjaśniono, jak wykorzystać Redux w komponencie Lit.

Zapoznaj się z biblioteką lit-mobx od Adobe, aby dowiedzieć się, jak duża witryna może wykorzystywać MobX w Lit.

Zapoznaj się też z Apollo Elements, aby dowiedzieć się, jak deweloperzy uwzględniają GraphQL w komponentach internetowych.

Lit współpracuje z natywnymi funkcjami przeglądarki, a większość rozwiązań do zarządzania stanem w zakresie przeglądarki można wykorzystać w komponencie Lit.

Styl

Shadow DOM

Aby natywnie hermetyzować style i DOM w niestandardowym elemencie, Lit używa Shadow DOM. Shadow Roots generują drzewo cieni oddzielone od głównego drzewa dokumentu. Oznacza to, że większość stylów jest ograniczona do tego dokumentu. Niektóre style, takie jak kolor i inne style związane z czcionką, są widoczne.

Shadow DOM wprowadza też do specyfikacji CSS nowe pojęcia i selektory:

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

Udostępnianie stylów

Lit ułatwia udostępnianie stylów między komponentami w postaci CSSTemplateResults za pomocą tagów szablonu css. Na przykład:

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

Motywy

Shadow roots stanowią pewne wyzwanie dla konwencjonalnego motywu, który zwykle jest podejściem do tagów stylów odgórnych. Tradycyjnym sposobem na rozwiązanie problemu z motywami w przypadku komponentów internetowych korzystających z Shadow DOM jest udostępnienie interfejsu API stylu za pomocą niestandardowych właściwości CSS. Oto przykład wzorca używanego w Material Design:

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

Użytkownik może następnie zmienić motyw witryny, stosując wartości właściwości niestandardowych:

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

Jeśli musisz zastosować motywowanie odgórne i nie możesz udostępnić stylów, zawsze możesz wyłączyć Shadow DOM, zastępując createRenderRoot wartością this. Spowoduje to renderowanie szablonu komponentów w samym elemencie niestandardowym, a nie w dołączonym do niego katalogu głównym Shadow DOM. W ten sposób utracisz hermetyzację stylu, hermetyzację DOM i sloty.

Produkcja

IE 11

Jeśli musisz obsługiwać starsze przeglądarki, takie jak IE 11, musisz wczytać niektóre polyfille, które zajmują około 33 KB. Więcej informacji znajdziesz tutaj.

Pakiety warunkowe

Zespół Lit zaleca wyświetlanie dwóch różnych pakietów: jednego dla IE 11 i jednego dla nowoczesnych przeglądarek. Daje to kilka korzyści:

  • Obsługa ES6 jest szybsza i obejmuje większość klientów.
  • Przekompilowany kod ES5 znacznie zwiększa rozmiar pakietu.
  • Pakiety warunkowe łączą zalety obu rozwiązań.
    • Obsługa IE 11
    • Brak spowolnienia w nowoczesnych przeglądarkach

Więcej informacji o tym, jak utworzyć pakiet wyświetlany warunkowo, znajdziesz w naszej dokumentacji tutaj.