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 kannst du gemeinsam nutzbare Komponenten, Anwendungen, Designsysteme und vieles mehr erstellen.

Lerninhalte

So werden mehrere React-Konzepte in Lit übersetzt, z. B.:

  • JSX und Vorlagen
  • Komponenten und Props
  • Status und Lebenszyklus
  • Hooks
  • Children
  • Verweise
  • Vermittlungsstatus

Aufgaben

Am Ende dieses Codelabs wirst du in der Lage sein, Konzepte der React-Komponente in Lit-Analoge umzuwandeln.

Voraussetzungen

  • Die neueste Version von Chrome, Safari, Firefox oder Edge
  • Kenntnisse in HTML, CSS, JavaScript und den Chrome-Entwicklertools
  • Wissen über React
  • (Fortgeschritten) Für die bestmögliche Entwicklung laden Sie VS Code herunter. Außerdem benötigen Sie das lit-Plug-in für VS Code und NPM.

2. Lit vs. React

Die Hauptkonzepte und -funktionen von Lit ähneln in vielerlei Hinsicht denen von React. Lit hat aber auch einige wichtige Unterschiede und Alleinstellungsmerkmale:

Es ist klein

Im Vergleich zu React + ReactDOM, das über 40 KB verfügt, sind nur etwa 5 KB verkleinert und mit gzip komprimiert.

Balkendiagramm der minimierten und komprimierten Bundle-Größe in KB. Die Lichtleiste ist 5 KB groß und das React- und das React-DOM ist 42,2 KB groß

Es ist schnell

In öffentlichen Benchmarks, in denen das Vorlagensystem von Lit, Lit-HTML, mit dem VDOM von React verglichen wird, ist Lit HTML im schlimmsten Fall 8–10% schneller als bei React und in den üblicheren Anwendungsfällen um 50%schneller.

LitElement (die Komponentenbasisklasse von Lit) erzeugt nur minimalen Mehraufwand für Lit-HTML, übertrifft jedoch die Leistung von React um 16–30%, wenn Komponentenfunktionen wie Arbeitsspeichernutzung, Interaktion und Startzeiten verglichen werden.

Gruppiertes Balkendiagramm zur Leistung, das Lit mit React in Millisekunden vergleicht (je niedriger, desto besser)

Erfordert keinen Build

Dank neuer Browserfunktionen wie ES-Modulen und getaggten Vorlagenliteralen erfordert Lit keine Kompilierung, 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 vielleicht nicht einmal NPM, um loszulegen.

Wenn Sie möchten, können Sie Lit-Code jedoch weiterhin erstellen und optimieren. Die jüngste Entwicklerkonzentrierung auf native ES-Module war gut für Lit: Lit ist einfach normales JavaScript und es sind keine frameworkspezifischen Befehlszeilen oder Build-Verarbeitungen erforderlich.

Unabhängig vom Framework

Die Komponenten von Lit basieren auf einer Reihe von Webstandards, den sogenannten Webkomponenten. Das bedeutet, dass Komponenten, die in Lit erstellt wurden, in aktuellen und zukünftigen Frameworks funktionieren. Wenn HTML-Elemente unterstützt werden, werden auch Webkomponenten unterstützt.

Die einzigen Probleme bei der Interoperabilität von Frameworks treten auf, wenn die Frameworks eine eingeschränkte Unterstützung für das DOM haben. React ist eines dieser Konzepte, aber es ermöglicht Ausstiegschancen über Refs, und Refs in React sind keine gute Entwicklererfahrung.

Das Lit-Team hat an einem experimentellen Projekt mit dem Namen @lit-labs/react gearbeitet, das Ihre Lit-Komponenten automatisch parst und einen React-Wrapper generiert, damit Sie keine Referenzen verwenden müssen.

Außerdem erfahren Sie auf 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, Lit ist jedoch in TypeScript geschrieben und das Lit-Team empfiehlt Entwicklern, ebenfalls TypeScript zu verwenden.

Das Lit-Team arbeitet mit der Lit-Community zusammen, um Projekte zu pflegen, bei denen TypeScript-Typprüfung und -Intelligenz in Lit-Vorlagen sowohl bei der Entwicklung als auch bei der Erstellung mit lit-analyzer und lit-plugin zur Verfügung stehen.

Screenshot einer IDE mit einer fehlerhaften Typprüfung zum Festlegen des umrissenen booleschen Werts auf eine Zahl

Screenshot einer IDE mit IntelliSense-Vorschlägen

Entwicklertools sind in den Browser integriert

Lit-Komponenten sind einfach HTML-Elemente im DOM. Das bedeutet, dass Sie zum Prüfen Ihrer Komponenten keine Tools oder Erweiterungen für Ihren Browser installieren müssen.

Öffnen Sie einfach die Entwicklertools, wählen Sie ein Element aus und sehen Sie sich seine Eigenschaften oder seinen Status an.

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

Bei der Entwicklung wurde das serverseitige Rendering (SSR) berücksichtigt.

Bei der Entwicklung von Lit 2 wurde auf die Unterstützung von SSR geachtet. Zum Zeitpunkt der Erstellung dieses Codelabs musste das Lit-Team die SSR-Tools noch nicht in einer stabilen Form veröffentlichen. Das Lit-Team hat jedoch bereits serverseitig gerenderte Komponenten für Google-Produkte bereitgestellt und die SSR in React-Anwendungen getestet. Das Lit-Team wird diese Tools voraussichtlich bald extern auf GitHub veröffentlichen.

In der Zwischenzeit kannst du den Fortschritt des Lit-Teams hier verfolgen.

Es ist ein geringer Einstieg

Die Nutzung von Lit ist nicht mit großen Verpflichtungen verbunden. Sie können Komponenten in Lit erstellen und Ihrem vorhandenen Projekt hinzufügen. Wenn Ihnen die Webkomponenten nicht gefallen, müssen Sie nicht die gesamte App auf einmal konvertieren, da sie auch in anderen Frameworks funktionieren.

Hast du eine ganze App in Lit erstellt und möchtest du sie ändern? In diesem Fall können Sie Ihre aktuelle Lit-Anwendung in Ihr neues Framework einfügen und alles, was Sie möchten, in die Komponenten des neuen Frameworks migrieren.

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

