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 kannst du verschiedene React-Konzepte in Lit übersetzen, z. B.:

  • JSX und Vorlagen
  • Komponenten und Requisiten
  • Status & 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 Chrome-Entwicklertools.
  • Wissen über React
  • (Fortgeschritten) Für die bestmögliche Entwicklung laden Sie VS Code herunter. Sie benötigen außerdem lit-plugin für VS Code und NPM.

2. Lit vs. React

Die Kernkonzepte und -funktionen von Lit ähneln in vielerlei Hinsicht dem von React, es gibt aber auch einige wesentliche Unterschiede und Unterscheidungsmerkmale:

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 Paketgröße in KB minimiert und komprimiert. Die Lichtleiste ist 5 KB groß und das React- und das React-DOM ist 42,2 KB groß

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 der Leistung im Vergleich zwischen Leuchten und React in Millisekunden (niedrigere Werte sind besser)

Benötigt keine Kreation

Durch neue Browserfunktionen wie ES-Module und getaggte Vorlagenliterale muss keine Kompilierung ausgeführt werden. Das bedeutet, dass Entwicklungsumgebungen mit einem Skript-Tag, einem Browser und einem Server eingerichtet werden können und Sie sofort loslegen 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 jedoch weiteren Lit-Code erstellen und optimieren. Die jüngste Konsolidierung der Entwickler um native ES-Module war für Lit – Lit ist einfach nur normales JavaScript und es werden keine Framework-spezifischen Befehlszeilen oder Build-Handhabungen benötigt.

Unabhängig vom Framework

Die Komponenten von Lit basieren auf einer Reihe von Webstandards, den sogenannten Webkomponenten. Das bedeutet, dass die Erstellung einer Komponente in Lit für aktuelle und zukünftige Frameworks funktioniert. Wenn HTML-Elemente unterstützt werden, werden auch Webkomponenten unterstützt.

Die einzigen Probleme mit der Framework-Interoperabilität treten auf, wenn die Frameworks eine restriktive 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 sehen Sie unter Benutzerdefinierte Elemente überall, mit welchen Frameworks und Bibliotheken benutzerdefinierte Elemente gut funktionieren.

Erstklassige TypeScript-Unterstützung

Sie können Ihren gesamten Lit-Code zwar in JavaScript schreiben, Lit ist jedoch in TypeScript geschrieben und das Lit-Team empfiehlt Entwicklern, auch 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 Intelisense-Vorschlägen

Entwicklertools sind in den Browser integriert

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

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

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.

Lit 2 wurde für die SSR-Unterstützung entwickelt. 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 erwartet, diese Tools bald extern auf GitHub zu veröffentlichen.

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

Die Zustimmung ist gering

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

Hast du eine ganze App in Lit erstellt und möchtest du sie ändern? Dann können Sie Ihre aktuelle Lit-Anwendung in Ihr neues Framework einfügen und beliebige Komponenten zu den Komponenten des neuen Frameworks migrieren.

Darüber hinaus unterstützen viele moderne Frameworks die Ausgabe in Webkomponenten, sodass sie normalerweise selbst in ein Lit-Element passen.

3. Einrichtung und Playground erkunden

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

  • Sie können alles online über den Browser erledigen.
  • (Fortgeschritten) Mit VS Code auf dem lokalen Computer

Auf den Code zugreifen

Im gesamten Codelab finden Sie Links zum Lit Playground:

Der Playground ist eine Code-Sandbox, die vollständig in deinem Browser ausgeführt wird. Sie kann TypeScript- und JavaScript-Dateien kompilieren und ausführen und außerdem Importe in Knotenmodule automatisch auflösen. z.B.

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

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

Du kannst das gesamte Tutorial im Lit Playground durchgehen und diese Checkpoints als Ausgangspunkt verwenden. Wenn Sie VS Code verwenden, können Sie diese Checkpoints nutzen, um den Startcode für einen beliebigen Schritt herunterzuladen und damit Ihre Arbeit zu ü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)

