Lit for React Developers

1. Wprowadzenie

Co to jest Lit

Lit to prosta biblioteka do tworzenia szybkich i lekkich komponentów sieciowych, które działają na dowolnej platformie lub nie mają żadnej platformy. Za pomocą Lit możesz tworzyć między innymi komponenty, aplikacje i systemy, które można udostępniać.

Czego się nauczysz

Jak przenieść niektóre pojęcia z React do Lit, np.:

  • JSX i szablony
  • Komponenty i rekwizyty
  • Stan i cykl życia
  • Hooks
  • Dzieci
  • Odwołania
  • Stan pośredniczenia

Co utworzysz

Po zakończeniu tego ćwiczenia w Codelabs będzie można przekonwertować koncepcje komponentu React na ich odpowiedniki Lit.

Czego potrzebujesz

  • najnowsza wersja przeglądarki Chrome, Safari, Firefox lub Edge;
  • znajomość HTML, CSS, JavaScriptu i Narzędzi deweloperskich w Chrome;
  • znajomość Reacta;
  • (Zaawansowane) Jeśli chcesz mieć jak najlepsze warunki do programowania, pobierz VS Code. Potrzebna będzie też lit-plugin na potrzeby kodu VS Code i NPM.

2. Lit vs React

Podstawowe koncepcje i możliwości Lita są pod wieloma względami podobne do rozwiązań React, ale cechuje się też kilkoma kluczowymi cechami:

Jest mały

Ilość podświetlenia jest niewielka: ilość treści jest zmniejszona do około 5 KB i spakowana w formacie gzip, natomiast React + ReactDOM: ponad 40 KB.

Wykres słupkowy rozmiaru pakietu po skompresowaniu i zmniejszeniu w KB. Pasek Lit to 5 KB, a model React + React DOM – 42,2 KB.

Szybkość

W publicznych testach porównawczych, które porównują system szablonów Lit, lit-html, z VDOM React, lit-html jest 8–10% szybszy od React w najgorszym przypadku i ponad 50% szybszy w bardziej typowych przypadkach użycia.

LitElement (klasa podstawowa komponentu Lite) w przypadku lit-html ma minimalny narzut, ale przekracza wydajność React o 16–30% przy porównywaniu funkcji komponentu, takich jak wykorzystanie pamięci, interakcje i czas uruchamiania.

grupowany wykres słupkowy skuteczności porównujący lit do React w milisekundach (im mniejsze wartości, tym lepiej)

Nie wymaga kompilacji

Dzięki nowym funkcjom przeglądarki, takim jak moduły ES i otagowane literale szablonów, Lit nie wymaga kompilacji przed uruchomieniem. Oznacza to, że można skonfigurować środowisko programistyczne za pomocą tagu skryptu + przeglądarki + serwer.

Dzięki modułom ES i nowoczesnym sieciom CDN, takim jak Skypack czy UNPKG, być może nie potrzebujesz już NPM, aby zacząć korzystać z usługi.

Jeśli chcesz, nadal możesz tworzyć i optymalizować kod Lit. Niedawna konsolidacja programistów wokół natywnych modułów ES sprawdza się w przypadku platformy – Lit to po prostu zwykły JavaScript i nie trzeba używać interfejsu wiersza poleceń dla konkretnej platformy ani obsługi kompilacji.

Niezależne od platformy

Komponenty Lit opierają się na zestawie standardów internetowych zwanych komponentami internetowymi. Oznacza to, że utworzenie komponentu w Lit będzie działać zarówno w obecnej, jak i przyszłej platformie. Jeśli obsługuje elementy HTML, obsługuje też komponenty internetowe.

Jedynymi problemami dotyczącymi interoperacyjności frameworków są te, w których frameworki mają ograniczone wsparcie dla DOM. React jest jedną z tych platform, ale ta metoda pozwala uciec w sytuacjach dzięki Refs in React.

Zespół Lit pracował nad eksperymentalnym projektem o nazwie @lit-labs/react, który automatycznie analizuje komponenty Lit i generuje zewnętrzny moduł React, dzięki czemu nie musisz używać odwołań.

Dodatkowo narzędzie Custom Elements Everywhere pokazuje, które platformy i biblioteki dobrze współpracują z elementami niestandardowymi.

Pierwsza klasa obsługi formatu TypeScript

Chociaż cały kod Lit można napisać w JavaScript, Lit jest napisany w TypeScript, a zespół Lit zaleca, aby deweloperzy również używali tego języka.

Zespół Lit współpracuje ze społecznością Lit, pomagając w prowadzeniu projektów, w których sprawdzanie typów i analizy typu TypeScript oraz szablony Lit jest możliwe zarówno podczas programowania, jak i budowania za pomocą lit-analyzer i lit-plugin.

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

Zrzut ekranu z IDE z supozycjami Intellisense

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

Komponenty Lit to tylko 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 dla programistów, wybrać element i sprawdzić jego właściwości lub stan.

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

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

Lit 2 został stworzony z myślą o obsługiwaniu żądań zwrotnych. W momencie pisania tego ćwiczenia z programowania zespół Lit nie wprowadził jeszcze narzędzi SSR w wersji stabilnej, ale zespół Lit wdrożył już w usługach Google komponenty renderowane po stronie serwera i przetestował SSR w aplikacjach React. Zespół Lit wkrótce udostępni te narzędzia na GitHubie.

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

niskie koszty wdrożenia,

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 konwertować całej aplikacji naraz, ponieważ komponenty web działają w ramach innych frameworków.

Czy masz już całą aplikację w Lit i chcesz ją zastąpić inną? Można umieścić obecną aplikację Lit wewnątrz nowej platformy i przenieść do niej dowolne elementy do komponentów nowej platformy.

Dodatkowo wiele nowoczesnych frameworków obsługuje dane wyjściowe w komponentach internetowych, co oznacza, że zwykle mogą one pasować do elementu Lit.

3. Konfigurowanie i poznawanie Playground

Możesz wykonać to zadanie na 2 sposoby:

  • Całkowicie możesz to zrobić online, w przeglądarce
  • (Zaawansowane) Możesz to zrobić na komputerze lokalnym za pomocą VS Code.

Uzyskiwanie dostępu do kodu

W trakcie ćwiczeń z programowania dostępne będą takie linki do placu zabaw Lit:

Playground to piaskownica kodu, która działa całkowicie w przeglądarce. Może kompilować i uruchamiać pliki TypeScript i JavaScript, a także automatycznie decydować o importowaniu do modułów węzłów, np.

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

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

Możesz przejść cały samouczek na Lit playground, korzystając z tych punktów kontrolnych jako punktów wyjścia. Jeśli korzystasz z VS Code, możesz użyć tych punktów kontrolnych do pobrania kodu początkowego dla dowolnego kroku, a także do wykorzystania ich do sprawdzenia swojej pracy.

Poznawanie interfejsu oświetlonego placu zabaw

Pasek kart selektora plików ma etykietę Sekcja 1, sekcja edycji kodu ma etykietę Sekcja 2, podgląd wyjścia ma etykietę Sekcja 3, a przycisk ponownego wczytania podglądu ma etykietę Sekcja 4

Zrzut ekranu z interfejsem Lit playground pokazuje sekcje, których użyjesz w tym Codelab.

  1. Selektor plików Zwróć uwagę na przycisk plusa...
  2. Edytor plików.
  3. Podgląd kodu.
  4. Przycisk ponownego załadowania.
  5. Przycisk Pobierz.

Konfiguracja VS Code (zaawansowane)

Oto zalety tej konfiguracji VS Code:

  • Sprawdzanie typu szablonu
  • Analiza szablonów i autouzupełnianie

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

  • Naciśnij przycisk pobierania.
  • Wyodrębnij zawartość pliku tar do katalogu.
  • (W przypadku rozwiązania do rozwiązywania problemów) skonfiguruj szybki plik tsconfig, który zwraca moduły es i kod es2015+
  • Zainstaluj serwer programisty, który obsługuje specyfikatory samego modułu (zespół Lit zaleca adres @web/dev-server).
  • Uruchom serwer deweloperski i otwórz przeglądarkę (jeśli używasz pakietu @web/dev-server, możesz użyć npx web-dev-server --node-resolve --watch --open).
    • Jeśli używasz przykładowego package.json, użyj npm run dev

4. JSX i tworzenie szablonów

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

Szablony JSX i Lit

JSX to rozszerzenie składni JavaScript, które pozwala użytkownikom Reacta na łatwe tworzenie szablonów w kodzie JavaScript. Szablony Lit służą do podobnego celu: wyrażania interfejsu użytkownika komponentu jako funkcji jego stanu.

Podstawowa składnia

W React renderowanie JSX „Witaj, świecie” wygląda tak:

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 tym przykładzie są 2 elementy i uwzględniona zmienna „name”. W Lit wykonaj 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
);

Pamiętaj, że szablony Lit nie wymagają fragmentu React, aby grupować w swoich szablonach wiele elementów.

W Lit szablony są zapakowane html szablonem z tagami LITeralnym – tak się składa, że to tam wzięła swoją nazwę.

Wartości szablonu