3. Einrichten und Playground kennenlernen

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

  • Sie können das ganz einfach online im Browser tun.
  • (Fortgeschritten) Mit VS Code auf dem lokalen Computer

Auf den Code zugreifen

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

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

// 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 ausführen und diese Prüfpunkte als Ausgangspunkt verwenden. Wenn Sie VS Code verwenden, können Sie mithilfe dieser Checkpoints den Startcode für jeden Schritt herunterladen und Ihre Arbeit überprüfen.

Erkundung der beleuchteten Spielplatz-Benutzeroberfläche

Die Registerkartenleiste der Dateiauswahl ist mit „Abschnitt 1“ beschriftet, der Abschnitt zur Codebearbeitung ist mit Abschnitt 2, die Ausgabevorschau als Abschnitt 3 und die Schaltfläche „Vorschau neu laden“ als Abschnitt 4 beschriftet.

Im Screenshot der Benutzeroberfläche von Lit Playground sind die Abschnitte hervorgehoben, 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 (erweitert)

Diese VS Code-Einrichtung bietet folgende Vorteile:

  • Prüfung des Vorlagentyps
  • Vorlagen-Intelligence und automatische Vervollständigung

Wenn Sie NPM und VS Code (mit dem lit-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:

  • Klicken Sie auf die Schaltfläche zum Herunterladen.
  • Inhalt der TAR-Datei in ein Verzeichnis extrahieren
  • (Bei TS) Quick tsconfig einrichten, die ES-Module und ES2015+ ausgibt
  • Installieren Sie einen dev-Server, der Bare-Modul-Bezeichner auflösen kann. Das Lit-Team empfiehlt @web/dev-server.
  • Führen Sie den dev-Server 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, verwenden 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 ihrem JavaScript-Code schreiben können. Lit-Vorlagen dienen einem ähnlichen Zweck: Sie stellen die Benutzeroberfläche einer Komponente als Funktion ihres Status dar.

Grundlegende Syntax

In React würden Sie eine JSX-Hello World wie folgt rendern:

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 enthaltene „name“-Variable. In Lit würden Sie Folgendes tun:

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

Für Lit-Vorlagen ist kein React-Fragment erforderlich, um mehrere Elemente in den Vorlagen zu gruppieren.

In „Lit“ sind Vorlagen mit html getaggten LIT-Vorlagen umschlossen. Lit erhält ebenfalls ihren Namen.

Vorlagenwerte

Lit-Vorlagen können andere Lit-Vorlagen enthalten, die als TemplateResult bezeichnet werden. Umschließen Sie name beispielsweise in Kursivschrift-Tags (<i>) und in einem getaggten Vorlagenliteral. Hinweis: Verwenden Sie das Graviszeichen (`) und nicht das Apostroph (').

import {html, render} from 'lit';

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

render(
  element,
  mountNode
);

Lit-TemplateResults können Arrays, Strings, andere TemplateResults und Anweisungen enthalten.

Als Übung können Sie versuchen, 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
);

Passieren und Requisiten aufstellen

Einer der größten Unterschiede zwischen der JSX- und der Lit-Syntax ist die Syntax der Datenbindung. Sehen wir uns beispielsweise diese React-Eingabe mit Bindungen an:

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 ausführt:

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

In Lit würden Sie Folgendes tun:

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 disabled-Attribut umzuschalten.

Als Nächstes erfolgt eine Bindung direkt an das class-Attribut und nicht an className. Sie können dem Attribut class mehrere Bindungen hinzufügen, es sei denn, Sie verwenden die Anweisung classMap, die ein deklaratives Hilfsprogramm zum Wechseln von Klassen ist.

Schließlich wird die Eigenschaft value auf die Eingabe festgelegt. Anders als in React wird das Eingabeelement dadurch nicht als schreibgeschützt festgelegt, da es der nativen Implementierung und dem Verhalten der Eingabe folgt.

Syntax für die Bindung von Lit-Properties

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 als disabled="false" verwenden, vom DOM trotzdem als „true“ interpretiert werden, weil inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • Das Präfix . ist die Bindungssyntax zum Festlegen einer Eigenschaft eines Elements.
  • Entspricht inputRef.propertyName = anyVar
  • Gut geeignet für die Weitergabe 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)
  • Geeignet für Basiswerte, Stilregelselektoren 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“ wird protokolliert, 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 Ereignis click ein Listener mit @click hinzugefügt.

Als Nächstes wird nicht onChange verwendet, sondern eine Bindung an das native input-Ereignis von <input>, da das native change-Ereignis nur bei blur ausgelöst wird (Reaktion-Zusammenfassungen über diese Ereignisse).

Syntax für Lit-Event-Handler

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

5. Komponenten &Requisiten

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

Klassenkomponenten und LitElement

Das Lit-Äquivalent einer React-Klassenkomponente ist LitElement und das Konzept der "reaktiven Eigenschaften" von Lit setzt sich aus den Eigenschaften und dem Status von React zusammen. 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 eine name
  • Legt den Standardwert von name auf einen leeren String fest ("")
  • name wird "Elliott" neu zugewiesen

So würden Sie dies 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>

Zusammenfassung des Vorgangs im obigen Beispiel:

@property({type: String})
name = '';
  • Definiert eine öffentliche reaktive Eigenschaft, also einen Teil der öffentlichen API Ihrer Komponente
  • Stellt (standardmäßig) ein Attribut sowie eine Property für Ihre Komponente bereit
  • Hier wird definiert, wie das Attribut der Komponente (Strings) in einen Wert umgewandelt wird.
static get properties() {
  return {
    name: {type: String}
  }
}
  • Er hat dieselbe Funktion wie der TS-Decorator @property, 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 Property ändert.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Hiermit wird der Tag-Name eines HTML-Elements einer Klassendefinition zugeordnet.
  • Gemäß dem Standard für benutzerdefinierte Elemente muss der Tag-Name einen Bindestrich (-) enthalten.
  • this in einem LitElement verweist auf die Instanz des benutzerdefinierten Elements (in diesem Fall <welcome-banner>).
customElements.define('welcome-banner', WelcomeBanner);
  • Dies ist das JavaScript-Äquivalent des @customElement-TS-Decorators.
<head>
  <script type="module" src="./index.js"></script>
</head>
  • Importiert die Definition des benutzerdefinierten Elements
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • Das benutzerdefinierte Element wird der Seite hinzugefügt.
  • Legt das Attribut name auf 'Elliott' fest

Funktionskomponenten

In Lit gibt es keine 1:1-Interpretation einer Funktionskomponente, da sie weder JSX noch einen Präprozessor verwendet. Es ist jedoch recht einfach, eine Funktion zu erstellen, die Eigenschaften annimmt und DOM basierend auf diesen Eigenschaften rendert. Beispiel:

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

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

In Lit würde das so aussehen:

import {html, render} from 'lit';

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

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

6. Status und Lebenszyklus

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

Status

Das Konzept „Reactive Properties“ von Lit ist eine Mischung aus den State- und Props-Objekten von React. Wenn sich reaktive Eigenschaften ändern, kann der Komponentenlebenszyklus ausgelöst werden. Es gibt zwei Varianten von reaktiven Properties:

Öffentliche reaktive Attribute

// 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 von @property
  • Ähnlich wie Props und State in React, aber veränderlich
  • Öffentliche API, auf die Nutzer der Komponente zugreifen und sie festlegen können

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

Lifecycle

Der Lebenszyklus von Lit ähnelt dem von React, es gibt jedoch einige bemerkenswerte 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';
  }
}
  • Das entspricht auch constructor
  • Es ist nicht erforderlich, dem Super-Aufruf etwas zu übergeben.
  • Aufgerufen durch (nicht vollständig inklusiv):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Auf der Seite befindet sich ein nicht aktualisierter Tag-Name und die Definition wird geladen und mit @customElement oder customElements.define registriert.
  • Ähnlich wie constructor von React

render

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

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Das entspricht auch render
  • Kann jedes renderbare Ergebnis zurückgeben, z.B. TemplateResult oder string usw.
  • Ähnlich wie „React“ sollte render() eine reine Funktion sein
  • Wird in jedem Knoten gerendert, den createRenderRoot() zurückgibt (standardmäßig ShadowRoot)

componentDidMount

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

firstUpdated

import Chart from 'chart.js';

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

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • Wird zum ersten Mal aufgerufen, wenn die Vorlage der Komponente in der Wurzel der Komponente gerendert wird.
  • Wird nur aufgerufen, wenn das Element verbunden ist. Wird also nicht über document.createElement('my-component') aufgerufen, bis dieser Knoten in den DOM-Baum eingefügt wurde.
  • Hier können Sie die Komponenten so einrichten, dass das von der Komponente gerenderte DOM erforderlich ist.
  • Im Gegensatz zu Reacts componentDidMount führen Änderungen an reaktiven Eigenschaften in firstUpdated zu einem erneuten Rendern. Der Browser führt die Änderungen jedoch in der Regel im selben Frame aus. Wenn für diese Änderungen kein Zugriff auf das DOM der Wurzel erforderlich ist, sollten sie normalerweise in willUpdate

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 die DOM-Baumstruktur eingefügt wird
  • Wenn benutzerdefinierte Elemente vom DOM getrennt werden, werden sie im Gegensatz zu React-Komponenten nicht gelöscht und können daher mehrmals „verbunden“ werden.
    • firstUpdated wird nicht noch einmal aufgerufen
  • Nützlich, um das DOM neu zu initialisieren oder Ereignis-Listener, die beim Trennen der Verbindung bereinigt wurden, wieder hinzuzufügen
  • Hinweis: connectedCallback wird möglicherweise vor firstUpdated aufgerufen. Daher ist das DOM beim ersten Aufruf 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);
  }
}
  • Das Lit-Äquivalent ist updated (die englische 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);
}
  • Liquiditätsäquivalent entspricht 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.
  • DOM innerhalb des Stamms ist noch an die Unterstruktur des Stamms angehängt
  • Nützlich zum Bereinigen von Event-Listenern und Leaky-Referenzen, damit der Browser die Komponente per Speicherbereinigung bereinigen kann

Übung

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:

  • Sie rendert „Hello World! Es ist“ und dann die Uhrzeit.
  • Jede Sekunde wird die Uhr aktualisiert
  • Wenn der Sensor entfernt wird, wird das Intervall gelöscht, das die Markierung auslöst.

Beginnen Sie zunächst 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);

Initialisieren Sie als Nächstes date und deklarieren Sie es als interne reaktive Eigenschaft mit @state, 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 Tick-Methode.

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

Als Nächstes kommt die Implementierung von componentDidMount. Auch hier ist das Lit-Analog eine Mischung aus firstUpdated und connectedCallback. Im Fall dieser Komponente benötigt der Aufruf von tick mit setInterval keinen Zugriff auf das DOM im Stammverzeichnis. Außerdem wird das Intervall gelöscht, wenn das Element aus dem Dokumentenbaum entfernt wird. Wenn es wieder angehängt wird, muss das Intervall also neu gestartet werden. Daher ist connectedCallback hier 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 abschließend das Intervall, damit der Tick nicht ausgeführt wird, nachdem das Element vom Dokumentenbaum getrennt wurde.

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

Das Ganze sollte wie folgt aussehen:

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

In diesem Abschnitt erfährst du, wie du React Hook-Konzepte in Lit

Die Konzepte von React-Hooks

React-Hooks bieten eine Möglichkeit, Funktionskomponenten in einen Status zu "verknüpfen". Dies hat mehrere Vorteile.

  • Sie vereinfachen die Wiederverwendung zustandsbasierter Logik.
  • Hilfe beim Aufteilen einer Komponente in kleinere Funktionen

Darüber hinaus wurden mit dem Fokus auf funktionsbasierten Komponenten bestimmte Probleme mit der klassenbasierten Syntax von React behoben, z. B.:

  • props muss von constructor nach super übergeben werden
  • Die unordentliche Initialisierung von Properties in der constructor
    • Dies war ein Grund, der damals vom React-Team angegeben wurde, aber mit ES2019 behoben wurde.
  • Probleme, die durch this verursacht werden, die sich nicht mehr auf die Komponente beziehen

React-Hooks-Konzepte in Lit

Wie im Abschnitt Komponenten und Requisiten erwähnt, bietet Lit keine Möglichkeit, benutzerdefinierte Elemente aus einer Funktion zu erstellen. LitElement löst jedoch die meisten Hauptprobleme mit Komponenten der React-Klasse. 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 nimmt keine Argumente an.
  • Alle @event Bindungen automatisch an this binden
  • this bezieht sich in den meisten Fällen auf den Verweis des benutzerdefinierten Elements
  • Klassenattribute können jetzt als Klassenmitglieder instanziiert werden. Dadurch werden konstruktorbasierte Implementierungen bereinigt.

Reaktive Controller

Die Hauptkonzepte hinter Hooks werden in Lit als reaktive Controller bezeichnet. Mit reaktiven Controller-Mustern können Sie zustandsabhängige Logik teilen, Komponenten in kleinere, modularere Teile aufteilen und sich an den Aktualisierungslebenszyklus eines Elements anschließen.

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

Der Lebenszyklus von ReactiveController und 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 mit addController an einen Host anhängen, wird der Lebenszyklus des Controllers zusammen mit dem des Hosts aufgerufen. Denken Sie zum Beispiel an die Uhr 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 „Hallo Welt! Es ist“ und dann die Uhrzeit.
  • Jede Sekunde wird die Uhr aktualisiert
  • Wenn der Sensor entfernt wird, wird das Intervall gelöscht, das die Markierung auslöst.

Bau des Gerüsts der Komponente

Beginnen Sie zuerst 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, erstellen Sie einen Kurs 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 beliebig erstellt werden, solange er die ReactiveController-Schnittstelle verwendet. Die Verwendung einer Klasse mit einer constructor, die eine ReactiveControllerHost-Schnittstelle sowie alle anderen Eigenschaften annehmen kann, die zum Initialisieren des Controllers erforderlich sind, ist ein Muster, das das Lit-Team in den meisten einfachen Fällen bevorzugt.

Jetzt müssen Sie die React-Lebenszyklusereignisse in Controller-Ereignisse umwandeln. Kurzum:

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

Weitere Informationen zur Umwandlung des React-Lebenszyklus in den Lit-Lebenszyklus finden Sie im Abschnitt Status und Lebenszyklus.

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.

// 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 Uhrencontroller verwenden möchten, importieren Sie ihn 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);

Wenn Sie den Controller verwenden möchten, müssen Sie ihn durch Weitergabe einer Referenz auf den Controller-Host (die <my-element>-Komponente) instanziieren und dann in der render-Methode verwenden.

Erneute Renderings im Controller auslösen

Die Uhrzeit wird angezeigt, wird 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 die date in der ClockController-Klasse ändert und nicht mehr in der Komponente. Das bedeutet, dass der Host angewiesen werden muss, seinen Update-Lebenszyklus mit host.requestUpdate() auszuführen, nachdem date auf dem Controller festgelegt wurde.

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

Jetzt sollte die Uhr ticken!

Einen detaillierteren Vergleich häufiger Anwendungsfälle mit Hooks finden Sie im Abschnitt Erweiterte Themen – Aufhänger.

8. Children

In diesem Abschnitt erfahren Sie, wie Sie mithilfe von Slots untergeordnete Elemente in Lit.

Spielautomaten und Kinder

Slots ermöglichen die Komposition, da Sie damit Komponenten verschachteln können.

In React werden untergeordnete Elemente durch Requisiten vererbt. Der Standard-Slot ist props.children und die Funktion render definiert, wo der Standard-Slot platziert wird. Beispiel:

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

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

In „Lit“ bestehen untergeordnete Elemente in der Renderingfunktion mit Slot-Elementen. Beachten Sie, dass untergeordnete Elemente nicht auf dieselbe Weise übernommen werden wie React. In Lit sind untergeordnete Elemente HTMLElements, die an Slots angehängt sind. Dieser Anhang wird als Projektion bezeichnet.

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

Mehrere Slots

In React entspricht das Hinzufügen mehrerer Anzeigenflächen im Grunde der Übernahme weiterer Eigenschaften.

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

Wenn du mehr <slot>-Elemente hinzufügst, werden auch mehr Slots in Lit erstellt. Mehrere Slots sind mit dem Attribut name definiert: <slot name="slot-name">. So können Kinder angeben, welcher Slot ihnen zugewiesen werden soll.

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

Standardinhalt des Slots

Slots zeigen ihren untergeordneten Knoten an, wenn keine Knoten auf diesen Slot projiziert werden. Wenn Knoten in einen Slot projiziert werden, zeigt dieser Slot nicht seine Unterstruktur, sondern projizierte Knoten an.

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

Untergeordnete Slots zuweisen

In React werden untergeordnete Elemente Slots über die Eigenschaften einer Komponente zugewiesen. Im folgenden Beispiel werden React-Elemente an die Props 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 Slots mithilfe des Attributs slot 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 kein Standardslot (z.B. <slot>) und kein Slot mit einem name-Attribut (z.B. <slot name="foo">) vorhanden ist, das mit dem slot-Attribut der untergeordneten Elemente des benutzerdefinierten Elements übereinstimmt (z.B. <div slot="foo">), wird dieser Knoten nicht projiziert und nicht angezeigt.

9. Verweise

Gelegentlich müssen Entwickler auf die API eines HTMLElements zugreifen.

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

Referenzen reagieren

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 HTMLElements gerendert.

In React sind Refs Speicherplatz im Arbeitsspeicher, der einen generierten 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 Schaltfläche mit Text rendern
  • Den Fokus auf die Eingabe legen, wenn auf die Schaltfläche geklickt wird

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

„References“ (Referenzen) mit @query austauschen

Die Bibliothek befindet sich nah am Browser und bietet eine sehr dünne Abstraktion nativer Browserfunktionen.

Die React-Entsprechung für refs in Lit ist das HTMLElement, das von den Decorators @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 Eigenschaft in MyElement mithilfe des Decorators @query (durch Erstellen eines Getters für einen HTMLInputElement).
  • Hier wird ein Rückruf für das Klickereignis namens onButtonClick deklariert und angehängt.
  • Der Fokus der Eingabe wird auf den Klick auf die Schaltfläche gelegt.

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

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

Nachdem die Lit-Komponente die Vorlage der render-Methode an das Stammelement von my-element gesendet hat, kann inputEl über den @query-Dekorator jetzt das erste input-Element zurückgeben, das im Renderstammelement gefunden wird. Wenn das angegebene Element nicht gefunden werden kann, wird null zurückgegeben.

Wenn sich im Render-Stamm mehrere input-Elemente befinden, gibt @queryAll eine Liste von Knoten zurück.

10. Vermittlungsstatus

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

Wiederverwendbare Komponenten

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

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 auf Grundlage des Werts props.step.
  • Rendert eine Schaltfläche mit dem Label „+step“ oder „-step“
  • Aktualisiert die übergeordnete Komponente, indem props.addToCounter mit props.step als Argument bei Klick aufgerufen wird

Obwohl es möglich ist, Callbacks in Lit zu übergeben, sind die herkömmlichen Muster anders. Die React-Komponente im Beispiel oben könnte im Beispiel unten als Lit-Komponente 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 Beispiel oben führt eine Lit-Komponente Folgendes aus:

  • Die reaktive Property step erstellen
  • Löst ein benutzerdefiniertes Ereignis namens update-counter aus, das beim Klick den Wert step des Elements enthält.

Browserereignisse werden von untergeordneten zu übergeordneten Elementen weitergegeben. Mit Ereignissen können Kinder Interaktionsereignisse und Statusänderungen übertragen. React übergibt den Status grundsätzlich in die entgegengesetzte Richtung. Daher ist es eher unüblich, dass React-Komponenten Ereignisse auf dieselbe Weise wie Lit-Komponenten senden und empfangen.

Komponenten mit Status

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

Im obigen Beispiel geschieht Folgendes:

  • Erstellt einen count-Status.
  • Erstellt einen Callback, der dem Status count eine Nummer 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 für ein übergeordnetes Element 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>
    `;
  }
}