Dies sind die Vorteile dieser VS Code-Einrichtung:

  • Prüfung des Vorlagentyps
  • Template Intellisense & automatische Vervollständigung

Wenn Sie NPM und VS Code (mit dem lit-plugin-Plug-in) bereits installiert haben und wissen, wie Sie diese Umgebung verwenden können, 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 Fehlerbehebung) Richten Sie eine schnelle tsconfig ein, 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 Beleuchtete Vorlagen

JSX ist eine Syntaxerweiterung für JavaScript, mit der React-Nutzer ganz einfach Vorlagen in ihren 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 einen enthaltenen "name". . 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
);

Beachten Sie, dass Lit-Vorlagen kein React-Fragment benötigen, um mehrere Elemente in seinen 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 akzeptieren, die als TemplateResult bezeichnet werden. Setzen Sie beispielsweise name in kursiv (<i>) Tags und umschließen Sie ihn mit einem getaggten Vorlagenliteral Hinweis. Verwenden Sie dabei ein Graviszeichen (`) und nicht ein einfaches 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
);

Lit-TemplateResults können Arrays, Strings, andere TemplateResults sowie Anweisungen akzeptieren.

Versuchen Sie für eine Übung, den folgenden React-Code in Lit umzuwandeln:

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 Syntax von JSX und Lit ist die Syntax für die Datenbindung. Nehmen wir als Beispiel diese 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 ausführt:

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

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.

Dann gibt es eine direkte Bindung an das Attribut class statt 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 das Attribut value für 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 Lit-Prop-Bindung

html`<my-element ?attribute-name=${booleanVar}>`;
  • Das Präfix ? ist die Bindungssyntax zum Ein-/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 eines Attributs eines Elements
  • Entspricht inputRef.propertyName = anyVar
  • Gut zur Weitergabe komplexer Daten wie Objekte, Arrays oder Klassen
html`<my-element attribute-name=${stringVar}>`;
  • Bindung an das Attribut eines Elements
  • 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 ausführt:

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

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

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 Event-Listener
  • Entspricht inputRef.addEventListener('event-name', ...)
  • Nutzt native DOM-Ereignisnamen

5. Komponenten und Requisiten

In diesem Abschnitt lernen Sie die Komponenten und Funktionen der Lit-Klasse kennen. 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 ist eine Kombination der Eigenschaften und des Zustands 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 ein name
  • Legt den Standardwert von name auf einen leeren String fest ("")
  • Weist name neu zu "Elliott"

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 ein Attribut (standardmäßig) sowie eine Eigenschaft Ihrer Komponente bereit
  • Definiert, wie das Attribut der Komponente (bei dem es sich um Strings handelt) in einen Wert übersetzt 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>`
}
  • Wird immer dann aufgerufen, wenn eine reaktive Eigenschaft geändert wird
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Hiermit wird der Name eines HTML-Element-Tags mit einer Klassendefinition verknüpft.
  • Aufgrund des Standards 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 zum TS-Decorator @customElement.
<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ä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. Status & Lebenszyklus

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

Status

Konzept von Lit zu „Reaktiven Eigenschaften“ ist eine Mischung aus Status und Requisiten von React. Wenn reaktive Eigenschaften geändert werden, kann dadurch der Lebenszyklus der Komponente 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 durch @property
  • Ähnlich wie die Eigenschaften und den Zustand von React, aber veränderlich
  • Öffentliche API, auf die Nutzer der Komponente zugreifen und sie festlegen können

Interner reaktiver Zustand

// 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 von React, aber veränderbar
  • Privater interner Status, auf den normalerweise über die Komponente oder Unterklassen zugegriffen wird

Lifecycle

