Lit für React-Entwickler

1. Einführung

Was ist Lit?

Lit ist eine einfache Bibliothek zum Erstellen schneller, schlanker Webkomponenten, die in jedem Framework oder ohne Framework funktionieren. Mit Lit können Sie wiederverwendbare Komponenten, Anwendungen, Designsysteme und mehr erstellen.

Lerninhalte

So übersetzen Sie mehrere React-Konzepte in Lit, z. B.:

  • JSX und Vorlagen
  • Komponenten und Attribute
  • Zustand und Lebenszyklus
  • Aufhänger
  • Kinder
  • Refs
  • Vermittlungsstatus

Aufgaben

Am Ende dieses Codelabs können Sie Konzepte von React-Komponenten in ihre Lit-Analoga umwandeln.

Voraussetzungen

  • Die aktuelle Version von Chrome, Safari, Firefox oder Edge.
  • Kenntnisse von HTML, CSS, JavaScript und den Chrome-Entwicklertools
  • React-Kenntnisse
  • (Erweitert) Wenn Sie die beste Entwicklungsumgebung nutzen möchten, laden Sie VS Code herunter. Sie benötigen außerdem lit-plugin für VS Code und NPM.

2. Lit im Vergleich zu React

Die Kernkonzepte und ‑funktionen von Lit ähneln in vielerlei Hinsicht denen von React, es gibt aber auch einige wichtige Unterschiede und Unterscheidungsmerkmale:

Es ist klein

Lit ist sehr klein: Die komprimierte und gezippte Version ist nur etwa 5 KB groß, während React + ReactDOM über 40 KB groß sind.

Balkendiagramm mit der minimierten und komprimierten Bundle-Größe in KB. Lit-Leiste ist 5 KB groß, React + React DOM 42,2 KB.

Es ist schnell

In öffentlichen Benchmarks, in denen das Templating-System von Lit, lit-html, mit dem VDOM von React verglichen wird, ist lit-html im schlimmsten Fall 8–10% schneller als React und in den häufigeren Anwendungsfällen über 50%schneller.

LitElement (die Komponentenbasisklasse von Lit) verursacht nur einen minimalen Overhead für lit-html, übertrifft die Leistung von React jedoch um 16–30%, wenn Komponentenfunktionen wie Arbeitsspeichernutzung, Interaktions- und Startzeiten verglichen werden.

Gruppiertes Balkendiagramm mit der Leistung von Lit im Vergleich zu React in Millisekunden (niedriger ist besser)

Kein Build erforderlich

Dank neuer Browserfunktionen wie ES-Modulen und getaggten Template-Literalen muss Lit nicht kompiliert werden, um ausgeführt zu werden. Das bedeutet, dass Entwicklungsumgebungen mit einem Script-Tag, einem Browser und einem Server eingerichtet werden können.

Mit ES-Modulen und modernen CDNs wie Skypack oder UNPKG benötigen Sie möglicherweise nicht einmal NPM, um loszulegen.

Wenn Sie möchten, können Sie aber weiterhin Lit-Code erstellen und optimieren. Die jüngste Konsolidierung von Entwicklern rund um native ES-Module war gut für Lit. Lit ist nur normales JavaScript und es sind keine frameworkspezifischen CLIs oder Build-Verarbeitung erforderlich.

Unabhängig vom Framework

Die Komponenten von Lit basieren auf einer Reihe von Webstandards, die als Webkomponenten bezeichnet werden. Das bedeutet, dass eine in Lit erstellte Komponente in aktuellen und zukünftigen Frameworks funktioniert. Wenn es HTML-Elemente unterstützt, unterstützt es auch Webkomponenten.

Probleme mit der Framework-Interoperabilität treten nur auf, wenn die Frameworks eine restriktive Unterstützung für das DOM haben. React ist eines dieser Frameworks, bietet aber über Refs die Möglichkeit, sich aus dem Framework zu befreien. Refs in React sind jedoch nicht benutzerfreundlich.

Das Lit-Team arbeitet an einem experimentellen Projekt namens @lit-labs/react, das Ihre Lit-Komponenten automatisch parst und einen React-Wrapper generiert, sodass Sie keine Referenzen verwenden müssen.

Außerdem erfahren Sie unter Custom Elements Everywhere, welche Frameworks und Bibliotheken gut mit benutzerdefinierten Elementen funktionieren.

Erstklassige TypeScript-Unterstützung

Es ist zwar möglich, den gesamten Lit-Code in JavaScript zu schreiben, aber Lit ist in TypeScript geschrieben und das Lit-Team empfiehlt Entwicklern, ebenfalls TypeScript zu verwenden.

Das Lit-Team hat mit der Lit-Community zusammengearbeitet, um Projekte zu pflegen, die TypeScript-Typprüfung und Intellisense für Lit-Vorlagen sowohl zur Entwicklungs- als auch zur Build-Zeit mit lit-analyzer und lit-plugin ermöglichen.

Screenshot einer IDE mit einer falschen Typüberprüfung für das Festlegen des booleschen Werts „outlined“ auf eine Zahl

Screenshot einer IDE mit Vorschlägen für die automatische Vervollständigung

Entwicklertools sind in den Browser integriert

Lit-Komponenten sind nur HTML-Elemente im DOM. Das bedeutet, dass Sie keine Tools oder Erweiterungen für Ihren Browser installieren müssen, um Ihre Komponenten zu prüfen.

Sie können einfach die Entwicklertools öffnen, ein Element auswählen und seine Eigenschaften oder seinen Status untersuchen.

Bild der Chrome-Entwicklertools mit folgenden Rückgaben: $0 gibt <mwc-textfield> zurück, $0.value gibt „hello world“ zurück, $0.outlined gibt „true“ zurück und {$0} zeigt die Erweiterung der Property.

Es wurde mit Blick auf das serverseitige Rendering (SSR) entwickelt.

Lit 2 wurde mit Blick auf die Unterstützung von SSR entwickelt. Zum Zeitpunkt der Erstellung dieses Codelabs hat das Lit-Team die SSR-Tools noch nicht in einer stabilen Form veröffentlicht. Das Lit-Team hat jedoch bereits serverseitig gerenderte Komponenten in Google-Produkten bereitgestellt und SSR in React-Anwendungen getestet. Das Lit-Team geht davon aus, dass diese Tools bald extern auf GitHub veröffentlicht werden.

In der Zwischenzeit können Sie den Fortschritt des Lit-Teams hier verfolgen.

Geringe Akzeptanz

Für die Nutzung von Lit ist kein großer Aufwand erforderlich. Sie können Komponenten in Lit erstellen und sie Ihrem bestehenden Projekt hinzufügen. Wenn Sie sie nicht mögen, müssen Sie nicht die gesamte App auf einmal konvertieren, da Webkomponenten auch in anderen Frameworks funktionieren.

Haben Sie eine ganze App in Lit erstellt und möchten zu etwas anderem wechseln? Dann können Sie Ihre aktuelle Lit-Anwendung in Ihr neues Framework einfügen und alles, was Sie möchten, zu den Komponenten des neuen Frameworks migrieren.

Außerdem unterstützen viele moderne Frameworks die Ausgabe in Webkomponenten. Das bedeutet, dass sie in der Regel selbst in ein Lit-Element passen.

3. Einrichtung und Nutzung des Playgrounds

Für dieses Codelab gibt es zwei Möglichkeiten:

  • Sie können alles online im Browser erledigen.
  • (Erweitert) Sie können dies auf Ihrem lokalen Computer mit VS Code tun.

Auf den Code zugreifen

Im gesamten Codelab finden Sie Links zum Lit-Playground, z. B.:

Die Playground-Umgebung ist eine Code-Sandbox, die vollständig in Ihrem Browser ausgeführt wird. Es kann TypeScript- und JavaScript-Dateien kompilieren und ausführen und auch automatisch Importe in Knotenmodule auflösen, z. B.

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

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