Im obigen Beispiel geschieht Folgendes:

  • Erstellt eine reaktive Property namens count, die die Komponente aktualisiert, wenn sich der Wert ändert.
  • Bindet den addToCounter-Callback an den @update-counter-Ereignis-Listener
  • Aktualisiert count, indem der in detail.step des update-counter-Ereignisses gefundene Wert 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 aufzurufen.

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

11. Stile

In diesem Abschnitt lernen Sie die Stile in Lit.

Stile

Lit bietet mehrere Möglichkeiten, Elemente zu stylen, sowie eine integrierte Lösung.

Inline-Stile

Lit unterstützt Inline-Styles und 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, jeweils mit einem Inline-Stil.

Importieren Sie jetzt einen Rahmen von 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 lästig sein, die Stilzeichenfolge jedes Mal berechnen zu müssen, daher bietet Lit eine entsprechende Anweisung an.

styleMap

Die directive styleMap vereinfacht die Verwendung von JavaScript zum Festlegen von Inline-Styles. 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 Rahmen und einer Farbauswahl an
  • Ändert den border-color in den Wert aus der Farbauswahl

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

CSSResult

Wir empfehlen, das css-getaggte Vorlagenliteral zu verwenden, um Komponenten zu stylen.

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

Im obigen Beispiel geschieht Folgendes:

  • Deklariert ein CSS-getaggtes Vorlagenliteral mit einer Bindung
  • Legt die Farben von zwei h1s mit IDs fest