Der Lit-Lebenszyklus ist dem von React sehr ähnlich, es gibt jedoch einige wesentliche 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';
  }
}
  • Lit-Äquivalent ist ebenfalls constructor
  • Es muss nichts an den Super Call übergeben werden.
  • Aufgerufen durch (nicht vollständig inklusiv):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Befindet sich auf der Seite 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>`;
}
  • Lit-Äquivalent ist ebenfalls render
  • Kann jedes Rendering-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 aufgerufen, wenn die Vorlage der Komponente zum ersten Mal im Stamm der Komponente gerendert wird
  • Wird nur aufgerufen, wenn das Element verbunden ist, z.B. erst über document.createElement('my-component') aufgerufen, bis dieser Knoten an die DOM-Struktur angehängt wird
  • Hier können Sie die Komponenten so einrichten, dass das von der Komponente gerenderte DOM erforderlich ist.
  • Im Gegensatz zu den componentDidMount-Änderungen an den reaktiven Eigenschaften von React in firstUpdated führen die Änderungen zu einem neuen Rendering, obwohl der Browser die Änderungen in der Regel im selben Frame stapelt. Wenn diese Änderungen keinen Zugriff auf das Stamm-DOM erfordern, sollten sie normalerweise in willUpdate vorgenommen werden.

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 „verbunden“ werden mehrmals
    • 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 kann vor firstUpdated aufgerufen werden, sodass beim ersten Aufruf möglicherweise kein DOM verfügbar ist.

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 kleine Äquivalent ist updated (in der englischen Vergangenheitsform „update“)
  • Im Gegensatz zu React wird updated auch beim ersten Rendering aufgerufen.
  • Ähnlich wie componentDidUpdate von 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 werden benutzerdefinierte Elemente, die vom DOM getrennt werden, nicht zerstört.
  • Im Gegensatz zu componentWillUnmount wird disconnectedCallback aufgerufen, nachdem das Element aus der Baumstruktur entfernt wurde
  • 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 ausführt:

  • Sie rendert „Hello World! Es ist“ und zeigt dann die Uhrzeit an,
  • Jede Sekunde wird die Uhr aktualisiert
  • Nach dem Abnehmen wird das Intervall gelöscht, das die Markierung markiert.

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

Als Nächstes rendern Sie 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 der Dokumentstruktur entfernt wird. Wenn es also wieder angehängt wird, muss das Intervall von vorn beginnen. 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 von der Dokumentstruktur 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

Reaktions-Hooks ermöglichen es, Funktionskomponenten in einen Zustand verwandelt. Dies hat mehrere Vorteile.

  • Sie vereinfachen die Wiederverwendung von zustandsorientierter 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 unsaubere Initialisierung von Eigenschaften im constructor
    • Dies war ein Grund, der damals vom React-Team genannt wurde, aber bis ES2019 behoben wurde
  • Probleme, die dadurch verursacht werden, dass sich this nicht mehr auf die Komponente bezieht

React-Hook-Konzepte in Lit

Wie im Abschnitt Komponenten und Props enthält, 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 diese Probleme an?

  • 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 gibt es in Lit als reaktive Controller. Reaktive Controllermuster ermöglichen das Teilen einer zustandsorientierten Logik, das Aufteilen von Komponenten in kleinere, modularere Bits sowie die Einbindung in den Aktualisierungslebenszyklus eines Elements.

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 parallel zum Host aufgerufen. Rufen Sie sich zum Beispiel das Uhrenbeispiel aus der Spalte State & Abschnitt zum 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:

  • Sie rendert „Hello World! Es ist“ und zeigt dann die Uhrzeit an,
  • Jede Sekunde wird die Uhr aktualisiert
  • Nach dem Abnehmen wird das Intervall gelöscht, das die Markierung markiert.

Bau des Gerüsts der Komponente

Beginnen Sie mit der Deklaration der Komponentenklasse und fügen Sie die Funktion render 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-Lebenszyklus-Callbacks in Controller-Callbacks übersetzen. Kurzum:

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

Weitere Informationen zur Übersetzung des React-Lebenszyklus in den Lit-Lebenszyklus findest du im Artikel zu 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 unter Status & 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 du den Uhr-Controller verwenden möchtest, importiere den Controller und aktualisiere 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);

Zur Verwendung des Controllers müssen Sie den Controller instanziieren. Dazu übergeben Sie einen Verweis auf den Controller-Host (die Komponente <my-element>) und dann den Controller in der render-Methode verwenden.

Erneute Renderings im Controller auslösen

Beachten Sie, dass hier die Uhrzeit angezeigt wird, diese aber nicht aktualisiert wird. Das liegt daran, dass der Controller das Datum jede Sekunde festlegt, der Host jedoch nicht aktualisiert. Das liegt daran, dass sich date in der Klasse ClockController und nicht mehr in der Komponente ändert. Das bedeutet, dass nach dem Festlegen von date auf dem Controller der Host 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();
}

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 Zusammensetzung, da Sie Komponenten verschachteln können.

In React werden untergeordnete Elemente durch Requisiten vererbt. Die Standardfläche ist props.children und die render-Funktion definiert, wo die Standardfläche positioniert wird. Beispiel:

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

Denken Sie daran, dass props.children React-Komponenten und keine HTML-Elemente sind.

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 Anzeigenflächen 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 Sie weitere <slot>-Elemente hinzufügen, entstehen mehr Flächen in Lit. Mit dem name-Attribut <slot name="slot-name"> werden mehrere Anzeigenflächen definiert. So können untergeordnete Elemente deklarieren, welcher Slot ihnen zugewiesen wird.

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

Standard-Anzeigenfläche

Slots zeigen ihre Unterstruktur an, wenn für diesen Slot keine Knoten prognostiziert wurden. 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 Anzeigenflächen über die Eigenschaften einer Komponente untergeordnete Elemente zugewiesen. Im folgenden Beispiel 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 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 muss ein 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 rendert HTMLElements.

In React stellen Refs den Speicherplatz im Speicher dar, 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:

  • Leere Texteingabe und Schaltfläche mit Text rendern
  • Eingabe fokussieren, wenn auf die Schaltfläche geklickt wird

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

Lit „Referenzen“ mit @query

Die Bibliothek befindet sich in der Nähe des Browsers 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 (erstellen eines Getters für einen HTMLInputElement).
  • Deklariert einen Klickereignis-Callback namens onButtonClick und hängt ihn an.
  • Fokussiert die Eingabe beim Klicken auf die Schaltfläche

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

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

Nachdem die Lit-Komponente die Vorlage der render-Methode dem Stamm von my-element per Commit übergeben hat, ermöglicht der @query-Decorator inputEl, das erste input-Element im Renderingstamm zurückzugeben. Es wird null zurückgegeben, wenn @query das angegebene Element nicht finden kann.

Wenn der Renderingstamm mehrere input-Elemente enthält, würde @queryAll eine Liste mit Knoten zurückgeben.

10. Vermittlungsstatus

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

Wiederverwendbare Komponenten

React ahmt funktionale Rendering-Pipelines mit einem Top-down-Datenfluss nach. Eltern stellen ihren Kindern mit Hilfsmitteln einen Zustand zur Verfügung. Kinder können über Rückrufe in Requisiten mit ihren Eltern kommunizieren.

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 eine Schaltfläche mit dem Label „+step“ oder „-step“
  • Aktualisiert die übergeordnete Komponente durch Aufrufen von props.addToCounter mit props.step als Argument beim Klick

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

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

Browserereignisse werden von untergeordneten Elementen zu übergeordneten Elementen übergehen. Mithilfe von Ereignissen können untergeordnete Publisher Interaktionsereignisse und Statusänderungen senden. React übergibt den Zustand grundsätzlich in die entgegengesetzte Richtung. Daher ist es ungewöhnlich, dass React-Komponenten Ereignisse auf dieselbe Weise auslösen und überwachen wie Lit-Komponenten.

Zustandsorientierte Komponenten

In React werden zum Verwalten des Status häufig Hooks verwendet. Eine MyCounter-Komponente kann durch Wiederverwenden 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 kann in Lit. 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 Eigenschaft namens count, die die Komponente aktualisiert, wenn sich der Wert ändert
  • Bindet den addToCounter-Callback an den @update-counter-Event-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. Es hat sich auch bewährt, das Ereignissystem des Browsers zu verwenden, um Details von unten nach oben auf einer Karte darzustellen.

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

Es gibt mehrere Möglichkeiten, Elemente zu gestalten, sowie eine integrierte Lösung.

Inline-Stile

Lit unterstützt Inline-Stile sowie Bindungen 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 und binden Sie einen Rahmen von border-color.js 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. Lit bietet daher eine entsprechende Anweisung.

styleMap

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

Im obigen Beispiel geschieht Folgendes:

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

Außerdem wird styleMap verwendet, um die Stile von h1 festzulegen. styleMap hat eine ähnliche Syntax wie die style-Attributbindungssyntax von React.

CSSResult

Die empfohlene Methode für den Stil von Komponenten ist die Verwendung des mit css getaggten Vorlagenliterals.

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 der Verwendung des Vorlagen-Tags css:

  • Einmal pro Klasse vs. pro Instanz geparst
  • Bei der Implementierung wurde die Wiederverwendbarkeit der Module berücksichtigt.
  • Können Stile einfach in eigene Dateien aufteilen
  • Kompatibel mit dem Polyfill für benutzerdefinierte CSS-Eigenschaften

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

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

Die Beleuchtet zeigt den Umfang Ihrer Komponenten an bis hin zu ihren Wurzeln. 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 Browser-Deduplizierung von CSSResult-Werten 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 & Virtuelles DOM

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

Hier ist 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 Stringarrays, die an die Tag-Funktion übergeben werden, 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). Die Bindung, das Parsen und das Vorlagen-Caching von Lit können diese Funktionen nutzen, ohne dass sich der Aufwand für die Laufzeit verändert.

Dieses integrierte Browserverhalten von getaggten Vorlagenliteralen verschafft Lit einen erheblichen Leistungsvorteil. Die meisten herkömmlichen virtuellen DOMs erledigen den Großteil ihrer Arbeit in JavaScript. Getaggte Vorlagenliterale haben jedoch die meisten Unterschiede in der C++ 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 & JSX + React DOM

JSX wird nicht im Browser ausgeführt, sondern nutzt einen Präprozessor, um JSX in JavaScript-Funktionsaufrufe zu konvertieren (normalerweise ü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. Das bedeutet, dass Sie für die ersten Schritte mit Lit nur eine HTML-Datei, ein ES-Modulskript und einen Server benötigen. Hier ist ein vollständig im Browser ausführbares Skript:

<!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 sowie beim Auslösen von Ereignissen weniger das Parsen der Laufzeit durchgeführt wird.

Komponenten und 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.

Benutzerdefinierte Elemente bieten als Komponentensystem einige Vorteile:

  • Nativ im Browser und ohne zusätzliche 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 „hydrate“ registriert werden DOM

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:Obwohl die Entwickler von Browserkomfort-Anbietern die selbstschließende Tag-Spezifikation bereuen, enthalten neuere Spezifikationen tendenziell 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

Lit hat benutzerdefinierte Elemente anstelle eines maßgeschneiderten Elementsystems verwendet, da die benutzerdefinierten Elemente in den Browser integriert sind, und das Lit-Team ist der Meinung, dass die Vorteile des plattformübergreifenden Frameworks die Vorteile einer Komponentenabstraktionsschicht überwiegen. Die Bemühungen des Lit-Teams im Lit-SSR-Bereich haben die Hauptprobleme bei der JavaScript-Registrierung überwunden. Darüber hinaus nutzen einige Unternehmen wie GitHub die Lazy-Registrierung für benutzerdefinierte Elemente, um Seiten schrittweise mit optionalem Flair zu verbessern.

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 – angesichts der folgenden LitElement-Definition:

// 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 werden Daten an dieses benutzerdefinierte Element übergeben:

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

Andere React-Lebenszyklus-Callbacks

static getDerivedStateFromProps

Es gibt kein Äquivalent in Lit, da Requisiten und Zustand beide die gleichen Klasseneigenschaften sind.

shouldComponentUpdate

  • Liquiditätsäquivalent ist shouldUpdate
  • Wird im Gegensatz zu React beim ersten Rendering aufgerufen
  • Ähnlich wie shouldComponentUpdate von React

getSnapshotBeforeUpdate

Im Feld „Lit“ entspricht getSnapshotBeforeUpdate sowohl update als auch willUpdate

willUpdate

  • Vor 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
  • Guter Ort zum Berechnen von Attributwerten, die von anderen Attributen abhängen und im weiteren Aktualisierungsprozess verwendet werden
  • Diese Methode wird auf dem Server in SSR aufgerufen, daher wird hier nicht empfohlen, auf das DOM zuzugreifen.

update

  • Nach willUpdate angerufen
  • Im Gegensatz zu getSnapshotBeforeUpdate wird update vor render aufgerufen
  • Änderungen an reaktiven Eigenschaften in update lösen den Aktualisierungszyklus nicht noch einmal aus, wenn sie vor dem Aufruf von super.update geändert 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. Sie sind:

attributeChangedCallback

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

Andere Lebenszyklusmethoden und -attribute

Diese Methoden und Attribute sind Klassenmitglieder, die Sie aufrufen, überschreiben oder warten können, um den Lebenszyklusprozess 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 deren Rendering-Zyklen synchron sein müssen. z.B.

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

performUpdate

Mit dieser Methode werden die Callbacks für den Aktualisierungslebenszyklus 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 Eigenschaft ist 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 Hauptdokumentstruktur angehängt ist.

Visualisierung des Lebenszyklus von Lit-Updates

Der Aktualisierungszyklus besteht aus drei Teilen:

  • Vor der Aktualisierung
  • Aktualisieren
  • Nach der Aktualisierung

Vorab-Update

Ein gerichteter azyklischer Graph von Knoten mit Callback-Namen. -Konstruktor für requestUpdate. @property zum Festlegen von Eigenschaften. „attributeChangedCallback“ in „Property Setter“. Property-Setter auf hasChanged. hasChanged zu requestUpdate geändert wurde. requestUpdate verweist auf die nächste Grafik zum Aktualisieren des Lebenszyklus.

Nach dem requestUpdate wird ein geplantes Update erwartet.

Aktualisieren

Ein gerichteter azyklischer Graph von Knoten mit Callback-Namen. Pfeil vom vorherigen Bild der Lebenszykluspunkte vor der Aktualisierung zu „PerformUpdate“. „PerformUpdate to shouldUpdate“. shouldUpdate weist sowohl auf &quot;complete update if false&quot; als auch auf willUpdate auf. wird durch „willUpdate“ aktualisiert. Aktualisierung sowohl auf das Rendering
als auch auf die nächste Grafik des Lebenszyklus nach der Aktualisierung. Das Rendern verweist auch auf die nächste
Lebenszyklusgrafik nach der Aktualisierung.

Nach Aktualisierung

Ein gerichteter azyklischer Graph von Knoten mit Callback-Namen. Pfeil von der vorherigen Abbildung der Aktualisierungslebenszyklus zu „firstUpdated“. firstUpdated to updated. aktualisiert 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 wesentlich einfacher und leichter lesbar als die entsprechenden 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
    • Rendert die Antwort
        von useAPI
      • Nutzer-ID + Nutzername
      • Fehlermeldung
        • 404, wenn Nutzer 11 erreicht wird (von Grund auf)
        • 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
      • Ob der Abruf geladen wird
      • 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 sind wie benutzerdefinierte Hooks
  • Übergabe nicht renderbarer Daten zwischen Callbacks und Effekten
    • React verwendet useRef, um Daten zwischen useEffect und useCallback zu übergeben
    • Lit verwendet eine private Klassen-Property
    • React ist im Wesentlichen das Nachahmen des Verhaltens einer privaten Klasseneigenschaft

Wenn Ihnen die Syntax der React-Funktionskomponente mit Hooks, aber derselben baulosen Umgebung wie Lit gefällt, empfiehlt das Lit-Team außerdem dringend die Haunted-Bibliothek.

Children

Standard-Slot

Wenn HTML-Elementen kein slot-Attribut zugewiesen ist, werden sie der unbenannten Standardfläche zugewiesen. Im folgenden Beispiel platziert MyApp einen Absatz in einer benannten Anzeigenfläche. Für den anderen Absatz wird standardmäßig die unbenannte Anzeigenfläche verwendet.

@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 Beispiel unten werden für den ersten Slot, der im shadowRoot gefunden wird, die assignedNodes 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>
   `;
  }
}