Sie können das gesamte Tutorial im Lit-Playground durcharbeiten und diese Prüfpunkte als Ausgangspunkt verwenden. Wenn Sie VS Code verwenden, können Sie diese Prüfpunkte nutzen, um den Startcode für einen beliebigen Schritt herunterzuladen und Ihre Arbeit zu überprüfen.

Benutzeroberfläche des beleuchteten Spielplatzes

Die Tab-Leiste der Dateiauswahl ist mit „Abschnitt 1“ gekennzeichnet, der Codebearbeitungsbereich mit „Abschnitt 2“, die Ausgabevorschau mit „Abschnitt 3“ und die Schaltfläche zum Neuladen der Vorschau mit „Abschnitt 4“.

Der Screenshot der Lit-Playground-Benutzeroberfläche zeigt die Abschnitte, die Sie in diesem Codelab verwenden werden.

  1. Dateiauswahl Beachten Sie das Pluszeichen…
  2. Dateieditor
  3. Codevorschau
  4. Schaltfläche „Aktualisieren“
  5. Grafik: Symbol zum Herunterladen

VS Code-Einrichtung (Advanced)

Diese VS Code-Einrichtung bietet folgende Vorteile:

  • Vorlagentyp prüfen
  • IntelliSense und automatische Vervollständigung für Vorlagen

Wenn Sie NPM und VS Code (mit dem lit-plugin-Plug-in) bereits installiert haben und wissen, wie Sie diese Umgebung verwenden, können Sie diese Projekte einfach herunterladen und starten. Gehen Sie dazu so vor:

  • Drücken Sie die Schaltfläche „Herunterladen“.
  • Inhalt der TAR-Datei in ein Verzeichnis extrahieren
  • (Falls TS) Richten Sie eine schnelle tsconfig ein, die ES-Module und ES2015+ ausgibt.
  • Installieren Sie einen Entwicklungsserver, der Bare-Modulspezifizierer auflösen kann. Das Lit-Team empfiehlt @web/dev-server.
  • Führen Sie den Entwicklungsserver aus und öffnen Sie Ihren Browser. Wenn Sie @web/dev-server verwenden, können Sie npx web-dev-server --node-resolve --watch --open verwenden.
    • Wenn Sie das Beispiel package.json verwenden, nutzen Sie npm run dev.

4. JSX und Vorlagen

In diesem Abschnitt lernen Sie die Grundlagen der Vorlagenerstellung in Lit kennen.

JSX- und Lit-Vorlagen

JSX ist eine Syntaxerweiterung für JavaScript, mit der React-Nutzer ganz einfach Vorlagen in ihren JavaScript-Code schreiben können. Lit-Vorlagen haben einen ähnlichen Zweck: Sie drücken die Benutzeroberfläche einer Komponente als Funktion ihres Status aus.

Grundlegende Syntax

In React rendern Sie ein JSX-„Hello World“ so:

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

Im obigen Beispiel gibt es zwei Elemente und eine eingeschlossene Variable „name“. In Lit gehen Sie so vor:

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

Beachten Sie, dass für Lit-Vorlagen kein React-Fragment erforderlich ist, um mehrere Elemente in den Vorlagen zu gruppieren.

In Lit werden Vorlagen mit einem html-Tagged Template LiteralLIT umschlossen. Daher kommt auch der Name von Lit.

Vorlagenwerte