Szablony Lit mogą akceptować inne szablony Lit, zwane TemplateResult. Możesz na przykład opakować tag name kursywą (<i>) i zapakować go w literał szablonu N.B.. Pamiętaj, aby użyć znaku grawisu (`), a nie pojedynczego cudzysłowu (').

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

W przypadku ćwiczenia spróbuj przekonwertować ten 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 atrybutów

Jedną z największych różnic między składnią JSX a Lit jest składnia wiązania danych. Na przykład użyj tych danych wejściowych w 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:

  • Ustawia wartość wyłączone na zdefiniowaną zmienną (w tym przypadku fałsz)
  • Ustawia klasę na static-class i zmienną (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 wartości logicznej w celu przełączania atrybutu disabled.

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

W końcu właściwość value jest ustawiana na wejściu. W przeciwieństwie do React nie spowoduje to ustawienia elementu wejściowego w trybie tylko do odczytu, ponieważ jest zgodny z natywną implementacją i zachowaniem danych wejściowych.

Składnia powiązania funkcji Lit prop

html`<my-element ?attribute-name=${booleanVar}>`;
  • Wstęp ? to składnia wiązania służąca do przełączania atrybutu w elemencie
  • Odpowiednik: inputRef.toggleAttribute('attribute-name', booleanVar)
  • Przydatne w przypadku elementów, które używają atrybutu disabled jako disabled="false", są nadal interpretowane jako prawdziwe przez DOM, ponieważ inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • Prefiks . to składnia powiązania służąca do ustawiania właściwości elementu.
  • Odpowiednik: inputRef.propertyName = anyVar
  • Dobrze nadaje się do przekazywania złożonych danych, takich jak obiekty, tablice lub klasy.
html`<my-element attribute-name=${stringVar}>`;
  • Wiązanie z atrybutem elementu
  • Odpowiednik: inputRef.setAttribute('attribute-name', stringVar)
  • Sprawdza się w przypadku wartości podstawowych, selektorów reguł stylu i selektora zapytań

Moduły obsługi przekazywania

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 tym przykładzie zostały zdefiniowane dane wejściowe:

  • Zapisuj słowo „kliknij” po kliknięciu tekstu wejściowego
  • Rejestrowanie wartości danych wejściowych, gdy użytkownik wpisze 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 z parametrem @click został dodany detektor.

Następnie, zamiast używać obiektu onChange, możesz utworzyć powiązanie z natywnym zdarzeniem input przeglądarki <input>, ponieważ natywne zdarzenie change uruchamia się tylko wtedy, gdy jest ustawione zdarzenie blur (wykorzystanie abstraktów z reakcji na te zdarzenia).

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

html`<my-element @event-name=${() => {...}}></my-element>`;
  • Prefiks @ to składnia powiązania detektora zdarzeń
  • Odpowiednik: inputRef.addEventListener('event-name', ...)
  • Używa natywnych nazw zdarzeń DOM.

5. Komponenty i rekwizyty

W tej sekcji poznasz komponenty i funkcje klasy Lit. Stan i funkcje wywołujące omawiamy bardziej szczegółowo w kolejnych sekcjach.

Komponenty klasowe i LitElement

Odpowiednikiem litowym komponentu klasy React jest LitElement. Pojęcie „właściwości reaktywnych” w przypadku Lita to połączenie właściwości i stanu 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:

  • Renderowanie name
  • Ustawia wartość domyślną name na pusty ciąg znaków ("")
  • Przepisuje name na "Elliott"

Oto jak to zrobić 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);

A w pliku HTML:

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

Przegląd tego, co dzieje się w przykładzie powyżej:

@property({type: String})
name = '';
  • Definiuje publiczną właściwość reaktywną – część publicznego interfejsu API komponentu
  • Wyświetla atrybut (domyślnie) oraz właściwość komponentu.
  • Określa, jak przekształcić atrybut komponentu (który jest ciągiem znaków) w wartość.
static get properties() {
  return {
    name: {type: String}
  }
}
  • Pełni taką samą funkcję jak dekorator @property, ale działa natywnie w języku JavaScript
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Jest ona wywoływana po zmianie dowolnej właściwości reaktywnej.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Połączenie nazwy tagu elementu HTML z definicją klasy
  • Zgodnie ze standardem elementów niestandardowych 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 JavaScriptu dekoratora TS @customElement
<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 funkcji

Lit nie interpretuje komponentu funkcji w sposób dokładny, ponieważ nie używa JSX ani preprocesora. Stworzenie funkcji, która przyjmuje właściwości i renderuje DOM na ich podstawie, jest jednak dość proste. Na przykład:

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

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

W przypadku Lit:

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 poznasz stan i cykl życia Lit.

Stan

Pojęcie „właściwości reaktywnych” Lita łączy stan i rekwizyty Reacta. Zmienione właściwości reaktywne mogą aktywować cykl życia komponentu. Właściwości reaktywne występują w 2 wariantach:

Public reactive properties

// 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 zmienne;
  • Publiczny interfejs API dostępny i ustawiany przez konsumentów komponentu

Wewnętrzny stan reakcji

// 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 React, ale zmienny
  • 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 React, ale występują 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 licencie to też constructor
  • Nie musisz przekazywać niczego do super połączenia.
  • Wywoływany przez (niepełna lista):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Jeśli na stronie znajduje się nazwa tagu, która nie została zaktualizowana, a definicja została załadowana i zarejestrowana za pomocą atrybutu @customElement lub customElements.define
  • Funkcja podobna do constructor w React

render

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

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Odpowiednik licencyjny to również render
  • Może zwracać dowolny wynik, który można wyświetlić, np. TemplateResult lub string itp.
  • Podobnie jak w przypadku React, render() powinna być czystą funkcją
  • Wyświetla dane z węzła zwracanego przez funkcję createRenderRoot() (domyślnie ShadowRoot).

componentDidMount

componentDidMount to połączenie wywołań zwrotnych cyklu życia firstUpdatedconnectedCallback.

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 przy pierwszym renderowaniu szablonu komponentu w katalogu głównym komponentu.
  • Jest wywoływany tylko wtedy, gdy element jest połączony, np. nie jest wywoływany za pomocą document.createElement('my-component'), dopóki ten węzeł nie zostanie dołączony do drzewa DOM.
  • To dobre miejsce do konfiguracji komponentu, która wymaga interfejsu DOM renderowanego przez komponent.
  • W przeciwieństwie do zmian componentDidMount we właściwościach reaktywnych w obiekcie firstUpdated powstanie ponowne renderowanie, ale przeglądarka zwykle umieszcza zmiany w tej samej ramce. Jeśli te zmiany nie wymagają dostępu do DOM głównego katalogu, należy zwykle wprowadzić je w interfejsie willUpdate.

connectedCallback

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

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • Wywoływane za każdym razem, gdy element niestandardowy jest wstawiony do drzewa DOM.
  • W przeciwieństwie do komponentów React, gdy elementy niestandardowe zostaną odłączone od DOM, nie są one usuwane, a więc mogą być „połączone” wielokrotnie.
    • Funkcja firstUpdated nie będzie już wywoływana
  • Przydatne do ponownego inicjowania DOM lub ponownego dołączania detektorów zdarzeń, które zostały wyczyszczone po rozłączeniu
  • Uwaga: funkcja connectedCallback może zostać wywołana przed firstUpdated, więc przy pierwszym wywołaniu 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);
  }
}
  • Odpowiednikiem w języku literackim jest updated (używane w języku angielskim w formie czasu przeszłego od „update”).
  • W przeciwieństwie do reakcji w reakcji obiekt updated jest również wywoływany podczas początkowego renderowania.
  • Działa podobnie jak componentDidUpdate React

componentWillUnmount

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

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • Odpowiednik w literaturze jest podobny do tekstu disconnectedCallback
  • W przeciwieństwie do komponentów React, gdy elementy niestandardowe są odłączane od DOM, komponent nie jest usuwany.
  • W przeciwieństwie do componentWillUnmount funkcja disconnectedCallback jest wywoływana po usunięciu elementu z drzewa.
  • DOM w korzenim nadal jest połączony z drzewem podrzędnym
  • Przydatne do czyszczenia detektorów zdarzeń i niestabilnych odwołań, aby przeglądarka mogła usunąć komponent z pamięci podręcznej.

Ćwiczenie

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 następujące czynności:

  • Wyświetli się komunikat „Hello, World! jest”, a potem wyświetla godzinę,
  • Co sekundę będzie aktualizować zegar.
  • Po zdjęciu z zaczepu powoduje wyczyszczenie interwału wywołująco-odpowiadającego tick.

Najpierw zacznij od deklaracji klasy 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 date i oznacz go jako wewnętrzną właściwość reaktywną za pomocą @state, ponieważ użytkownicy komponentu nie będą ustawiać wartości 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 zaimplementuj metodę znaczników.

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

Kolejny krok to implementacja componentDidMount. Analog litu to mieszanina firstUpdatedconnectedCallback. W przypadku tego komponentu wywołanie metody tick za pomocą metody setInterval nie wymaga dostępu do DOM w katalogu głównym. Dodatkowo, gdy element zostanie usunięty z drzewa dokumentu, interwał zostanie wyczyszczony, więc jeśli zostanie ponownie dołączony, interwał będzie musiał zostać rozpoczęty od początku. Dlatego lepszą opcją 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ł znaku wyboru po odłączeniu elementu od drzewa dokumentu.

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

Po połączeniu wszystkiego 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. Hooks

W tej sekcji dowiesz się, jak przenosić koncepcje React Hook do Lit.

Pojęcia związane z hakami w React

Punkty zaczepienia reakcji umożliwiają zaczerpanie się komponentów funkcji ze stanu. Ma to kilka zalet.

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

Komponenty oparte na funkcjach rozwiązywały też pewne problemy ze składnią klasową React, takie jak:

  • Trzeba przesłać przejazd (props) z: constructor do: super
  • Niepożądana inicjalizacja właściwości w: constructor
    • Był to powód podany przez zespół React, ale rozwiązany w ES2019
  • Problemy spowodowane tym, że atrybut this nie odnosi się już do komponentu

Koncepcje funkcji reakcji w Lit

Jak wspomnieliśmy w sekcji Komponenty i rekwizyty, Lit nie oferuje sposobu tworzenia elementów niestandardowych na podstawie funkcji, ale LitElement rozwiązuje większość głównych problemów z komponentami klas 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?

  • Funkcja constructor nie przyjmuje żadnych argumentów.
  • Wszystkie @event powiązań są automatycznie powiązane z elementem this
  • this w większości przypadków odnosi się do odwołania elementu niestandardowego
  • Właściwości klasy mogą teraz być instancjonowane jako elementy klasy. Upraszcza to implementacje konstruktorowe

Kontrolery reaktywne

Podstawowe koncepcje dotyczące uchwytów występują w Lit jako kontrolery reaktywne. Reaktywne wzorce kontrolera pozwalają współużytkować logikę stanową, dzielić komponenty na mniejsze, bardziej modułowe fragmenty oraz łączyć się z cyklem aktualizacji elementu.

Reaktywność kontrolera to interfejs obiektu, który może wykorzystywać cykl aktualizacji kontrolera hosta, np. LitElement.

Cykl życia ReactiveControllerreactiveControllerHost:

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ą funkcji addController, cykl życia kontrolera będzie wywoływany razem z cyklem życia hosta. Przypomnijmy sobie przykład zegara 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 mamy prosty zegar, który wykonuje następujące czynności:

  • Wyświetla ona „Hello World!”. jest”, a potem wyświetla godzinę,
  • Co sekundę będzie aktualizować zegar.
  • Po zdjęciu z zaczepu powoduje wyczyszczenie interwału wywołująco-odpowiadającego tick.

Tworzenie szkieletu komponentu

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 przejdź do clock.ts i utwórz zajęcia dla użytkownika 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() {
  }
}

Reaktywny kontroler można utworzyć w dowolny sposób, o ile ma wspólny interfejs ReactiveController, ale używa klasy z constructor, która może przyjmować interfejs ReactiveControllerHost, a także wszelkie inne właściwości potrzebne do inicjowania kontrolera. To wzorzec, którego zespół Lit preferuje w większości podstawowych przypadków.

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

  • componentDidMount
    • Do LitElement's connectedCallback
    • Do hostConnected kontrolera
  • ComponentWillUnmount
    • Do LitElement's disconnectedCallback
    • Do hostDisconnected kontrolera

Więcej informacji o przekształcaniu cyklu życia React w cykl życia Lit znajdziesz w sekcji Stan i cykl życia.

Następnie zaimplementuj metodę zwracającą wywołanie zwrotne hostConnected i metodę tick oraz wyczyścić interwał w metodie hostDisconnected tak, jak to pokazano 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 kontrolera hosta (który jest komponentem <my-element>), a następnie użyć kontrolera w metodzie render.

Uruchamianie ponownego renderowania w kontrolerze

Zwróć uwagę, że wyświetla się godzina, ale godzina się nie aktualizuje. Dzieje się tak, ponieważ kontroler ustawia datę co sekundę, ale host nie jest aktualizowany. Dzieje się tak, ponieważ date zmienia się w klasie ClockController, a nie w komponencie. Oznacza to, że po ustawieniu na kontrolerze obiektu date musisz poinformować hosta, że ma uruchomić cykl aktualizacji z użyciem host.requestUpdate().

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

Teraz zegar powinien tykać!

Więcej informacji o porównaniu typowych zastosowań z zastosowaniem funkcji hook znajdziesz w sekcji Zaawansowane tematy – hooki.

8. Dzieci

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

Sloty i dzieci

Slots umożliwiają tworzenie kompozycji, ponieważ pozwalają zagnieżdżać komponenty.

W Reactie elementy podrzędne są dziedziczone za pomocą właściwości. Przedział domyślny to props.children, a funkcja render określa, gdzie znajduje się boks domyślny. 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 elementy podrzędne są tworzone w funkcji renderowania z elementami boksu. Zauważ, że elementy podrzędne nie są dziedziczone w taki sam sposób jak w React. W Lit elementy podrzędne to elementy HTMLElements dołączone do boksów. Ten załącznik nazywa się Odwzorowanie.

@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 zasadniczo tym samym, co dziedziczenie 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> spowoduje utworzenie większej liczby slotów w Lit. W atrybucie name zdefiniowano wiele przedziałów: <slot name="slot-name">. Dzięki temu mogą zadeklarować, do którego przedziału czasu zostaną 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>
   `;
  }
}