Verweise

Referenzerstellung

Sowohl Lit als auch React stellen einen Verweis auf ein HTMLElement bereit, nachdem ihre render-Funktionen aufgerufen wurden. 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 funktionsfähige Pipeline, die React-Komponenten und nicht HTMLElements 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 von Lit-HTML, um ein Vorlagenelement im Hintergrund zu erstellen. LitElement fügt nach dem Rendern den Inhalt der Vorlage 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-Decorator erstellt dann einen Getter für die Property, der im Grunde eine this.shadowRoot.querySelector für den Bereichsstamm ausführt.

Mehrere Elemente abfragen

Im folgenden Beispiel gibt der @queryAll-Decorator die beiden Absätze im Schattenstamm 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 Anweisung @queryAsync 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 Callbacks verwendet, da der Status von React selbst vermittelt wird. Es empfiehlt sich, sich nicht auf den Zustand der Elemente zu verlassen. Das DOM ist einfach eine Auswirkung des Rendering-Prozesses.

Externer Status

Es ist möglich, Redux, MobX oder jede andere State Management Library neben Lit.

Lit-Komponenten werden im Browserbereich erstellt. So ist jede Bibliothek, die auch im Browser-Bereich vorhanden ist, für Lit. 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 und erfahren Sie, wie eine große Website MobX in Lit nutzen kann.