Vorteile des css-Vorlagetags:

  • Einmal pro Kurs oder pro Instanz geparst
  • Modular und wiederverwendbar
  • Stile lassen sich ganz einfach in eigene Dateien trennen.
  • Kompatibel mit der Polyfill-Funktion für benutzerdefinierte CSS-Properties

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

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

Lit ordnet die Stile Ihrer Komponenten ihren Wurzeln zu. Das bedeutet, dass Stile nicht ein- und herausragen. Zur Übergabe von Stilen an Komponenten empfiehlt das Lit-Team die Verwendung von benutzerdefinierten CSS-Eigenschaften, da diese den Gültigkeitsbereich von Lit-Stilen durchdringen können.

Stil-Tags

Sie können <style>-Tags auch einfach inline in Ihre Vorlagen einfügen. Der Browser dedupliziert diese Stil-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.

Die Verwendung von <link rel="stylesheet"> in Ihrer Vorlage ist auch für Stile möglich. Dies wird jedoch nicht empfohlen, da dies zu einem anfänglichen Blitzen von Inhalten ohne Stil (FOUC) führen kann.

12. Erweiterte Themen (optional)

JSX und Vorlagen

Lit und Virtual DOM

Lit-HTML enthält kein konventionelles virtuelles DOM, das die einzelnen Knoten unterscheidet. Stattdessen werden Leistungsmerkmale genutzt, die der Spezifikation Tagged-Vorlagenliteral von ES2015 unverzichtbar sind. Getaggte Vorlagenliterale sind Vorlagenliteralstrings, an die Tag-Funktionen angehängt sind.