Domyślna zawartość boksu

Jeśli w danym gnieździe nie ma żadnych węzłów, wyświetla się jego poddrzewo. Gdy węzły są rzutowane na slot, nie wyświetla on swojego poddrzewa, a zamiast tego wyświetla węzły rzutowane.

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

Przypisz elementy podrzędne do przedziałów

W React elementy podrzędne są przypisywane do slotów za pomocą właściwości komponentu. W przykładzie poniżej elementy React są przekazywane do właściwości headerChildrensectionChildren.

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

W Lit elementy podrzędne są przypisywane do przedziałó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 slotu (np. <slot>) i nie ma też slotu, który ma atrybut name (np. <slot name="foo">) pasujący do atrybutu slot elementów podrzędnych elementu niestandardowego (np. <div slot="foo">), ten węzeł nie będzie wyświetlany.

9. Odwołania

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

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

Odwołania w React

Komponent React jest transpilowany w serii wywołań funkcji, które po wywołaniu tworzą wirtualny DOM. Ten wirtualny DOM jest interpretowany przez ReactDOM i renderuje element HTMLElements.

W React referencje to miejsce w pamięci na 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 będzie wykonywać te czynności:

  • Renderowanie pustego pola tekstowego i przycisku z tekstem
  • Zaznacz dane wejściowe po kliknięciu przycisku

Po pierwszym renderowaniu React ustawia wartość inputRef.current na wygenerowaną wartość HTMLInputElement za pomocą atrybutu ref.

Lit „References” with @query

Lit znajduje się blisko przeglądarki i ma bardzo małą abstrakcję względem natywnych funkcji przeglądarki.