In Lit-Vorlagen können andere Lit-Vorlagen verwendet werden, die als TemplateResult bezeichnet werden. Setzen Sie name beispielsweise in Kursiv-Tags (<i>) und in ein getaggtes Vorlagenliteral N.B.. Verwenden Sie dabei das Graviszeichen (`) und nicht das einfache Anführungszeichen (').

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

TemplateResults können Arrays, Strings, andere TemplateResults sowie Direktiven akzeptieren.

Versuchen Sie, den folgenden React-Code in Lit zu konvertieren:

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

Antwort:

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

Props übergeben und festlegen

Einer der größten Unterschiede zwischen der JSX- und der Lit-Syntax ist die Syntax für die Datenbindung. Hier ist ein Beispiel für eine React-Eingabe mit Bindungen:

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

Im obigen Beispiel wird eine Eingabe definiert, die Folgendes bewirkt:

  • Legt „disabled“ auf eine definierte Variable fest (in diesem Fall „false“).
  • Legt die Klasse auf static-class plus eine Variable (in diesem Fall "static-class my-class") fest.
  • Standardwert festlegen

In Lit gehen Sie so vor:

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

Im Lit-Beispiel wird eine boolesche Bindung hinzugefügt, um das Attribut disabled umzuschalten.

Als Nächstes gibt es eine Bindung direkt an das Attribut class anstelle von className. Dem Attribut class können mehrere Bindungen hinzugefügt werden, es sei denn, Sie verwenden die Direktive classMap, die eine deklarative Hilfsfunktion zum Umschalten von Klassen ist.

Schließlich wird die Eigenschaft value für die Eingabe festgelegt. Im Gegensatz zu React wird das Eingabeelement dadurch nicht auf „Nur lesen“ gesetzt, da es der nativen Implementierung und dem nativen Verhalten von „input“ folgt.

Syntax für die Bindung von Lit-Attributen

html`<my-element ?attribute-name=${booleanVar}>`;
  • Das Präfix ? ist die Bindungssyntax zum Ein- und Ausschalten eines Attributs für ein Element.
  • Entspricht inputRef.toggleAttribute('attribute-name', booleanVar)
  • Nützlich für Elemente, die disabled verwenden, da disabled="false" vom DOM weiterhin als „true“ gelesen wird, weil inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • Das Präfix . ist die Bindungssyntax zum Festlegen einer Property eines Elements.
  • Entspricht inputRef.propertyName = anyVar
  • Gut für die Übergabe komplexer Daten wie Objekte, Arrays oder Klassen
html`<my-element attribute-name=${stringVar}>`;
  • Wird an das Attribut eines Elements gebunden
  • Entspricht inputRef.setAttribute('attribute-name', stringVar)
  • Gut geeignet für einfache Werte, Selektoren für Stilregeln und querySelectors

Handler übergeben

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

Im obigen Beispiel wird eine Eingabe definiert, die Folgendes bewirkt:

  • Das Wort „click“ protokollieren, wenn auf die Eingabe geklickt wird
  • Wert der Eingabe protokollieren, wenn der Nutzer ein Zeichen eingibt

In Lit gehen Sie so vor:

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

Im Lit-Beispiel wird dem click-Ereignis mit @click ein Listener hinzugefügt.

Anstelle von onChange wird als Nächstes eine Bindung an das native input-Ereignis von <input> verwendet, da das native change-Ereignis nur für blur ausgelöst wird (React abstrahiert diese Ereignisse).

Syntax für Lit-Event-Handler

html`<my-element @event-name=${() => {...}}></my-element>`;
  • Das Präfix @ ist die Bindungssyntax für einen Event-Listener.
  • Entspricht inputRef.addEventListener('event-name', ...)
  • Verwendet native DOM-Ereignisnamen

5. Komponenten und Attribute

In diesem Abschnitt erfahren Sie mehr über die Komponenten und Funktionen der Lit-Klasse. State und Hooks werden in späteren Abschnitten ausführlicher behandelt.

Klassenkomponenten und LitElement

Das Lit-Äquivalent einer React-Klassenkomponente ist LitElement. Das Lit-Konzept der „reaktiven Eigenschaften“ ist eine Kombination aus den Props und dem Status von React. Beispiel:

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

Im obigen Beispiel gibt es eine React-Komponente, die:

  • Rendert einen name.
  • Legt den Standardwert von name auf einen leeren String ("") fest.
  • Weist name "Elliott" neu zu

So würden Sie das in LitElement tun

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

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

Und in der HTML-Datei:

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

Hier eine Zusammenfassung der Vorgänge im obigen Beispiel:

@property({type: String})
name = '';
  • Definiert eine öffentliche reaktive Eigenschaft – ein Teil der öffentlichen API Ihrer Komponente.
  • Stellt (standardmäßig) ein Attribut sowie eine Eigenschaft für Ihre Komponente bereit.
  • Definiert, wie das Attribut der Komponente (das ein String ist) in einen Wert übersetzt wird.
static get properties() {
  return {
    name: {type: String}
  }
}
  • Dies entspricht dem @property-TS-Decorator, wird aber nativ in JavaScript ausgeführt.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Diese Funktion wird immer dann aufgerufen, wenn sich eine reaktive Eigenschaft ändert.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Dadurch wird ein HTML-Element-Tag-Name einer Klassendefinition zugeordnet.
  • Gemäß dem Standard für benutzerdefinierte Elemente muss der Tag-Name einen Bindestrich (-) enthalten.
  • this in einem LitElement bezieht sich auf die Instanz des benutzerdefinierten Elements (in diesem Fall <welcome-banner>).
customElements.define('welcome-banner', WelcomeBanner);
  • Dies ist das JavaScript-Äquivalent des @customElement-TS-Dekorators.
<head>
  <script type="module" src="./index.js"></script>
</head>
  • Importiert die Definition des benutzerdefinierten Elements
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • Fügt das benutzerdefinierte Element der Seite hinzu
  • Legt das Attribut name auf 'Elliott' fest.

Funktionskomponenten

In Lit gibt es keine 1:1-Entsprechung einer Funktionskomponente, da kein JSX oder Präprozessor verwendet wird. Es ist jedoch ganz einfach, eine Funktion zu erstellen, die Eigenschaften entgegennimmt und basierend auf diesen Eigenschaften DOM rendert. Beispiel:

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

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

In Lit wäre das:

import {html, render} from 'lit';

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

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

6. Zustand und Lebenszyklus

In diesem Abschnitt erfahren Sie mehr über den Status und den Lebenszyklus von Lit.

Bundesland

Das Konzept von „Reactive Properties“ in Lit ist eine Mischung aus dem Status und den Attributen von React. Wenn sich reaktive Eigenschaften ändern, kann dies den Lebenszyklus der Komponente auslösen. Es gibt zwei Varianten von reaktiven Properties:

Öffentliche reaktive Eigenschaften

// 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';
}
  • Definiert durch @property
  • Ähnlich wie die Props und der Status von React, aber veränderlich
  • Öffentliche API, auf die von Nutzern der Komponente zugegriffen und die von ihnen festgelegt wird

Interner reaktiver Status

// 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';
}
  • Definiert durch @state
  • Ähnlich wie der Status in React, aber veränderlich
  • Privater interner Status, auf den in der Regel von der Komponente oder von Unterklassen aus zugegriffen wird

Lifecycle

Der Lit-Lebenszyklus ähnelt dem von React, es gibt jedoch einige wichtige Unterschiede.

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';
  }
}
  • Die beleuchtete Entsprechung ist ebenfalls constructor.
  • Es muss nichts an den Super-Aufruf übergeben werden.
  • Aufgerufen von (nicht vollständig):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Wenn ein nicht aktualisierter Tag-Name auf der Seite vorhanden ist und die Definition mit @customElement oder customElements.define geladen und registriert wird
  • Funktioniert ähnlich wie constructor in React

render

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

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Die beleuchtete Entsprechung ist ebenfalls render.
  • Kann ein beliebiges renderbares Ergebnis zurückgeben, z.B. TemplateResult oder string.
  • Ähnlich wie bei React sollte render() eine reine Funktion sein.
  • Wird auf dem Knoten gerendert, der von createRenderRoot() zurückgegeben wird (standardmäßig ShadowRoot).

componentDidMount

componentDidMount ähnelt einer Kombination aus den beiden Lit-Lebenszyklus-Callbacks firstUpdated und connectedCallback.

firstUpdated

import Chart from 'chart.js';

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

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • Wird beim ersten Rendern der Vorlage der Komponente in den Stamm der Komponente aufgerufen.
  • Wird nur aufgerufen, wenn das Element verbunden ist, z. B. nicht über document.createElement('my-component'), bis dieser Knoten an den DOM-Baum angehängt wird.
  • Hier können Sie die Komponente einrichten, wenn der DOM-Baum der Komponente gerendert werden muss.
  • Im Gegensatz zu React führen componentDidMount-Änderungen an reaktiven Eigenschaften in firstUpdated zu einem erneuten Rendern. Der Browser fasst die Änderungen jedoch in der Regel im selben Frame zusammen. Wenn für diese Änderungen kein Zugriff auf das DOM des Root-Elements erforderlich ist, sollten sie in der Regel in willUpdate erfolgen.

connectedCallback

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

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • Wird aufgerufen, wenn das benutzerdefinierte Element in den DOM-Baum eingefügt wird.
  • Im Gegensatz zu React-Komponenten werden benutzerdefinierte Elemente, wenn sie vom DOM getrennt werden, nicht zerstört und können daher mehrmals „verbunden“ werden.
    • firstUpdated wird nicht noch einmal aufgerufen
  • Nützlich, um das DOM neu zu initialisieren oder Event-Listener neu anzuhängen, die beim Trennen entfernt wurden
  • Hinweis : connectedCallback kann vor firstUpdated aufgerufen werden. Beim ersten Aufruf ist das DOM möglicherweise nicht verfügbar.

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);
  }
}
  • Die wörtliche Übersetzung ist updated (mit der englischen Vergangenheitsform von „update“).
  • Im Gegensatz zu React wird updated auch beim ersten Rendern aufgerufen.
  • Funktioniert ähnlich wie componentDidUpdate in React

componentWillUnmount

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

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • Die lit-Entsprechung ähnelt disconnectedCallback.
  • Im Gegensatz zu React-Komponenten wird die Komponente nicht zerstört, wenn benutzerdefinierte Elemente vom DOM getrennt werden.
  • Im Gegensatz zu componentWillUnmount wird disconnectedCallback nach dem Entfernen des Elements aus dem Baum aufgerufen.
  • Das DOM innerhalb des Stammverzeichnisses ist weiterhin mit dem Unterbaum des Stammverzeichnisses verbunden.
  • Nützlich, um Event-Listener und Speicherlecks zu bereinigen, damit der Browser die Komponente bereinigen kann

Training

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

Im obigen Beispiel gibt es eine einfache Uhr, die Folgendes tut:

  • Es wird „Hello World!“ gerendert. Es ist“ und dann die Uhrzeit angezeigt wird.
  • Die Uhr wird jede Sekunde aktualisiert.
  • Wenn das Modul entfernt wird, wird das Intervall, in dem der Tick aufgerufen wird, gelöscht.

Beginnen Sie zuerst mit der Deklaration der Komponentenklasse:

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

Als Nächstes initialisieren Sie date und deklarieren es mit @state als interne reaktive Eigenschaft, da Nutzer der Komponente date nicht direkt festlegen.

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

Rendern Sie als Nächstes die Vorlage.

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

Implementieren Sie nun die Methode „tick“.

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

Als Nächstes folgt die Implementierung von componentDidMount. Auch hier ist das Lit-Analogon eine Mischung aus firstUpdated und connectedCallback. Bei dieser Komponente ist für den Aufruf von tick mit setInterval kein Zugriff auf das DOM im Stammverzeichnis erforderlich. Außerdem wird das Intervall gelöscht, wenn das Element aus dem Dokumentbaum entfernt wird. Wenn es also wieder angehängt wird, muss das Intervall neu gestartet werden. connectedCallback ist hier also die bessere Wahl.

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

Bereinigen Sie das Intervall schließlich so, dass der Tick nicht ausgeführt wird, nachdem das Element vom Dokumentbaum getrennt wurde.

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

Das Gesamtbild:

// 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. Aufhänger

In diesem Abschnitt erfahren Sie, wie Sie React Hook-Konzepte in Lit übersetzen.

Konzepte von React-Hooks

Mit React-Hooks können Funktionskomponenten in den Status „eingehängt“ werden. Das bietet mehrere Vorteile.

  • Sie vereinfachen die Wiederverwendung von zustandsbehafteter Logik.
  • Komponente in kleinere Funktionen aufteilen

Außerdem wurden durch die Konzentration auf funktionsbasierte Komponenten bestimmte Probleme mit der klassenbasierten Syntax von React behoben, z. B.:

  • props von constructor nach super weiterleiten
  • Die unsaubere Initialisierung von Attributen in constructor
    • Das war ein Grund, der vom React-Team zu diesem Zeitpunkt angegeben wurde, aber durch ES2019 behoben wurde.
  • Probleme, die dadurch verursacht werden, dass this nicht mehr auf die Komponente verweist

React-Hooks-Konzepte in Lit

Wie im Abschnitt Komponenten und Attribute erwähnt, bietet Lit keine Möglichkeit, benutzerdefinierte Elemente aus einer Funktion zu erstellen. LitElement behebt jedoch die meisten der Hauptprobleme mit React-Klassenkomponenten. Beispiel:

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

Wie geht Lit mit diesen Problemen um?

  • constructor akzeptiert keine Argumente.
  • Alle @event-Bindungen werden automatisch an this gebunden.
  • this bezieht sich in den meisten Fällen auf die Referenz des benutzerdefinierten Elements.
  • Klasseneigenschaften können jetzt als Klassenmitglieder instanziiert werden. Dadurch werden konstruktorbasierte Implementierungen bereinigt.

Reaktive Controller

Die primären Konzepte hinter Hooks sind in Lit als reaktive Controller vorhanden. Mit reaktiven Controller-Mustern können Sie zustandsbehaftete Logik freigeben, Komponenten in kleinere, modulare Teile aufteilen und in den Aktualisierungszyklus eines Elements eingreifen.

Ein reaktiver Controller ist eine Objektschnittstelle, die in den Aktualisierungszyklus eines Controller-Hosts wie LitElement eingebunden werden kann.

Der Lebenszyklus eines ReactiveController und eines reactiveControllerHost sieht so aus:

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

Wenn Sie einen reaktiven Controller erstellen und ihn mit addController an einen Host anhängen, wird der Lebenszyklus des Controllers zusammen mit dem des Hosts aufgerufen. Erinnern Sie sich beispielsweise an das Uhrenbeispiel aus dem Abschnitt Status und Lebenszyklus:

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

Im obigen Beispiel gibt es eine einfache Uhr, die Folgendes tut:

  • Es wird „Hello World!“ gerendert. Es ist“ und dann die Uhrzeit angezeigt wird.
  • Die Uhr wird jede Sekunde aktualisiert.
  • Wenn das Modul entfernt wird, wird das Intervall, in dem der Tick aufgerufen wird, gelöscht.

Komponentengerüst erstellen

Beginnen Sie mit der Deklaration der Komponentenklasse und fügen Sie die render-Funktion hinzu.

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

Controller erstellen

Wechseln Sie jetzt zu clock.ts und erstellen Sie eine Klasse für ClockController und richten Sie constructor ein:

// 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() {
  }
}

Ein reaktiver Controller kann auf beliebige Weise erstellt werden, solange er die ReactiveController-Schnittstelle verwendet. Das Lit-Team bevorzugt jedoch für die meisten grundlegenden Fälle die Verwendung einer Klasse mit einem constructor, das eine ReactiveControllerHost-Schnittstelle sowie alle anderen Eigenschaften akzeptieren kann, die zum Initialisieren des Controllers erforderlich sind.

Jetzt müssen Sie die React-Lifecycle-Callbacks in Controller-Callbacks übersetzen. Kurzum:

  • componentDidMount
    • Zu connectedCallback von LitElement
    • Zum hostConnected des Controllers
  • ComponentWillUnmount
    • Zu disconnectedCallback von LitElement
    • Zum hostDisconnected des Controllers

Weitere Informationen zum Übersetzen des React-Lebenszyklus in den Lit-Lebenszyklus finden Sie im Abschnitt State & Lifecycle.

Implementieren Sie als Nächstes den hostConnected-Callback und die tick-Methoden und bereinigen Sie das Intervall in hostDisconnected, wie im Beispiel im Abschnitt Status und Lebenszyklus beschrieben.

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

Controller verwenden

Wenn Sie den Zeitgeber-Controller verwenden möchten, importieren Sie den Controller und aktualisieren Sie die Komponente in index.ts oder 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);

Um den Controller zu verwenden, müssen Sie ihn instanziieren, indem Sie eine Referenz zum Controller-Host (der <my-element>-Komponente) übergeben, und ihn dann in der render-Methode verwenden.

Neu rendern im Controller auslösen

Die Uhrzeit wird angezeigt, aber nicht aktualisiert. Das liegt daran, dass der Controller das Datum jede Sekunde festlegt, der Host es aber nicht aktualisiert. Das liegt daran, dass sich date in der Klasse ClockController und nicht mehr in der Komponente ändert. Das bedeutet, dass der Host nach dem Festlegen des date auf dem Controller angewiesen werden muss, seinen Aktualisierungszyklus mit host.requestUpdate() auszuführen.

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

Die Uhr sollte jetzt ticken.

Einen detaillierteren Vergleich gängiger Anwendungsfälle mit Hooks finden Sie im Abschnitt Erweiterte Themen – Hooks.

8. Kinder

In diesem Abschnitt erfahren Sie, wie Sie mit Slots Kinder in Lit verwalten.

Slots & Kinder

Mit Slots können Sie Komponenten verschachteln.

In React werden untergeordnete Elemente über Props vererbt. Der Standardslot ist props.children und die Funktion render definiert, wo sich der Standardslot befindet. Beispiel:

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

props.children sind React-Komponenten und keine HTML-Elemente.

In Lit werden untergeordnete Elemente in der Render-Funktion mit Slot-Elementen zusammengesetzt. Beachten Sie, dass untergeordnete Elemente nicht auf dieselbe Weise wie in React übernommen werden. In Lit sind Kinder HTMLElements, die an Slots angehängt sind. Diese Anlage wird als Projektion bezeichnet.

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

Mehrere Slots

In React ist das Hinzufügen mehrerer Slots im Grunde dasselbe wie das Übernehmen weiterer Attribute.

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

Wenn Sie weitere <slot>-Elemente hinzufügen, werden auch mehr Slots in Lit erstellt. Mit dem Attribut name sind mehrere Slots definiert: <slot name="slot-name">. So können Kinder angeben, welchem Slot sie zugewiesen werden.

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

Standardinhalte für Slots

In Slots wird der zugehörige Unterbaum angezeigt, wenn keine Knoten in diesen Slot projiziert werden. Wenn Knoten in einen Slot projiziert werden, wird der zugehörige Unterbaum nicht angezeigt. Stattdessen werden die projizierten Knoten angezeigt.

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

Kindern Zeitblöcke zuweisen

In React werden untergeordnete Elemente über die Eigenschaften einer Komponente Slots zugewiesen. Im Beispiel unten werden React-Elemente an die Attribute headerChildren und sectionChildren übergeben.

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

In Lit werden untergeordnete Elemente mithilfe des Attributs slot Slots zugewiesen.

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

Wenn es keinen Standardslot (z.B. <slot>) und keinen Slot mit einem name-Attribut (z.B. <slot name="foo">) gibt, das mit dem slot-Attribut der untergeordneten Elemente des benutzerdefinierten Elements (z.B. <div slot="foo">) übereinstimmt, wird dieser Knoten nicht projiziert und nicht angezeigt.

9. Refs

Gelegentlich muss ein Entwickler auf die API eines HTMLElement zugreifen.

In diesem Abschnitt erfahren Sie, wie Sie Elementreferenzen in Lit abrufen.

React-Referenzen

Eine React-Komponente wird in eine Reihe von Funktionsaufrufen transpiliert, die beim Aufrufen ein virtuelles DOM erstellen. Dieses virtuelle DOM wird von ReactDOM interpretiert und rendert HTMLElements.

In React sind Refs Speicherplatz im Arbeitsspeicher, der ein generiertes HTMLElement enthält.

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

Im obigen Beispiel führt die React-Komponente Folgendes aus:

  • Leeres Texteingabefeld und Button mit Text rendern
  • Fokus auf das Eingabefeld setzen, wenn auf die Schaltfläche geklickt wird

Nach dem ersten Rendern setzt React inputRef.current über das Attribut ref auf das generierte HTMLInputElement.

„Referenzen“ mit @query beleuchten

Lit ist nah am Browser angesiedelt und bietet eine sehr dünne Abstraktion über native Browserfunktionen.

Das React-Äquivalent zu refs in Lit ist das HTMLElement, das von den Dekoratoren @query und @queryAll zurückgegeben wird.

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

Im obigen Beispiel führt die Lit-Komponente Folgendes aus:

  • Definiert eine Property für MyElement mithilfe des Decorators @query (erstellt einen Getter für ein HTMLInputElement).
  • Deklariert und fügt einen Click-Event-Callback namens onButtonClick hinzu.
  • Fokus auf die Eingabe bei Schaltflächenklick

In JavaScript führen die Decorators @query und @queryAll die Funktionen querySelector bzw. querySelectorAll aus. Dies ist das JavaScript-Äquivalent von @query('input') inputEl!: HTMLInputElement;

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

Nachdem die Lit-Komponente die Vorlage der render-Methode an den Stamm von my-element übergeben hat, kann inputEl mit dem @query-Decorator das erste input-Element zurückgeben, das im Renderstamm gefunden wird. null wird zurückgegeben, wenn das angegebene Element nicht gefunden werden kann.@query

Wenn es mehrere input-Elemente im Render-Root gäbe, würde @queryAll eine Liste von Knoten zurückgeben.

10. Vermittlungsstatus

In diesem Abschnitt erfahren Sie, wie Sie den Status zwischen Komponenten in Lit vermitteln.

Wiederverwendbare Komponenten

React ahmt funktionale Rendering-Pipelines mit Top-down-Datenfluss nach. Eltern vermitteln Kindern durch Requisiten einen Zustand. Kinder kommunizieren mit Eltern über Callbacks in den Attributen.

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


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

Im obigen Beispiel führt eine React-Komponente Folgendes aus:

  • Erstellt ein Label basierend auf dem Wert props.step.
  • Rendert einen Button mit „+step“ oder „-step“ als Label.
  • Aktualisiert die übergeordnete Komponente, indem bei einem Klick props.addToCounter mit props.step als Argument aufgerufen wird.

Obwohl es möglich ist, Callbacks in Lit zu übergeben, sind die herkömmlichen Muster anders. Die React-Komponente im obigen Beispiel könnte als Lit-Komponente im folgenden Beispiel geschrieben werden:

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

Im obigen Beispiel führt eine Lit-Komponente Folgendes aus:

  • Reaktive Property step erstellen
  • Ein benutzerdefiniertes Ereignis namens update-counter wird ausgelöst, das den step-Wert des Elements bei einem Klick enthält.

Browserereignisse werden von untergeordneten an übergeordnete Elemente weitergegeben. Mit Ereignissen können Kinder Interaktionsereignisse und Statusänderungen übertragen. In React wird der Status grundsätzlich in die entgegengesetzte Richtung übergeben. Daher ist es ungewöhnlich, dass React-Komponenten Ereignisse auf dieselbe Weise wie Lit-Komponenten senden und darauf reagieren.

Zustandsorientierte Komponenten

In React werden häufig Hooks verwendet, um den Status zu verwalten. Eine MyCounter-Komponente kann durch Wiederverwendung der CounterButton-Komponente erstellt werden. Beachten Sie, dass addToCounter an beide Instanzen von CounterButton übergeben wird.

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

Das Beispiel oben führt Folgendes aus:

  • Erstellt einen count-Status.
  • Erstellt einen Callback, der eine Zahl zu einem count-Zustand hinzufügt.
  • CounterButton verwendet addToCounter, um count bei jedem Klick um step zu aktualisieren.

Eine ähnliche Implementierung von MyCounter ist in Lit möglich. Beachten Sie, dass addToCounter nicht an counter-button übergeben wird. Stattdessen wird der Callback als Event-Listener an das @update-counter-Ereignis eines übergeordneten Elements gebunden.

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

Das Beispiel oben führt Folgendes aus:

  • Erstellt ein reaktives Attribut namens count, das die Komponente aktualisiert, wenn der Wert geändert wird.
  • Bindet den addToCounter-Callback an den @update-counter-Event-Listener
  • Aktualisiert count, indem der Wert aus detail.step des Ereignisses update-counter hinzugefügt wird.
  • Legt den step-Wert von counter-button über das Attribut step fest.

Es ist üblicher, reaktive Eigenschaften in Lit zu verwenden, um Änderungen von übergeordneten an untergeordnete Elemente zu übertragen. Ebenso empfiehlt es sich, das Ereignissystem des Browsers zu verwenden, um Details von unten nach oben weiterzugeben.

Dieser Ansatz entspricht den Best Practices und dem Ziel von Lit, plattformübergreifende Unterstützung für Webkomponenten zu bieten.

11. Stile

In diesem Abschnitt erfahren Sie mehr über das Styling in Lit.

Stile

Lit bietet mehrere Möglichkeiten zum Formatieren von Elementen sowie eine integrierte Lösung.

Inline-Formatierungen

Lit unterstützt Inline-Stile sowie die Bindung an sie.

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

Im obigen Beispiel gibt es zwei Überschriften mit jeweils einem Inline-Stil.

Importieren Sie nun einen Rahmen aus border-color.js und binden Sie ihn an den orangefarbenen Text:

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

...

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

Es kann etwas mühsam sein, den Stilstring jedes Mal neu zu berechnen. Daher bietet Lit eine Direktive, die dabei hilft.

styleMap

Die styleMap-Anweisung erleichtert das Festlegen von Inline-Stilen mit JavaScript. Beispiel:

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

Das obige Beispiel führt Folgendes aus:

  • Zeigt ein h1 mit einem Rahmen und einer Farbauswahl an
  • Ändert border-color in den Wert aus der Farbauswahl.

Außerdem gibt es styleMap, mit dem die Formatierungen des h1 festgelegt werden. styleMap folgt einer Syntax, die der Attributbindungssyntax von style in React ähnelt.

CSSResult

Es wird empfohlen, Komponenten mit dem Tagged Template Literal css zu gestalten.

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

Das obige Beispiel führt Folgendes aus:

  • Deklariert ein CSS-Tagged Template Literal mit einer Bindung
  • Legt die Farben von zwei h1s mit IDs fest.

Vorteile der Verwendung des Vorlagen-Tags css:

  • Einmal pro Klasse im Vergleich zu einmal pro Instanz geparst
  • Implementiert mit Blick auf die Wiederverwendbarkeit von Modulen
  • Stile lassen sich ganz einfach in separate Dateien aufteilen.
  • Kompatibel mit dem CSS-Polyfill für benutzerdefinierte Eigenschaften

Beachten Sie außerdem den <style>-Tag in index.html:

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

In Lit werden die Stile Ihrer Komponenten auf ihre Roots beschränkt. Das bedeutet, dass Stile nicht nach innen und außen durchsickern. Um Stile an Komponenten zu übergeben, empfiehlt das Lit-Team die Verwendung von benutzerdefinierten CSS-Eigenschaften, da diese die Lit-Stilbereichsbegrenzung durchdringen können.

Stil-Tags

Sie können <style>-Tags auch einfach inline in Ihre Vorlagen einfügen. Der Browser dedupliziert diese Style-Tags. Wenn Sie sie jedoch in Ihre Vorlagen einfügen, werden sie pro Komponenteninstanz und nicht pro Klasse geparst, wie es bei der mit css getaggten Vorlage der Fall ist. Außerdem ist die Deduplizierung von CSSResults im Browser viel schneller.

Sie können auch ein <link rel="stylesheet"> in Ihrer Vorlage für Stile verwenden. Dies wird jedoch nicht empfohlen, da es zu einem anfänglichen Aufblitzen von nicht formatierten Inhalten (Flash of Unstyled Content, FOUC) kommen kann.

12. Weiterführende Informationen (optional)

JSX und Vorlagen

Lit & virtuelles DOM

Lit-html enthält kein herkömmliches virtuelles DOM, in dem jeder einzelne Knoten verglichen wird. Stattdessen werden Leistungsmerkmale genutzt, die in der ES2015-Spezifikation für Tagged Template Literals (markierte Vorlagenliterale) enthalten sind. Markierte Vorlagenliterale sind Vorlagenliteral-Strings, an die Tag-Funktionen angehängt sind.

Hier ist ein Beispiel für ein Template-Literal:

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

Hier ist ein Beispiel für ein getaggtes Template-Literal:

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

Im obigen Beispiel ist das Tag die Funktion tag und die Funktion f gibt einen Aufruf eines getaggten Vorlagenliterals zurück.

Ein Großteil der Leistungssteigerung in Lit beruht darauf, dass die an die Tag-Funktion übergebenen String-Arrays denselben Zeiger haben (wie im zweiten console.log gezeigt). Der Browser erstellt nicht bei jedem Aufruf der Tag-Funktion ein neues strings-Array, da er dasselbe Template-Literal verwendet (d.h. an derselben Stelle im AST). Die Bindung, das Parsen und das Vorlagencaching von Lit können diese Funktionen also ohne großen Laufzeit-Diffing-Overhead nutzen.

Dieses integrierte Browserverhalten von getaggten Template-Literalen bietet Lit einen erheblichen Leistungsvorteil. Bei den meisten herkömmlichen virtuellen DOMs wird der Großteil der Arbeit in JavaScript erledigt. Die meisten Differenzierungen von getaggten Template-Literalen werden jedoch im C++-Code des Browsers durchgeführt.

Wenn Sie mit HTML-getaggten Template-Literalen in React oder Preact beginnen möchten, empfiehlt das Lit-Team die htm-Bibliothek.

Wie bei der Google Codelabs-Website und mehreren Online-Code-Editoren werden Sie jedoch feststellen, dass die Syntaxhervorhebung für getaggte Template-Literale nicht sehr verbreitet ist. Einige IDEs und Texteditoren unterstützen sie standardmäßig, z. B. Atom und der Codeblock-Highlighter von GitHub. Das Lit-Team arbeitet auch sehr eng mit der Community zusammen, um Projekte wie das lit-plugin zu pflegen. Das ist ein VS Code-Plug-in, das Syntaxhervorhebung, Typüberprüfung und IntelliSense für Ihre Lit-Projekte bietet.

Lit & JSX + React DOM

JSX wird nicht im Browser ausgeführt, sondern verwendet einen Präprozessor, um JSX in JavaScript-Funktionsaufrufe zu konvertieren (in der Regel über Babel).

Babel transformiert beispielsweise Folgendes:

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

in Folgendes ändern:

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

React DOM nimmt dann die React-Ausgabe und übersetzt sie in das tatsächliche DOM – mit Eigenschaften, Attributen, Ereignis-Listenern usw.

Lit-html verwendet getaggte Template-Literale, die ohne Transpilierung oder Vorprozessor im Browser ausgeführt werden können. Um mit Lit zu beginnen, benötigen Sie also nur eine HTML-Datei, ein ES-Modul-Script und einen Server. Hier ist ein Skript, das vollständig im Browser ausgeführt werden kann:

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

Da das Templating-System von Lit, lit-html, kein herkömmliches virtuelles DOM verwendet, sondern die DOM API direkt nutzt, ist Lit 2 mit weniger als 5 KB minimiert und gezippt im Vergleich zu React (2,8 KB) + react-dom (39,4 KB) mit 40 KB minimiert und gezippt.

Ereignisse

React verwendet ein synthetisches Ereignissystem. Das bedeutet, dass in „react-dom“ jedes Ereignis definiert werden muss, das in jeder Komponente verwendet wird, und dass für jeden Knotentyp ein entsprechendes CamelCase-Ereignis bereitgestellt werden muss. Daher gibt es in JSX keine Methode zum Definieren eines Event-Listeners für ein benutzerdefiniertes Ereignis. Entwickler müssen ref verwenden und dann imperativ einen Listener anwenden. Dies führt zu einer schlechten Entwicklererfahrung bei der Integration von Bibliotheken, die nicht für React entwickelt wurden, da ein React-spezifischer Wrapper geschrieben werden muss.

Lit-html greift direkt auf das DOM zu und verwendet native Ereignisse. Das Hinzufügen von Event-Listenern ist daher so einfach wie @event-name=${eventNameListener}. Das bedeutet, dass weniger Laufzeit-Parsing für das Hinzufügen von Event-Listenern und das Auslösen von Ereignissen erforderlich ist.

Komponenten und Attribute

React-Komponenten und benutzerdefinierte Elemente

LitElement verwendet benutzerdefinierte Elemente, um seine Komponenten zu verpacken. Benutzerdefinierte Elemente bringen einige Kompromisse zwischen React-Komponenten mit sich, wenn es um die Komponentisierung geht (Status und Lebenszyklus werden im Abschnitt Status und Lebenszyklus näher erläutert).

Einige Vorteile von benutzerdefinierten Elementen als Komponentensystem:

  • Sie sind im Browser integriert und erfordern keine zusätzlichen Tools.
  • Passen Sie jede Browser-API von innerHTML und document.createElement bis querySelector an.
  • Kann in der Regel in allen Frameworks verwendet werden
  • Kann mit customElements.define verzögert registriert und das DOM „hydriert“ werden.

Einige Nachteile von benutzerdefinierten Elementen im Vergleich zu React-Komponenten:

  • Es ist nicht möglich, ein benutzerdefiniertes Element zu erstellen, ohne eine Klasse zu definieren (daher keine JSX-ähnlichen funktionalen Komponenten).
  • Muss ein schließendes Tag
      enthalten.
    • Hinweis:Trotz der Entwicklerfreundlichkeit bedauern Browseranbieter in der Regel die Spezifikation für selbstschließende Tags. Daher enthalten neuere Spezifikationen in der Regel keine selbstschließenden Tags.
  • Fügt dem DOM-Baum einen zusätzlichen Knoten hinzu, was zu Layoutproblemen führen kann
  • Muss über JavaScript registriert werden

Lit verwendet benutzerdefinierte Elemente anstelle eines eigenen Elementsystems, da benutzerdefinierte Elemente in den Browser integriert sind. Das Lit-Team ist der Ansicht, dass die frameworkübergreifenden Vorteile die Vorteile einer Komponentenabstraktionsschicht überwiegen. Tatsächlich hat das Lit-Team mit seinen Bemühungen im Bereich „lit-ssr“ die Hauptprobleme bei der JavaScript-Registrierung behoben. Einige Unternehmen wie GitHub nutzen außerdem die Lazy Registration von benutzerdefinierten Elementen, um Seiten schrittweise mit optionalen Elementen zu erweitern.

Daten an benutzerdefinierte Elemente übergeben

Ein häufiges Missverständnis bei benutzerdefinierten Elementen ist, dass Daten nur als Strings übergeben werden können. Dieser Irrtum beruht wahrscheinlich darauf, dass Elementattribute nur als Strings geschrieben werden können. Es stimmt zwar, dass Lit String-Attribute in die definierten Typen umwandelt, benutzerdefinierte Elemente können aber auch komplexe Daten als Eigenschaften akzeptieren.

Beispiel:

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

Es wird eine primitive reaktive Property num definiert, die den Stringwert eines Attributs in ein number konvertiert. Anschließend wird mit attribute:false eine komplexe Datenstruktur eingeführt, die die Attributverarbeitung von Lit deaktiviert.

So übergeben Sie Daten an dieses benutzerdefinierte Element:

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

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

Zustand und Lebenszyklus

Andere React-Lebenszyklus-Callbacks

static getDerivedStateFromProps

In Lit gibt es kein Äquivalent, da „props“ und „state“ beides dieselben Klassenattribute sind.

shouldComponentUpdate

  • Die entsprechende Literatur ist shouldUpdate.
  • Wird beim ersten Rendern aufgerufen, anders als bei React
  • Funktioniert ähnlich wie shouldComponentUpdate in React

getSnapshotBeforeUpdate

In Lit ähnelt getSnapshotBeforeUpdate sowohl update als auch willUpdate.

willUpdate

  • Angerufen vor dem update
  • Im Gegensatz zu getSnapshotBeforeUpdate wird willUpdate vor render aufgerufen.
  • Änderungen an reaktiven Eigenschaften in willUpdate lösen den Aktualisierungszyklus nicht noch einmal aus.
  • Guter Ort, um Attributwerte zu berechnen, die von anderen Attributen abhängen und im restlichen Aktualisierungsprozess verwendet werden
  • Diese Methode wird auf dem Server in SSR aufgerufen. Daher wird davon abgeraten, hier auf das DOM zuzugreifen.

update

  • Angerufen nach dem willUpdate
  • Im Gegensatz zu getSnapshotBeforeUpdate wird update vor render aufgerufen.
  • Änderungen an reaktiven Attributen in update lösen den Aktualisierungszyklus nicht neu aus, wenn sie vor dem Aufrufen von super.update geändert werden.
  • Guter Ort, um Informationen aus dem DOM rund um die Komponente zu erfassen, bevor die gerenderte Ausgabe im DOM gespeichert wird
  • Diese Methode wird beim serverseitigen Rendern nicht auf dem Server aufgerufen.

Andere Lit-Lebenszyklus-Callbacks

Es gibt mehrere Lebenszyklus-Callbacks, die im vorherigen Abschnitt nicht erwähnt wurden, da es in React kein Analogon dazu gibt. Diese sind:

attributeChangedCallback

Sie wird aufgerufen, wenn sich eine der observedAttributes des Elements ändert. Sowohl observedAttributes als auch attributeChangedCallback sind Teil der Spezifikation für benutzerdefinierte Elemente und werden von Lit im Hintergrund implementiert, um eine Attribut-API für Lit-Elemente bereitzustellen.

adoptedCallback

Wird aufgerufen, wenn die Komponente in ein neues Dokument verschoben wird, z.B. von der HTMLTemplateElement-Datei documentFragment in die Hauptdatei document. Dieser Callback ist auch Teil der Spezifikation für benutzerdefinierte Elemente und sollte nur für erweiterte Anwendungsfälle verwendet werden, wenn die Komponente Dokumente ändert.

Andere Methoden und Eigenschaften für den Lebenszyklus

Diese Methoden und Eigenschaften sind Klassenmember, die Sie aufrufen, überschreiben oder abwarten können, um den Lebenszyklusprozess zu bearbeiten.

updateComplete

Dies ist ein Promise, der aufgelöst wird, wenn das Element aktualisiert wurde, da die Update- und Render-Lifecycles asynchron sind. Beispiel:

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

getUpdateComplete

Diese Methode sollte überschrieben werden, um anzupassen, wann updateComplete aufgelöst wird. Das ist häufig der Fall, wenn eine Komponente eine untergeordnete Komponente rendert und ihre Renderzyklen synchronisiert werden müssen, z. B.:

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

performUpdate

Mit dieser Methode werden die Update-Lifecycle-Callbacks aufgerufen. Dies ist in der Regel nur in seltenen Fällen erforderlich, in denen die Aktualisierung synchron erfolgen muss oder eine benutzerdefinierte Planung erforderlich ist.

hasUpdated

Diese Eigenschaft ist true, wenn die Komponente mindestens einmal aktualisiert wurde.

isConnected

Diese Eigenschaft ist Teil der Spezifikation für benutzerdefinierte Elemente und wird auf true gesetzt, wenn das Element derzeit an den Hauptdokumentbaum angehängt ist.

Visualisierung des Lebenszyklus von Literatur-Updates

Der Aktualisierungslebenszyklus besteht aus drei Teilen:

  • Vor dem Update
  • Aktualisieren
  • Nach der Aktualisierung

Vor dem Update

Ein gerichteter azyklischer Graph von Knoten mit Callback-Namen. constructor to requestUpdate. @property zu Property Setter, attributeChangedCallback zu Property Setter. Property-Setter zu „hasChanged“. „hasChanged“ zu „requestUpdate“. „requestUpdate“ verweist auf das nächste Diagramm für den Aktualisierungslebenszyklus.

Nach dem requestUpdate wird ein geplantes Update erwartet.

Aktualisieren

Ein gerichteter azyklischer Graph mit Knoten mit Callback-Namen. Ein Pfeil aus dem vorherigen Bild der Lebenszyklus-Punkte vor der Aktualisierung zeigt auf „performUpdate“. „performUpdate“ zeigt auf „shouldUpdate“. „shouldUpdate“ zeigt sowohl auf „complete update if false“ als auch auf „willUpdate“. „willUpdate“ zeigt auf „update“. „update“ zeigt sowohl auf „render“ als auch auf das nächste Lebenszyklusdiagramm nach der Aktualisierung. „render“ zeigt ebenfalls auf das nächste Lebenszyklusdiagramm nach der Aktualisierung.

Nach der Aktualisierung

Ein gerichteter azyklischer Graph mit Knoten mit Callback-Namen. Pfeil vom vorherigen Bild des Aktualisierungslebenszyklus zeigt auf „firstUpdated“. „firstUpdated“ zeigt auf „updated“. „updated“ zeigt auf „updateComplete“.

Aufhänger

Warum Hooks?

Hooks wurden in React für einfache Anwendungsfälle von Funktionskomponenten eingeführt, die einen Status erforderten. In vielen einfachen Fällen sind Funktionskomponenten mit Hooks viel einfacher und lesbarer als ihre entsprechenden Klassenkomponenten. Wenn jedoch asynchrone Statusaktualisierungen eingeführt und Daten zwischen Hooks oder Effekten übergeben werden, reicht das Hooks-Muster in der Regel nicht aus und eine klassenbasierte Lösung wie reaktive Controller ist besser geeignet.

Hooks und Controller für API-Anfragen

Es ist üblich, einen Hook zu schreiben, der Daten von einer API anfordert. Diese React-Funktionskomponente führt beispielsweise Folgendes aus:

  • index.tsx
    • Text wird gerendert
    • Rendert die Antwort von useAPI 
      • Nutzer-ID + Nutzername
      • Fehlermeldung
        • 404, wenn Nutzer 11 erreicht wird (designbedingt)
        • Fehler abbrechen, wenn der API-Abruf abgebrochen wird
      • Benachrichtigung zum Laden
    • Rendert eine Aktionsschaltfläche 
      • „Nächster Nutzer“: Ruft die API für den nächsten Nutzer ab.
      • Abbrechen: Der API-Abruf wird abgebrochen und ein Fehler wird angezeigt.
  • useApi.tsx
    • Definiert einen benutzerdefinierten useApi-Hook.
    • Ruft ein Nutzerobjekt asynchron von einer API ab.
    • Ausgabe:
      • Nutzername
      • Gibt an, ob der Abruf geladen wird
      • Alle Fehlermeldungen
      • Ein Callback zum Abbrechen des Abrufs
    • Bricht laufende Abrufe ab, wenn die Komponente demontiert wird.

Hier finden Sie die Implementierung des Lit + Reactive-Controllers.

Fazit:

  • Reaktive Controller ähneln am ehesten benutzerdefinierten Hooks.
  • Nicht renderbare Daten zwischen Callbacks und Effekten übergeben 
    • In React wird useRef verwendet, um Daten zwischen useEffect und useCallback zu übergeben.
    • Lit verwendet eine private Klasseneigenschaft
    • React ahmt im Wesentlichen das Verhalten einer privaten Klasseneigenschaft nach.

Wenn Ihnen die React-Funktionskomponentensyntax mit Hooks, aber die gleiche buildlose Umgebung von Lit gefällt, empfiehlt das Lit-Team die Haunted-Bibliothek.

Kinder

Standardslot

Wenn HTML-Elementen kein slot-Attribut zugewiesen wird, werden sie dem standardmäßigen unbenannten Slot zugewiesen. Im folgenden Beispiel wird mit MyApp ein Absatz in einen benannten Slot eingefügt. Der andere Absatz wird standardmäßig im unbenannten Slot platziert.“

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

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

Slot-Updates

Wenn sich die Struktur der untergeordneten Slots ändert, wird ein slotchange-Ereignis ausgelöst. Eine Lit-Komponente kann einen Event-Listener an ein slotchange-Ereignis binden. Im Beispiel unten werden die assignedNodes des ersten Slots, der in shadowRoot gefunden wird, am slotchange in der Konsole protokolliert.

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

Refs

Referenzgenerierung

Sowohl Lit als auch React stellen nach dem Aufrufen ihrer render-Funktionen eine Referenz auf ein HTMLElement bereit. Es ist jedoch sinnvoll, sich anzusehen, wie React und Lit das DOM zusammensetzen, das später über einen Lit-@query-Decorator oder eine React-Referenz zurückgegeben wird.

React ist eine funktionale Pipeline, mit der React-Komponenten und keine HTMLElements erstellt werden. Da eine Ref deklariert wird, bevor ein HTMLElement gerendert wird, wird ein Speicherplatz zugewiesen. Daher sehen Sie null als Anfangswert einer Ref, da das tatsächliche DOM-Element noch nicht erstellt (oder gerendert) wurde, d.h. useRef(null).

Nachdem ReactDOM eine React-Komponente in ein HTMLElement konvertiert hat, wird in der React-Komponente nach einem Attribut namens ref gesucht. Falls verfügbar, platziert ReactDOM die Referenz des HTMLElement auf ref.current.

LitElement verwendet die html-Vorlagen-Tag-Funktion aus lit-html, um im Hintergrund ein Template-Element zu erstellen. LitElement stempelt den Inhalt der Vorlage nach dem Rendern in das Shadow DOM eines benutzerdefinierten Elements. Das Shadow-DOM ist ein DOM-Baum mit Bereich, der von einem Shadow-Root gekapselt wird. Der Decorator @query erstellt dann eine Getter-Methode für die Eigenschaft, die im Wesentlichen ein this.shadowRoot.querySelector für den Bereichs-Root ausführt.

Mehrere Elemente abfragen

Im folgenden Beispiel gibt der Dekorator @queryAll die beiden Absätze im Shadow Root als NodeList zurück.

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

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

Im Grunde erstellt @queryAll einen Getter für paragraphs, der die Ergebnisse von this.shadowRoot.querySelectorAll() zurückgibt. In JavaScript kann ein Getter deklariert werden, um denselben Zweck zu erfüllen:

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

Elemente für Abfrageänderungen

Der @queryAsync-Decorator eignet sich besser für die Verarbeitung eines Knotens, der sich je nach Status einer anderen Element-Property ändern kann.

Im folgenden Beispiel wird mit @queryAsync das erste Absatz-Element gefunden. Ein Absatz-Element wird jedoch nur gerendert, wenn renderParagraph zufällig eine ungerade Zahl generiert. Die @queryAsync-Anweisung gibt ein Promise zurück, das aufgelöst wird, wenn der erste Absatz verfügbar ist.

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

Vermittlungsstatus

In React werden üblicherweise Callbacks verwendet, da der Status von React selbst verwaltet wird. React versucht, nicht auf den von Elementen bereitgestellten Status angewiesen zu sein. Das DOM ist einfach ein Ergebnis des Rendering-Prozesses.

Externer Status

Sie können Redux, MobX oder eine andere Bibliothek zur Statusverwaltung zusammen mit Lit verwenden.

Lit-Komponenten werden im Browserbereich erstellt. Daher ist jede Bibliothek, die auch im Browserbereich vorhanden ist, für Lit verfügbar. Es wurden viele tolle Bibliotheken entwickelt, um vorhandene Statusverwaltungssysteme in Lit zu nutzen.

Hier finden Sie eine Serie von Vaadin, in der erklärt wird, wie Sie Redux in einer Lit-Komponente nutzen können.

Hier finden Sie ein Beispiel von Adobe, wie MobX in Lit für eine große Website eingesetzt werden kann.

Unter Apollo Elements erfahren Sie, wie Entwickler GraphQL in ihre Webkomponenten einbinden.

Lit funktioniert mit nativen Browserfunktionen und die meisten Lösungen zur Statusverwaltung im Browserbereich können in einer Lit-Komponente verwendet werden.

Stile

Shadow DOM

Um Stile und DOM nativ in einem benutzerdefinierten Element zu kapseln, verwendet Lit Shadow DOM. Shadow Roots generieren einen Shadow-Baum, der vom Hauptdokumentbaum getrennt ist. Das bedeutet, dass die meisten Stile auf dieses Dokument beschränkt sind. Bestimmte Stile wie Farbe und andere schriftbezogene Stile werden übernommen.

Mit Shadow DOM werden auch neue Konzepte und Selektoren in die CSS-Spezifikation eingeführt:

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

Stile teilen

Mit Lit können Sie Stile ganz einfach in Form von CSSTemplateResults über css-Vorlagen-Tags zwischen Komponenten teilen. Beispiel:

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

Designs

Shadow-Roots stellen eine Herausforderung für das herkömmliche Theming dar, das in der Regel auf Top-down-Style-Tags basiert. Die herkömmliche Methode zum Anwenden von Designs auf Webkomponenten, die Shadow DOM verwenden, besteht darin, eine Stil-API über benutzerdefinierte CSS-Eigenschaften bereitzustellen. Hier ist ein Beispiel für ein Muster, das in Material Design verwendet wird:

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

Der Nutzer würde dann das Design der Website ändern, indem er benutzerdefinierte Eigenschaftswerte anwendet:

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

Wenn die Top-down-Gestaltung ein Muss ist und Sie keine Stile verfügbar machen können, ist es immer möglich, Shadow DOM zu deaktivieren. Dazu müssen Sie createRenderRoot überschreiben, damit this zurückgegeben wird. Das Vorlage-Rendering Ihrer Komponenten erfolgt dann für das benutzerdefinierte Element selbst und nicht für einen Shadow-Root, der an das benutzerdefinierte Element angehängt ist. Dadurch gehen die Stil- und DOM-Kapselung sowie Slots verloren.

Produktion

IE 11

Wenn Sie ältere Browser wie IE 11 unterstützen müssen, müssen Sie einige Polyfills laden, die etwa 33 KB zusätzlich ausmachen. Weitere Informationen

Bedingte Sets

Das Lit-Team empfiehlt, zwei verschiedene Bundles bereitzustellen: eines für IE 11 und eines für moderne Browser. Das hat mehrere Vorteile:

  • ES 6 ist schneller und wird von den meisten Ihrer Clients unterstützt.
  • Durch das Transpilieren von ES5 wird die Bundle-Größe erheblich erhöht.
  • Mit bedingten Paketen erhalten Sie das Beste aus beiden Welten 
    • IE 11-Unterstützung
    • Keine Verlangsamung bei modernen Browsern

Weitere Informationen zum Erstellen eines bedingt bereitgestellten Bundles finden Sie in unserer Dokumentation.