Hier ein Beispiel für ein Vorlagenliteral:

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

Hier ist ein Beispiel für ein getaggtes Vorlagenliteral:

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ßer Vorteil der Leistung in „Lit“ besteht darin, dass die an die Tag-Funktion übergebenen String-Arrays denselben Zeiger haben (siehe zweite console.log). Der Browser erstellt nicht bei jedem Tag-Funktionsaufruf ein neues strings-Array neu, da er dasselbe Vorlagenliteral verwendet (d.h. an derselben Stelle in der AST). So können die Bindung, das Parsen und das Vorlagen-Caching von Lit diese Funktionen ohne großen Overhead bei der Laufzeitdifferenzierung nutzen.

Dieses integrierte Browserverhalten von getaggten Vorlagenliteralen bietet Lit einen erheblichen Leistungsvorteil. Die meisten herkömmlichen virtuellen DOMs erledigen den Großteil ihrer Arbeit in JavaScript. Bei getaggten Vorlagenliteralen erfolgt der Großteil des Vergleichs jedoch im C++-Code des Browsers.

Wenn Sie HTML-getaggte Vorlagenliterale in React oder Preact verwenden möchten, empfiehlt das Lit-Team die htm-Bibliothek.

Allerdings werden Sie, wie bei der Google Codelabs-Website und mehreren Online-Code-Editoren, feststellen, dass die literale Syntaxhervorhebung in Vorlagen nicht sehr üblich ist. Einige IDEs und Texteditoren unterstützen diese standardmäßig, z. B. Atom und der Codeblock Highlighter von GitHub. Das Lit-Team arbeitet außerdem eng mit der Community zusammen, um Projekte wie das lit-plugin zu pflegen. Dies ist ein VS Code-Plug-in, das Ihre Lit-Projekte um Syntaxhervorhebung, Typprüfung und Intelligenz ergänzt.