Odpowiednikiem klasy refs w Lit jest element HTMLElement zwrócony przez dekoratory @query i @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 wykona te działania:

  • Definiuje właściwość w MyElement przy użyciu dekoratora @query (tworząc metodę pobierania dla HTMLInputElement).
  • Deklaruje i dołącza wywołanie zwrotne zdarzenia kliknięcia o nazwie onButtonClick.
  • Aktywuje dane wejściowe po kliknięciu przycisku

W języku JavaScript dekoratory @query i @queryAll wykonują odpowiednio querySelector i querySelectorAll. To jest odpowiednik JavaScriptu @query('input') inputEl!: HTMLInputElement;

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

Gdy komponent Lit zapisze szablon metody render w korzeniach komponenta my-element, dekorator @query pozwoli komponentowi inputEl zwrócić pierwszy element input znaleziony w korzeniach renderowania. Jeśli @query nie znajdzie podanego elementu, zwróci wartość null.

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

10. Stan pośrednictwa

W 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. Rodzice przekazują stan dzieciom za pomocą rekwizytów. Dzieci komunikują się z rodzicami za pomocą wywołań zwrotnych dostępnych w rekwizytach.

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


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

W tym przykładzie komponent React wykonuje te działania:

  • Tworzy etykietę na podstawie wartości props.step.
  • Renderuje przycisk z etykietą +step lub -step.
  • Aktualizuje komponent nadrzędny przez wywołanie metody props.addToCounter z argumentem props.step jako argumentem przy kliknięciu

Chociaż można przekazywać wywołania zwrotne w Lit, konwencjonalne wzorce są inne. W przykładzie poniżej komponent React może być napisany jako komponent Lit:

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

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

    this.dispatchEvent(event);
  }

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

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

W powyższym przykładzie komponent Lit wykona te działania:

  • Tworzenie usługi reaktywnej step
  • Wyślij zdarzenie niestandardowe o nazwie update-counter z wartością step elementu po kliknięciu

Zdarzenia w przeglądarce przesuwają się od elementów podrzędnych do elementów nadrzędnych. Wydarzenia umożliwiają dzieciom przekazywanie informacji o zdarzeniach interakcji i zmianach stanu. React przekazuje stan w przeciwnym kierunku, więc rzadko zdarza się, aby komponenty React wysyłały i odbierały zdarzenia w taki sam sposób jak komponenty Lit.

Komponenty stanowe

W React do zarządzania stanem często używa się haka. Komponent MyCounter można utworzyć, używając komponentu CounterButton. Zwróć uwagę, że parametr addToCounter jest przekazywany do obu instancji funkcji 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>
 );
};

W przykładzie powyżej:

  • Tworzy stan count.
  • Tworzy wywołanie zwrotne, które dodaje numer do stanu count.
  • CounterButton używa narzędzia addToCounter do aktualizowania wartości count do step po każdym kliknięciu.

Podobną implementację MyCounter można uzyskać w Lit. Zwróć uwagę, że metoda addToCounter nie jest przekazywana do pola counter-button. Wywołanie zwrotne jest natomiast powiązane jako detektor zdarzenia @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>
    `;
  }
}

Przykład powyżej wygląda tak:

  • Tworzy właściwość reaktywną o nazwie count, która aktualizuje komponent po zmianie wartości.
  • Łączy wywołanie zwrotne addToCounter z detektorem zdarzeń @update-counter
  • Aktualizuje pole count, dodając wartość znajdującą się w parametrze detail.step zdarzenia update-counter
  • Ustawia wartość step elementu counter-button za pomocą atrybutu step.

W Lit bardziej konwencjonalne jest używanie właściwości reaktywnych do rozpowszechniania zmian z elementów nadrzędnych na podrzędne. Warto też korzystać z systemu zdarzeń przeglądarki, aby szczegółowe informacje były przekazywane od dołu do góry.

Takie podejście jest zgodne ze sprawdzonymi metodami i zgodnie z celem Lit, którym jest zapewnienie obsługi komponentów internetowych na różnych platformach.

11. Styl

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

Styl

Lit oferuje wiele sposobów określania stylu elementów, a także rozwiązanie wbudowane.

Style wbudowane

Lit obsługuje style wbudowane oraz wiązania 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 są 2 nagłówki, z których każdy ma styl wstawiany w tekście.

Teraz zaimportuj i połącz obramowanie z border-color.js z pomarańczowym tekstem:

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

...

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

Konieczność obliczania ciągu stylu za każdym razem może być irytująca, dlatego Lit oferuje dyrektywę, która w tym pomaga.

styleMap

styleMap Instrukcja ułatwia użycie JavaScriptu do ustawiania stylów wstawianych. 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>
    `;
  }
}

W przykładzie powyżej:

  • Wyświetla h1 z obrzeżeniem i selektorem kolorów
  • Zmienia border-color na wartość z selektora kolorów

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

CSSResult