Auf der Website Apollo Elements erfahren Sie außerdem, 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

Schatten-DOM

Um Stile und DOMs nativ in einem benutzerdefinierten Element zu kapseln, verwendet Lit Shadow DOM. Mit Schattenwurzeln wird eine Schattenstruktur erstellt, die vom Hauptdokumentbaum 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.

Shadow DOM führt außerdem neue Konzepte und Selektoren in die CSS-Spezifikation ein:

: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“ ist es einfach, Stile in Form von CSSTemplateResults über css-Vorlagen-Tags zwischen Komponenten zu 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

Schattenwurzeln stellen eine Herausforderung für konventionelle Themen dar, bei denen es sich in der Regel um Top-down-Tag-Ansätze handelt. Die herkömmliche Methode zur Behandlung des Designs mit Webkomponenten, die Shadow DOM verwenden, besteht darin, eine Stil-API über benutzerdefinierte CSS-Eigenschaften bereitzustellen. Dies ist zum Beispiel ein Muster, das Material Design verwendet:

.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 das Shadow DOM jederzeit deaktivieren. Überschreiben Sie dazu createRenderRoot, sodass this zurückgegeben wird, das dann die Komponenten rendert. an das benutzerdefinierte Element selbst anstatt an einen Schattenstamm an, der an das benutzerdefinierte Element angehängt ist. Dadurch gehen Sie verloren: Stilkapselung, DOM-Kapselung und Slots.

Produktion

IE 11

Falls ältere Browser wie IE 11 unterstützt werden müssen, müssen einige Polyfills geladen werden, deren Größe noch etwa 33 KB beträgt. Weitere Informationen finden Sie hier.

Bedingte Sets

Das Lit-Team empfiehlt die Bereitstellung von zwei verschiedenen Bundles: eines für IE 11 und eines für moderne Browser. Dies hat mehrere Vorteile:

  • Die Bereitstellung von ES 6 ist schneller und wird für die meisten Ihrer Kunden nützlich sein.
  • Transpiliertes ES 5 erhöht die Bundle-Größe erheblich
  • Bedingte Pakete bieten das Beste aus beiden Welten
    • Unterstützung für IE 11
    • Keine Verlangsamung bei modernen Browsern

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