Lit und JSX + React DOM

JSX wird nicht im Browser ausgeführt, sondern mit einem Preprozessor in JavaScript-Funktionsaufrufe umgewandelt (in der Regel über Babel).

Babel wandelt beispielsweise Folgendes um:

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

in diese Datei einfügen:

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

React DOM verwendet dann die React-Ausgabe und übersetzt sie in das tatsächliche DOM – Eigenschaften, Attribute, Ereignis-Listener und so weiter.

Lit-HTML verwendet getaggte Vorlagenliterale, die ohne Transpilation oder Präprozessor im Browser ausgeführt werden können. Um mit Lit zu beginnen, benötigen Sie also nur eine HTML-Datei, ein ES-Modulscript und einen Server. Hier ist ein vollständig im Browser ausführbares Script:

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

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

Da das Vorlagensystem von Lit, lit-html, kein herkömmliches virtuelles DOM verwendet, sondern direkt das DOM-API nutzt, liegt die Größe von Lit 2 unter 5 KB minimiert und gzip-im Vergleich zu React (2,8 KB) und React (39,4 KB) mit 40 KB minimiert und gizip.

Ereignisse

React verwendet ein synthetisches Ereignissystem. Das bedeutet, dass die Reaktionsdomäne jedes Ereignis definieren muss, das für jede Komponente verwendet wird, und ein CamelCase-Ereignis-Listener-Äquivalent für jeden Knotentyp bereitstellen muss. Daher verfügt JSX über keine Methode zum Definieren eines Ereignis-Listeners für ein benutzerdefiniertes Ereignis. Entwickler müssen eine ref verwenden und dann imperativ einen Listener anwenden. Dies führt zu einer suboptimalen Entwicklererfahrung bei der Integration von Bibliotheken, die React nicht berücksichtigen. Daher muss ein React-spezifischer Wrapper geschrieben werden.

Lit-html greift direkt auf das DOM zu und verwendet native Ereignisse, sodass das Hinzufügen von Ereignis-Listenern so einfach ist wie @event-name=${eventNameListener}. Das bedeutet, dass beim Hinzufügen von Ereignis-Listenern und zum Auslösen von Ereignissen weniger das Parsen der Laufzeit durchgeführt wird.

Komponenten &Requisiten

React-Komponenten und benutzerdefinierte Elemente

Intern verwendet LitElement benutzerdefinierte Elemente, um seine Komponenten zu verpacken. Bei benutzerdefinierten Elementen gibt es einige Kompromisse zwischen React-Komponenten bei der Komponentenisierung. Status und Lebenszyklus werden im Abschnitt Status und Lebenszyklus näher erläutert.

Einige Vorteile von benutzerdefinierten Elementen als Komponentensystem:

  • Sie sind browsereigen und erfordern keine Tools.
  • Passend für jede Browser-API von innerHTML über document.createElement bis querySelector
  • Können in der Regel in verschiedenen Frameworks verwendet werden
  • Kann verzögert bei customElements.define und DOM „hydrate“ registriert werden

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

  • Ein benutzerdefiniertes Element kann nicht ohne Definition einer Klasse erstellt werden (d. h. ohne JSX-ähnliche funktionale Komponenten).
  • Muss ein schließendes Tag
      enthalten.
    • Hinweis: Trotz der Vorteile für Entwickler bedauern Browseranbieter 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, der zu Layoutproblemen führen kann.
  • Muss über JavaScript registriert werden

Bei Lit wurden benutzerdefinierte Elemente anstelle eines benutzerdefinierten Elementsystems verwendet, da die benutzerdefinierten Elemente in den Browser eingebunden sind. Das Lit-Team ist der Meinung, dass die Vorteile eines plattformübergreifenden Frameworks die Vorteile einer Komponentenabstraktionsschicht überwiegen. Tatsächlich haben die Bemühungen des Lit-Teams im Bereich „lit-ssr“ die Hauptprobleme bei der JavaScript-Registrierung behoben. Außerdem nutzen einige Unternehmen wie GitHub die Lazy-Registrierung benutzerdefinierter Elemente, um Seiten nach und nach mit optionalen Designelementen zu optimieren.

Daten an benutzerdefinierte Elemente übergeben

Ein weitverbreiteter Irrglaube bei benutzerdefinierten Elementen ist, dass Daten nur als Strings übergeben werden können. Diese Missverständnis ergibt sich wahrscheinlich aus der Tatsache, dass Elementattribute nur als Strings geschrieben werden können. Obwohl es stimmt, dass Lit Zeichenfolgenattribute in ihre definierten Typen umwandelt, können benutzerdefinierte Elemente auch komplexe Daten als Eigenschaften akzeptieren.

Beispiel: Hier ist die Definition eines LitElements:

// 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 Eigenschaft num definiert, die den Stringwert eines Attributs in einen number umwandelt. Anschließend wird eine komplexe Datenstruktur mit attribute:false eingeführt, wodurch die Attributverarbeitung von Lit deaktiviert wird.

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>

Status und Lebenszyklus

Andere React-Lebenszyklus-Callbacks

static getDerivedStateFromProps

In Lit gibt es keine entsprechende Funktion, da Props und State dieselben Klasseneigenschaften sind.

shouldComponentUpdate

  • Der Lit-Wert ist shouldUpdate.
  • Wird im Gegensatz zu React beim ersten Rendering aufgerufen
  • Funktioniert ähnlich wie shouldComponentUpdate in React

getSnapshotBeforeUpdate

In Lit ist getSnapshotBeforeUpdate sowohl update als auch willUpdate ähnlich.