Zalecanym sposobem stylu komponentów jest użycie literału szablonu otagowanego 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>
    `;
  }
}

W przykładzie powyżej:

  • Deklaruje tagowany szablon CSS z wiązaniem
  • Ustawia kolory 2 elementów h1 z identyfikatorami

Zalety korzystania z tagu szablonu css:

  • Przetwarzanie raz na klasę lub raz na instancję
  • Moduł został zaimplementowany z myślą o możliwości ponownego użycia.
  • Możliwość łatwego rozdzielania stylów na osobne pliki
  • Zgodny z kodem polyfill CSS Custom Właściwości

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

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

Lit pozwala dostosować style komponentów do ich poziomu głównego. Oznacza to, że style nie będą się przenikać. Aby przekazywać style do komponentów, zespół Lit zaleca używanie własnych właściwości CSS, ponieważ mogą one obejmować zakres stylów Lit.

Tagi stylów

Możesz też wstawiać tagi <style> w swojej treści. Przeglądarka usunie duplikaty tych tagów stylu, ale jeśli umieścisz je w szablonach, będą one analizowane dla wystąpienia komponentu, a nie dla klasy, jak ma to miejsce w przypadku szablonu z tagiem css. Dodatkowo w przeglądarce usuwanie duplikatów elementów CSSResult jest znacznie szybsze.

Użycie w szablonie elementu <link rel="stylesheet"> jest też możliwe, ale nie jest zalecane, ponieważ może spowodować początkowe wyświetlenie treści bez stylów (FOUC).

12. Tematy zaawansowane (opcjonalnie)

JSX i szablony

Lit & Virtual DOM

Lit-html nie zawiera konwencjonalnego wirtualnego DOM, który rozróżnia poszczególne węzły. Wykorzystuje w nim funkcje wydajności wchodzące w skład specyfikacji literału szablonu z tagami ES2015. Literały szablonów otagowane to ciągi literału szablonu z dołączonymi 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 otagowanego 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 tagiem jest funkcja tag, a funkcja f zwraca wywołanie literału szablonu oznaczonego tagiem.

Wiele magii wydajności w Lit wynika z faktu, że tablice ciągu znaków przekazywane do funkcji tagu mają ten sam wskaźnik (jak widać w drugim elemencie console.log). Przeglądarka nie odtwarza nowej tablicy strings przy każdym wywołaniu funkcji tagu, ponieważ używa tego samego literału szablonu (tj. w tym samym miejscu w AST). Dzięki temu mechanizmy wiązania, analizowania i zapamiętywania szablonów w Lit mogą korzystać z tych funkcji bez nadmiernego obciążania procesu różnicowania.

To wbudowane w przeglądarce zachowanie otagowanych literałów szablonów zapewnia firmie Lit sporą przewagę pod względem wydajności. Większość konwencjonalnych wirtualnych DOM wykonuje większość pracy w języku JavaScript. Jednak znaczniki w literalach szablonów są głównie porównywane w języku C++ przeglądarki.

Jeśli chcesz zacząć korzystać z literałów szablonów otagowanych HTML w React lub Preact, zespół Lit poleca bibliotekę htm.

Mimo że, podobnie jak w przypadku witryny Google Codelabs i kilku edytorów kodu online, możesz zauważyć, że wyróżnianie składni dosłownej szablonu tagów nie jest zbyt powszechne. Niektóre IDE i edytorzy tekstu obsługują je domyślnie, np. Atom i podświetlanie bloku kodu w GitHub. Zespół Lit ściśle współpracuje też ze społecznością, aby rozwijać projekty takie jak lit-plugin, czyli wtyczka do VS Code, która umożliwia wyróżnianie składni, sprawdzanie typu i intellisense w projektach Lit.

Lit i JSX + React DOM

JSX nie działa w przeglądarce i zamiast tego korzysta z preprocesora do konwersji JSX na wywołania funkcji JavaScriptu (zwykle przez Babel).

Babel na przykład przekształci to:

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

do tego:

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

Następnie interfejs React DOM przenosi dane wyjściowe z React na rzeczywisty model DOM, czyli właściwości, atrybuty, detektory zdarzeń i inne elementy.

Lit-html używa tagowanych literali szablonów, które mogą działać w przeglądarce bez transpilacji lub preprocesora. Oznacza to, że aby rozpocząć korzystanie z Lit, wystarczy plik HTML, skrypt modułu ES i serwer. Oto skrypt, który w pełni działa 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, ponieważ system szablonów Lit, lit-html, nie używa konwencjonalnego wirtualnego DOM, ale korzysta bezpośrednio z interfejsu DOM API, wersja Lit 2 jest zminifikowana i skompresowana do pliku gzip w porównaniu z reactem (2,8 KB) oraz kompresowaniem 40 KB z minifikacją 40 KB.

Wydarzenia

React korzysta z syntetycznego systemu zdarzeń. Oznacza to, że element Reag-dom musi określać każde zdarzenie, które będzie używane w każdym komponencie, oraz udostępnić detektor zdarzeń CamlCase dla każdego typu węzła. W efekcie JSX nie ma metody definiowania odbiornika zdarzenia niestandardowego, a deweloperzy muszą użyć właściwości ref, a następnie bezwzględnie zastosować detektor. Uniemożliwia to deweloperom integrowanie bibliotek, które nie uwzględniają tej technologii, i wymaga napisania kodu związanego z reakcją.

Lit-html ma bezpośredni dostęp do modelu DOM i używa zdarzeń natywnych, więc dodawanie detektorów zdarzeń jest niezwykle proste w @event-name=${eventNameListener}. Oznacza to, że przy dodawaniu detektorów zdarzeń i uruchamianiu zdarzeń wymagane jest mniej analizy środowiska wykonawczego.

Komponenty i rekwizyty

Komponenty React i elementy niestandardowe

Pod gołym niebem LitElement wykorzystuje elementy niestandardowe, by pakować swoje komponenty. Elementy niestandardowe wprowadzają pewne kompromisy między komponentami React w zakresie komponentyzacji (stan i cykl życia są omawiane w sekcji Stan i cykl życia).

Oto kilka zalet systemu komponentów w przypadku elementów niestandardowych:

  • są natywne dla przeglądarki i nie wymagają żadnych narzędzi;
  • Zgodność z interfejsem API każdej przeglądarki od innerHTML i document.createElement do querySelector
  • zazwyczaj można go używać w różnych ramach;
  • Można zarejestrować leniwie za pomocą funkcji customElements.define i modelu DOM „hydrate” (hydrata)

Elementy niestandardowe mają kilka wad w porównaniu z komponentami React:

  • Nie można utworzyć elementu niestandardowego bez zdefiniowania klasy (co oznacza brak komponentów funkcjonalnych podobnych do JSX).
  • Musi zawierać tag zamykający
    • Uwaga: mimo że specyfikacja tagów samozamykających ułatwia pracę programistom, dostawcy przeglądarek nie są z niej zadowoleni, dlatego nowsze specyfikacje zwykle nie zawierają tagów samozamykających.
  • Wprowadza dodatkowy węzeł do drzewa DOM, co może powodować problemy z układem
  • Musi być zarejestrowany za pomocą kodu JavaScript

Lit postawił na elementy niestandardowe nad systemem elementów niestandardowych, ponieważ są one wbudowane w przeglądarkę, a zespół Lit uważa, że korzyści związane z międzyramami przewyższają korzyści wynikające z warstwy abstrakcji komponentów. W fakcie wysiłki zespołu Lit w zakresie lit-ssr pozwoliły rozwiązać główne problemy z rejestracją JavaScript. Poza tym niektóre firmy, takie jak GitHub, wykorzystują leniwą rejestrację elementów niestandardowych, aby stopniowo ulepszać strony za pomocą opcjonalnych dodatków.

Przekazywanie danych do elementów niestandardowych

W przypadku elementów niestandardowych często panuje przekonanie, że dane można przekazywać tylko jako ciągi znaków. To nieporozumienie prawdopodobnie wynika z tego, że atrybuty elementów można zapisać tylko jako ciągi znaków. Chociaż to prawda, że Lit będzie przesyłać atrybuty w postaci ciągów znaków do zdefiniowanych typów, elementy niestandardowe mogą też akceptować złożone dane jako właściwości.

Na przykład o takiej definicji 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 prosta reaktywna właściwość num, która konwertuje wartość ciągu atrybutu na number, a potem wprowadza się złożoną strukturę danych za pomocą attribute:false, która dezaktywuje obsługę atrybutów Lit.

Aby przekazywać 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ż props i state są tymi samymi właściwościami klasy.

shouldComponentUpdate

  • Odpowiednik licencyjny: shouldUpdate
  • Wywoływane przy pierwszym renderowaniu w odróżnieniu od React
  • Funkcja podobna do shouldComponentUpdate w React

getSnapshotBeforeUpdate

W Lit słowo getSnapshotBeforeUpdate jest podobne do zarówno 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 komponencie willUpdate nie powodują ponownego uruchamiania cyklu aktualizacji
  • To dobre miejsce do obliczania wartości właściwości, które zależą od innych właściwości i są używane w dalszej części procesu aktualizacji.
  • Ta metoda jest wywoływana na serwerze w SSR, więc nie zaleca się uzyskiwania dostępu do DOM.

update

  • Wywołane 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 wywołania cyklu aktualizacji przed wywołaniem funkcji super.update
  • Dobre miejsce do przechwytywania informacji z DOM wokół komponentu, zanim wyrenderowany wynik zostanie zapisany w DOM.
  • Ta metoda nie jest wywoływana na serwerze w ramach SSR

Inne wywołania zwrotne cyklu życia Lit

Istnieje kilka wywołań zwrotnych cyklu życia, o których nie wspomnieliśmy w poprzedniej sekcji, ponieważ nie ma dla nich odpowiednika w React. Są to:

attributeChangedCallback

Jest ona wywoływana po zmianie jednego z właściwości observedAttributes elementu. Zarówno observedAttributes, jak i attributeChangedCallback są częścią specyfikacji elementów niestandardowych i zostały zaimplementowane przez Lit, aby zapewnić interfejs API atrybutu dla elementów Lit.

adoptedCallback

Wywoływany, gdy komponent jest przenoszony do nowego dokumentu, np. z HTMLTemplateElement documentFragment do głównego document. Ten wywołanie zwrotne jest też częścią specyfikacji elementów niestandardowych i należy go używać tylko w zaawansowanych przypadkach użycia, gdy komponent zmienia dokumenty.

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

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

updateComplete

Jest to Promise, który rozwiązuje się po zakończeniu aktualizacji elementu, 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

Tą metodę należy zastąpić, aby dostosować czas rozwiązania updateComplete. Jest to typowe, gdy komponent renderuje komponent podrzędny, a cykle renderowania muszą być zsynchronizowane. Przykład:

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, gdy aktualizacje muszą być przeprowadzane synchronicznie lub w przypadku niestandardowego harmonogramu.

hasUpdated

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

isConnected

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

Wizualizacja cyklu życia aktualizacji Lit

Cykl życia aktualizacji składa się z 3 części:

  • Przed aktualizacją
  • Aktualizuj
  • Po aktualizacji

Przed aktualizacją

Kierunkowy graf acykliczny węzłów z nazwami wywołania zwrotnego. konstruktor do requestUpdate. @property do ustawienia usługi. Właściwość Setter do hasChanged. hasChanged do requestUpdate. requestUpdate wskazuje na następny wykres cyklu życia aktualizacji.

Po godzinie requestUpdate nastąpi zaplanowana aktualizacja.

Aktualizuj

Skierowany graf acykliczny węzłów z nazwami wywołania zwrotnego. Strzałka z poprzedniego obrazu cyklu życia przed aktualizacją wskazuje na performUpdate. performUpdate wskazuje na shouldUpdate. shouldUpdate wskazuje zarówno na „complete update if false”, jak i willUpdate. willUpdate wskazuje na update. update wskazuje zarówno na render, jak i na następny wykres cyklu życia po aktualizacji. render wskazuje również na następny wykres cyklu życia po aktualizacji.

Aktualizacja

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

Hooks

Dlaczego warto używać haków

Elementy przykuwające uwagę zostały wprowadzone do React w celu zastosowania prostych komponentów funkcji, które wymagają stanu. W wielu prostych przypadkach komponenty funkcji z hakami są zwykle znacznie prostsze i łatwiejsze do odczytania niż ich odpowiedniki w komponencie klasy. Mimo że przy wprowadzaniu aktualizacji stanu niesynchonologicznego oraz przekazywaniu danych między punktami zaczepienia lub efektami wzór zaczepienia zwykle nie jest wystarczający, a rozwiązanie oparte na klasie, takie jak kontrolery reaktywne, zwykle sprawdza się świetnie.

Węzły i kontrolery żądań interfejsu API

Często zdarza się, że używany jest element przykuwający żądanie danych do interfejsu API. Weźmy na przykład ten komponent funkcji React, który wykonuje te czynności:

  • index.tsx
    • Renderowanie tekstu
    • Renderowanie odpowiedzi użytkownika useAPI
      • Identyfikator użytkownika + nazwa użytkownika
      • Komunikat o błędzie
        • 404, gdy dociera do użytkownika 11 (według projektu)
        • Błąd przerwania, jeśli pobieranie interfejsu API zostało przerwane
      • Wczytywanie wiadomości
    • Renderuje przycisk polecenia
      • Następny użytkownik: który pobiera interfejs API dla następnego użytkownika.
      • Anuluj: anuluje pobieranie interfejsu API i wyświetla błąd.
  • useApi.tsx
    • Definiuje niestandardowy punkt zaczepienia useApi
    • asynchroniczne pobieranie obiektu użytkownika z interfejsu API.
    • Emituje:
      • Nazwa użytkownika
      • Wskazuje, czy pobieranie się wczytuje
      • jakiekolwiek komunikaty o błędach.
      • Wywołanie zwrotne polegające na przerwaniu pobierania
    • Przerywa pobieranie, jeśli urządzenie zostanie odłączone.

Oto implementacja kontrolera reaktywnego i Lit.

Wnioski:

  • Elementy sterujące reagujące na zmiany są najbardziej podobne do niestandardowych haka.
  • Przekazywanie danych, których nie można renderować, między funkcjami zwracanymi i efektami
    • React używa useRef do przekazywania danych między useEffect i useCallback
    • Lit używa prywatnej właściwości klasy
    • React w podstawie naśladuje zachowanie prywatnej właściwości klasy.

Poza tym, jeśli naprawdę podoba Ci się składnia komponentu funkcji React z haczykami, ale to samo środowisko bez kompilacji, co Lit, zespół Lit zdecydowanie poleca bibliotekę Haunted.

Dzieci

Gniazdo domyślne

Jeśli elementom HTML nie przypisano atrybutu slot, są one przypisywane do domyślnego, nienazwanego boksu. W poniższym przykładzie MyApp wstawi jeden akapit w nazwanym boksie. Drugi akapit zostanie domyślnie umieszczony w boksie bez nazwy.

@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 boksu, wywoływane jest zdarzenie slotchange. Komponent Lit może powiązać detektor zdarzeń ze zdarzeniem slotchange. W przykładzie poniżej pierwszy przedział znaleziony w instancji shadowRoot będzie mieć assignedNodes zarejestrowane w konsoli 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 referencji

Lit i React udostępniają odniesienie do elementu HTMLElement po wywołaniu ich funkcji render. Warto jednak sprawdzić, jak React i Lit tworzą DOM, który jest później zwracany przez dekorator Lit @query lub odwołanie do Reacta.

React to funkcjonalny kanał, który tworzy komponenty React, a nie elementy HTMLElement. Ponieważ referencja jest deklarowana przed wyrenderowaniem elementu HTMLElement, jest przydzielany jej fragment pamięci. Dlatego jako wartość początkową źródła odsyłającego widzisz null, ponieważ element DOM nie został jeszcze utworzony (ani wyrenderowany), tj. useRef(null).

Gdy ReactDOM przekształci komponent React w element HTMLElement, szuka w komponencie ReactComponent atrybutu o nazwie ref. Jeśli to możliwe, ReactDOM umieszcza odniesienie HTMLElement na 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 zakresie ograniczonym do korzeni cienia. Dekorator @query tworzy getter dla właściwości, który zasadniczo wykonuje operację this.shadowRoot.querySelector w przypadku ograniczonego korzenia.

Wysyłanie zapytań do wielu elementów

W poniższym przykładzie dekorator @queryAll zwróci 2 akapity w głównym cieniu 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>
   `;
  }
}

Zasadniczo @queryAll tworzy metodę dostępu do paragraphs, która zwraca wyniki paragraphs. W języku JavaScript można zadeklarować metodę pobierania w takim samym celu:

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

Zapytania dotyczące elementów zmieniających się

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

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

@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żywanie wywołań zwrotnych, ponieważ stan jest pośredniczony przez React. W przypadku Reacta lepiej jest nie polegać na stanie dostarczanym przez elementy. DOM to po prostu efekt procesu renderowania.

Stan zewnętrzny

Oprócz Lit możesz używać Redux, MobX lub innej biblioteki do zarządzania stanem.

Komponenty Lit są tworzone w zakresie przeglądarki. Oznacza to, że każda biblioteka, która jest dostępna w ramach przeglądarki, jest dostępna dla Lit. Powstało wiele niesamowitych bibliotek, które wykorzystują istniejące systemy zarządzania stanowego w Lit.

Oto seria artykułów firmy Vaadin, w której wyjaśnia ona, jak korzystać z Redux w komponencie Lit.

Zapoznaj się z dokumentem lit-mobx firmy Adobe, aby dowiedzieć się, jak witryna na dużą skalę może wykorzystać możliwości MobX w Lit.

Zapoznaj się też z artykułem Apollo Elements, aby dowiedzieć się, jak programiści wykorzystują GraphQL w swoich komponentach internetowych.