willUpdate

  • Vor dem update angerufen
  • Im Gegensatz zu getSnapshotBeforeUpdate wird willUpdate vor render aufgerufen
  • Durch Änderungen an reaktiven Properties in „willUpdate“ wird der Aktualisierungszyklus nicht noch einmal ausgelöst
  • Hier können Property-Werte berechnet werden, die von anderen Properties abhängen und im restlichen Aktualisierungsprozess verwendet werden.
  • Diese Methode wird auf dem Server in SSR aufgerufen, daher wird hier nicht empfohlen, auf das DOM zuzugreifen.

update

  • Nach dem willUpdate angerufen
  • Im Gegensatz zu getSnapshotBeforeUpdate wird update vor render aufgerufen.
  • Änderungen an reaktiven Properties in update lösen den Aktualisierungsvorgang nicht noch einmal aus, wenn sie vor dem Aufruf von super.update vorgenommen werden.
  • Guter Ort zum Erfassen von Informationen aus dem DOM, das die Komponente umgibt, bevor die gerenderte Ausgabe an das DOM übergeben wird
  • Diese Methode wird auf dem Server in SSR nicht aufgerufen.

Andere Lit Lifecycle-Callbacks

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

attributeChangedCallback

Es 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 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 documentFragment eines HTMLTemplateElement in die Haupt-document. Dieser Rückruf ist ebenfalls Teil der Spezifikation für benutzerdefinierte Elemente und sollte nur für erweiterte Anwendungsfälle verwendet werden, wenn die Komponente Dokumente ändert.

Weitere Lebenszyklusmethoden und ‑eigenschaften

Diese Methoden und Eigenschaften sind Klassenmitglieder, die Sie aufrufen, überschreiben oder abwarten können, um den Lebenszyklus zu beeinflussen.

updateComplete

Dies ist ein Promise, der aufgelöst wird, wenn die Aktualisierung des Elements abgeschlossen ist, da die Aktualisierungs- und Renderingzyklen 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, wenn updateComplete aufgelöst wird. Dies ist häufig der Fall, wenn eine Komponente eine untergeordnete Komponente rendert und ihre Renderzyklen synchronisiert werden müssen. Beispiele:

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

performUpdate

Über diese Methode werden die Lifecycle-Callbacks für die Aktualisierung aufgerufen. Dies sollte im Allgemeinen nicht erforderlich sein, außer in seltenen Fällen, in denen die Aktualisierung synchron oder im Rahmen einer benutzerdefinierten Planung erfolgen muss.

hasUpdated

Diese Property hat den Wert true, wenn die Komponente mindestens einmal aktualisiert wurde.

isConnected

Diese Eigenschaft ist Teil der Spezifikation für benutzerdefinierte Elemente und lautet true, wenn das Element derzeit an die Hauptdokumentenstruktur angehängt ist.

Visualisierung des Lebenszyklus von Lit-Aktualisierungen

Der Aktualisierungszyklus besteht aus drei Teilen:

  • Vor der Aktualisierung
  • Aktualisieren
  • Nach der Aktualisierung

Vor dem Update

Ein gerichteter azyklischer Graph von Knoten mit Rückrufnamen. Konstruktor zu requestUpdate. @property zu Property Setter. attributeChangedCallback zu Property Setter. Property Setter to hasChanged. hasChanged to requestUpdate. requestUpdate points out to the next, update lifecycle graph.

Nach dem requestUpdate wird ein geplantes Update erwartet.

Aktualisieren

Ein gerichteter azyklischer Graph mit Knoten mit Rückrufnamen. Pfeil vom vorherigen Bild der Lebenszykluspunkte vor der Aktualisierung, um performUpdate auszuführen. „PerformUpdate“ zu „ shouldUpdate“. shouldUpdate Punkte sollte sowohl auf „complete update if false“ als auch auf willUpdate. willUpdate. update. update. Mit „update“ wird sowohl auf das Rendering als auch auf die nächste Grafik für den Lebenszyklus nach der Aktualisierung aktualisiert. Das Rendering verweist auch auf die nächste Grafik für den Lebenszyklus nach der Aktualisierung.

Nach dem Update

Ein gerichteter azyklischer Graph von Knoten mit Callback-Namen. Der Pfeil vom vorherigen Bild des Update-Lebenszyklus verweist auf „firstUpdated“. „firstUpdated“ verweist auf „updated“. „updated“ verweist auf „updateComplete“.

Hooks

Warum-Hooks

In React wurden Hooks für einfache Anwendungsfälle von Funktionskomponenten eingeführt, bei denen ein Zustand erforderlich war. In vielen einfachen Fällen sind Funktionskomponenten mit Hooks in der Regel viel einfacher und übersichtlicher als ihre Klassenkomponenten. Bei der Einführung asynchroner Statusaktualisierungen und der Weitergabe von Daten zwischen Hooks oder Effekten reicht das Hook-Muster jedoch tendenziell nicht aus, und eine klassenbasierte Lösung wie reaktive Controller glänzen in der Regel.

API-Anfrage-Hooks und -Controller

Es ist üblich, einen Hook zu schreiben, der Daten von einer API anfordert. Nehmen wir als Beispiel diese React-Funktionskomponente, die Folgendes ausführt:

  • index.tsx
    • Rendert Text
    • Antwort von useAPI wird gerendert
      • Nutzer-ID + Nutzername
      • Fehlermeldung
        • 404-Fehler, wenn Nutzer 11 erreicht wird (beabsichtigt)
        • Fehler beim Abbrechen des API-Abrufs
      • Benachrichtigung zum Laden
    • Rendert eine Aktionsschaltfläche
      • Nächster Nutzer: ruft die API für den nächsten Nutzer ab
      • Abbrechen: Dadurch wird der API-Abruf abgebrochen und ein Fehler angezeigt.
  • useApi.tsx
    • Definiert einen benutzerdefinierten useApi-Hook
    • Ruft ein Nutzerobjekt asynchron von einer API ab
    • Sendet:
      • Nutzername
      • Gibt an, ob die Daten abgerufen werden
      • Alle Fehlermeldungen
      • Callback zum Abbrechen des Abrufs
    • Bricht Abrufe ab, wenn sie getrennt werden

Hier sehen Sie die Implementierung des Lit + Reactive Controllers.

Fazit:

  • Reaktive Controller ähneln am ehesten benutzerdefinierten Hooks.
  • Nicht renderbare Daten zwischen Callbacks und Effekten übergeben
    • React verwendet useRef, um Daten zwischen useEffect und useCallback zu übergeben
    • Lit verwendet eine private Klasseneigenschaft
    • React ist im Wesentlichen das Nachahmen des Verhaltens einer Privatklassen-Property

Wenn Ihnen die Syntax der React-Funktionskomponente mit Hooks gefällt, Sie aber die serverlose Umgebung von Lit bevorzugen, empfiehlt das Lit-Team die Haunted-Bibliothek.

Children

Standard-Slot

Wenn HTML-Elementen kein slot-Attribut zugewiesen ist, werden sie dem standardmäßigen unbenannten Steckplatz zugewiesen. Im folgenden Beispiel fügt MyApp einen Absatz in einen benannten Slot ein. Der andere Absatz wird standardmäßig in den unbenannten Slot verschoben.“

@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 nachgeordneten Slots ändert, wird ein slotchange-Ereignis ausgelöst. Eine Lit-Komponente kann einen Ereignis-Listener an ein slotchange-Ereignis binden. Im folgenden Beispiel werden die assignedNodes des ersten Slots, der in shadowRoot gefunden wird, in der Konsole unter slotchange 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>
   `;
  }
}

Verweise

Referenzerstellung

Sowohl Lit als auch React geben nach dem Aufruf ihrer render-Funktionen eine Referenz auf einen HTMLElement zurück. Es lohnt sich jedoch, 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, die React-Komponenten und keine HTML-Elemente erstellt. Da ein Ref deklariert wird, bevor ein HTMLElement gerendert wird, wird ein Leerzeichen im Arbeitsspeicher zugeordnet. Aus diesem Grund wird null als Anfangswert einer Ref angezeigt, 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, sucht es in der ReactComponent nach einem Attribut namens ref. Sofern verfügbar, platziert ReactDOM den Verweis des HTMLElements auf ref.current.

LitElement verwendet die Vorlagen-Tag-Funktion html aus lit-html, um ein Template-Element zu erstellen. LitElement prägt den Inhalt der Vorlage nach dem Rendern in das Shadow DOM eines benutzerdefinierten Elements ein. Das Schatten-DOM ist eine beschränkte DOM-Baumstruktur, die von einem Schattenstamm gekapselt wird. Der @query-Dekorator erstellt dann einen Getter für die Property, der im Wesentlichen eine this.shadowRoot.querySelector auf dem übergeordneten Root ausführt.

Mehrere Elemente abfragen

Im folgenden Beispiel gibt der @queryAll-Dekorator die beiden Absätze im Schatten-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 Wesentlichen erstellt @queryAll einen Getter für paragraphs, der die Ergebnisse von this.shadowRoot.querySelectorAll() zurückgibt. In JavaScript kann ein Getter für denselben Zweck deklariert werden:

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

Elemente, die die Abfrage ändern

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

Im folgenden Beispiel findet @queryAsync das erste Absatzelement. Ein Absatzelement wird jedoch nur gerendert, wenn renderParagraph zufällig eine ungerade Zahl generiert. Die @queryAsync-Direktive gibt ein Promise zurück, das aufgelöst wird, sobald 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. Es empfiehlt sich, sich nicht auf den Zustand der Elemente zu verlassen. Das DOM ist einfach eine Folge des Rendering-Prozesses.

Externer Status

Sie können neben Lit auch Redux, MobX oder eine andere Bibliothek zur Zustandsverwaltung verwenden.

Lit-Komponenten werden im Browserbereich erstellt. Jede Bibliothek, die auch im Browserbereich vorhanden ist, ist für Lit verfügbar. Es wurden viele erstaunliche Bibliotheken entwickelt, um die bestehenden staatlichen Verwaltungssysteme in Lit.

Hier ist eine Reihe von Vaadin, in der erläutert wird, wie Redux in einer Lit-Komponente genutzt wird.

Sehen Sie sich lit-mobx von Adobe an, um zu erfahren, wie MobX in Lit auf einer großen Website genutzt werden kann.

Sehen Sie sich auch Apollo Elements an, um zu sehen, 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. Schattenwurzeln generieren einen Schattenbaum, der vom Hauptdokumentenbaum getrennt ist. Das bedeutet, dass die meisten Stile auf dieses Dokument angewendet werden. Bestimmte Stile wie Farben und andere schriftartbezogene Stile können jedoch durchdringen.

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 freigeben

Mit Lit können Sie Stile ganz einfach in Form von CSSTemplateResults über css-Vorlagen-Tags für Komponenten freigeben. 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

Schattenwurzeln stellen eine kleine Herausforderung für herkömmliche Themen dar, bei denen in der Regel Top-down-Tag-Ansätze verwendet werden. Die herkömmliche Methode zur Gestaltung von Webkomponenten mit Shadow DOM besteht darin, eine Style-API über benutzerdefinierte CSS-Properties bereitzustellen. Dies ist beispielsweise 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 ändert dann das Design der Website, indem er benutzerdefinierte Eigenschaftswerte anwendet:

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

Wenn das Top-down-Design unverzichtbar ist und Sie keine Stile bereitstellen können, können Sie Shadow DOM jederzeit deaktivieren. Überschreiben Sie dazu createRenderRoot und geben Sie this zurück. Dadurch wird die Vorlage Ihrer Komponenten in das benutzerdefinierte Element selbst gerendert, anstatt in einen Schattenstamm, der an das benutzerdefinierte Element angehängt ist. Dadurch gehen Sie verloren: Stilkapselung, DOM-Kapselung und Slots.

Produktion

IE 11

Wenn Sie ältere Browser wie IE 11 unterstützen möchten, müssen Sie einige Polyfills laden, die etwa 33 KB groß sind. Weitere Informationen finden Sie hier.

Bedingte Sets

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

  • Das Bereitstellen von ES 6 ist schneller und wird den meisten Ihrer Kunden dienen.
  • Transpiliertes ES 5 erhöht die Bundle-Größe erheblich
  • Bedingte Sets bieten das Beste aus beiden Welten
    • IE 11-Unterstützung
    • Keine Verlangsamung in modernen Browsern

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