Lit współpracuje z wbudowanymi funkcjami przeglądarki, a w komponencie Lit można używać większości rozwiązań do zarządzania stanem w ramach przeglądarki.

Styl

Shadow DOM

Aby hermetyzować style i DOM w elementach niestandardowych, Lit używa Shadow DOM. Źródło cienia generuje drzewo cienia niezależne od głównego drzewa dokumentu. Oznacza to, że większość stylów jest ograniczona do tego dokumentu. Niektóre style mogą przeciekać przez nie, na przykład kolory i inne style powiązane z czcionkami.

Shadow DOM wprowadza też nowe zagadnienia i selektory do specyfikacji 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.
   */
}

Style udostępniania

Lit ułatwia udostępnianie stylów między komponentami w postaci CSSTemplateResults za pomocą tagów szablonów 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

Pierwiastki cienia stanowią pewną wyzwanie dla konwencjonalnych motywów, czyli zwykle z odgórnego podejścia do tagów stylu. Konwencjonalny sposób tworzenia motywów za pomocą komponentów internetowych, które korzystają z Shadow DOM, polega na udostępnieniu interfejsu API stylów za pomocą własnych właściwości CSS. Oto przykład używany w projekcie Material Design:

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

Następnie użytkownik 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 stylizacja od góry do dołu jest niezbędna, a nie możesz ujawnić stylów, możesz wyłączyć DOM cieniowany, zastępując createRenderRoot wartością this, która spowoduje wyrenderowanie szablonu komponentów do samego elementu niestandardowego, a nie do korzenia cieniowanego dołączonego do elementu niestandardowego. W efekcie utracisz hermetyzację stylów, hermetyzację DOM i sloty.

Produkcja

IE 11

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

Pakiety warunkowe

Zespół Lit zaleca wyświetlanie 2 różnych pakietów: jednego dla IE 11 i drugiego dla nowoczesnych przeglądarek. Ma to kilka zalet:

  • Usługa ES 6 jest szybsza i będzie obsługiwać większość Twoich klientów.
  • Transpilacja ES 5 znacznie zwiększa rozmiar pakietu
  • Pakiety warunkowe zapewniają to, co najlepsze w obu tych miejscach
    • Obsługa przeglądarki IE 11
    • Brak korka w nowoczesnych przeglądarkach

Więcej informacji o tworzeniu pakietu wyświetlanego warunkowo znajdziesz w naszej dokumentacji tutaj.