Web Components erstellen, die sogar mit React funktionieren

Avatar of Adam Rackis
Adam Rackis am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Diejenigen von uns, die schon länger als ein paar Jahre Webentwickler sind, haben wahrscheinlich schon Code mit mehr als einem JavaScript-Framework geschrieben. Bei all den verfügbaren Optionen — React, Svelte, Vue, Angular, Solid — ist das fast unvermeidlich. Eines der frustrierendsten Dinge, mit denen wir beim Arbeiten über Frameworks hinweg umgehen müssen, ist das Neuerstellen all dieser Low-Level-UI-Komponenten: Buttons, Tabs, Dropdowns usw. Besonders frustrierend ist, dass wir sie normalerweise in einem Framework, z. B. React, definiert haben, sie aber neu schreiben müssen, wenn wir etwas in Svelte erstellen wollen. Oder Vue. Oder Solid. Und so weiter.

Wäre es nicht besser, wenn wir diese Low-Level-UI-Komponenten einmal, framework-agnostisch definieren und dann zwischen Frameworks wiederverwenden könnten? Natürlich wäre das besser! Und das können wir auch; Web Components sind der Weg. Dieser Beitrag zeigt Ihnen, wie.

Derzeit ist die SSR-Unterstützung für Web Components noch etwas lückenhaft. Declarative Shadow DOM (DSD) ist die Art und Weise, wie ein Web Component serverseitig gerendert wird, aber zum Zeitpunkt des Schreibens ist es nicht in Ihre bevorzugten Anwendungsframeworks wie Next, Remix oder SvelteKit integriert. Wenn dies eine Anforderung für Sie ist, informieren Sie sich unbedingt über den neuesten Status von DSD. Ansonsten, wenn Sie SSR nicht verwenden, lesen Sie weiter.

Zuerst etwas Kontext

Web Components sind im Wesentlichen HTML-Elemente, die Sie selbst definieren, wie <yummy-pizza> oder was auch immer, von Grund auf. Sie sind hier bei CSS-Tricks ausführlich behandelt (einschließlich einer ausführlichen Serie von Caleb Williams und einer von John Rhea), aber wir gehen den Prozess kurz durch. Im Wesentlichen definieren Sie eine JavaScript-Klasse, erben sie von HTMLElement und definieren dann beliebige Eigenschaften, Attribute und Stile, die das Web Component hat, und natürlich das Markup, das es letztendlich für Ihre Benutzer rendert.

Die Möglichkeit, benutzerdefinierte HTML-Elemente zu definieren, die an keine bestimmte Komponente gebunden sind, ist spannend. Aber diese Freiheit ist auch eine Einschränkung. Da sie unabhängig von jedem JavaScript-Framework existieren, können Sie nicht wirklich mit diesen JavaScript-Frameworks interagieren. Denken Sie an eine React-Komponente, die einige Daten abruft und dann eine *andere* React-Komponente rendert und die Daten weitergibt. Das würde als Web Component nicht wirklich funktionieren, da ein Web Component nicht weiß, wie es eine React-Komponente rendern soll.

Web Components eignen sich besonders gut als Leaf Components. Leaf Components sind die letzten Elemente, die in einem Komponentenbaum gerendert werden. Dies sind die Komponenten, die einige Props erhalten und eine UI rendern. Dies sind *nicht* die Komponenten, die sich in der Mitte Ihres Komponentenbaums befinden, Daten weitergeben, Kontext festlegen usw. — nur reine UI-Teile, die gleich aussehen, unabhängig davon, welches JavaScript-Framework den Rest der App antreibt.

Das Web Component, das wir erstellen

Anstatt etwas Langweiliges (und Übliches) wie einen Button zu erstellen, erstellen wir etwas etwas anderes. In meinem letzten Beitrag haben wir uns angesehen, wie man unscharfe Bild-Vorschauen verwendet, um Content-Reflow zu verhindern und eine gute Benutzeroberfläche für Benutzer bereitzustellen, während unsere Bilder geladen werden. Wir haben uns angesehen, wie man unscharfe, degradierte Versionen unserer Bilder als Base64 kodiert und diese in unserer Benutzeroberfläche anzeigt, während das echte Bild geladen wird. Wir haben uns auch angesehen, wie man unglaublich kompakte, unscharfe Vorschauen mit einem Werkzeug namens Blurhash generiert.

Dieser Beitrag zeigte Ihnen, wie Sie diese Vorschauen generieren und in einem React-Projekt verwenden. Dieser Beitrag zeigt Ihnen, wie Sie diese Vorschauen aus einem Web Component verwenden, damit sie von *jedem* JavaScript-Framework genutzt werden können.

Aber wir müssen erst laufen lernen, also gehen wir zuerst etwas Triviales und Albernes durch, um genau zu sehen, wie Web Components funktionieren.

Alles in diesem Beitrag wird aus Vanilla-Web-Components ohne jegliches Tooling erstellt. Das bedeutet, der Code hat ein bisschen Boilerplate, sollte aber relativ leicht zu verstehen sein. Tools wie Lit oder Stencil sind für die Erstellung von Web Components konzipiert und können verwendet werden, um viel von diesem Boilerplate zu entfernen. Ich rate Ihnen dringend, sich diese anzusehen! Aber für diesen Beitrag ziehe ich ein wenig mehr Boilerplate vor, im Austausch dafür, dass ich keine weitere Abhängigkeit einführen und lehren muss.

Eine einfache Zähler-Komponente

Erstellen wir den klassischen „Hallo Welt“-Klassiker für JavaScript-Komponenten: einen Zähler. Wir rendern einen Wert und einen Button, der diesen Wert inkrementiert. Einfach und langweilig, aber es erlaubt uns, uns die einfachste mögliche Web Component anzusehen.

Um eine Web Component zu erstellen, ist der erste Schritt, eine JavaScript-Klasse zu erstellen, die von HTMLElement erbt

class Counter extends HTMLElement {}

Der letzte Schritt ist die Registrierung der Web Component, aber nur, wenn wir sie noch nicht registriert haben

if (!customElements.get("counter-wc")) {
  customElements.define("counter-wc", Counter);
}

Und natürlich rendern

<counter-wc></counter-wc>

Und alles dazwischen ist, wie wir die Web Component dazu bringen, das zu tun, was wir wollen. Eine gängige Lifecycle-Methode ist connectedCallback, die ausgelöst wird, wenn unsere Web Component dem DOM hinzugefügt wird. Wir könnten diese Methode verwenden, um beliebige Inhalte zu rendern. Denken Sie daran, dass dies eine JS-Klasse ist, die von HTMLElement erbt, was bedeutet, dass unser this-Wert das Web Component-Element selbst ist, mit all den normalen DOM-Manipulationsmethoden, die Sie bereits kennen und lieben.

Im einfachsten Fall könnten wir das tun

class Counter extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<div style='color: green'>Hey</div>";
  }
}

if (!customElements.get("counter-wc")) {
  customElements.define("counter-wc", Counter);
}

...was auch funktioniert.

The word "hey" in green.

Echten Inhalt hinzufügen

Fügen wir einige nützliche, interaktive Inhalte hinzu. Wir benötigen ein <span>, um den aktuellen Zahlenwert anzuzeigen, und einen <button>, um den Zähler zu inkrementieren. Vorerst erstellen wir diesen Inhalt in unserem Konstruktor und fügen ihn hinzu, wenn die Web Component tatsächlich im DOM ist.

constructor() {
  super();
  const container = document.createElement('div');

  this.valSpan = document.createElement('span');

  const increment = document.createElement('button');
  increment.innerText = 'Increment';
  increment.addEventListener('click', () => {
    this.#value = this.#currentValue + 1;
  });

  container.appendChild(this.valSpan);
  container.appendChild(document.createElement('br'));
  container.appendChild(increment);

  this.container = container;
}

connectedCallback() {
  this.appendChild(this.container);
  this.update();
}

Wenn Sie sich ekeln vor der manuellen DOM-Erstellung, denken Sie daran, dass Sie innerHTML setzen können, oder sogar ein Template-Element einmal als statische Eigenschaft Ihrer Web Component-Klasse erstellen, es klonen und die Inhalte für neue Web Component-Instanzen einfügen. Es gibt wahrscheinlich noch andere Optionen, an die ich nicht denke, oder Sie können immer ein Web Component-Framework wie Lit oder Stencil verwenden. Aber für diesen Beitrag halten wir es einfach.

Weiter geht's, wir brauchen eine setzbare JavaScript-Klassen-Eigenschaft namens value

#currentValue = 0;

set #value(val) {
  this.#currentValue = val;
  this.update();
}

Es ist nur eine normale Klassen-Eigenschaft mit einem Setter, zusammen mit einer zweiten Eigenschaft zur Speicherung des Wertes. Ein lustiger Kniff ist, dass ich die private JavaScript-Klassen-Eigenschaftssyntax für diese Werte verwende. Das bedeutet, niemand außerhalb unserer Web Component kann diese Werte jemals anfassen. Das ist Standard-JavaScript das in allen modernen Browsern unterstützt wird, also scheuen Sie sich nicht, es zu verwenden.

Oder nennen Sie es gerne _value, wenn Sie möchten. Und schließlich unsere update-Methode

update() {
  this.valSpan.innerText = this.#currentValue;
}

Es funktioniert!

The counter web component.

Offensichtlich ist das kein Code, den Sie in großem Maßstab pflegen möchten. Hier ist ein vollständiges funktionierendes Beispiel, wenn Sie es genauer betrachten möchten. Wie ich bereits sagte, Tools wie Lit und Stencil sind dafür konzipiert, dies zu vereinfachen.

Einige weitere Funktionalitäten hinzufügen

Dieser Beitrag ist keine Tiefenbohrung in Web Components. Wir werden nicht alle APIs und Lifecycles behandeln; wir werden nicht einmal Shadow Roots oder Slots behandeln. Es gibt endlose Inhalte zu diesen Themen. Mein Ziel ist es hier, eine ausreichend gute Einführung zu geben, um Interesse zu wecken, zusammen mit einigen nützlichen Anleitungen zur tatsächlichen *Verwendung* von Web Components mit den beliebten JavaScript-Frameworks, die Sie bereits kennen und lieben.

Zu diesem Zweck werden wir unsere Zähler-Web-Component etwas erweitern. Wir lassen sie ein color-Attribut akzeptieren, um die Farbe des angezeigten Wertes zu steuern. Und wir lassen sie auch eine increment-Eigenschaft akzeptieren, damit Verbraucher dieser Web Component sie in 2er-, 3er-, 4er-Schritten inkrementieren können. Und um diese Zustandsänderungen anzutreiben, verwenden wir unseren neuen Zähler in einer Svelte-Sandbox — wir werden uns in Kürze mit React befassen.

Wir beginnen mit der gleichen Web Component wie zuvor und fügen ein Farb-Attribut hinzu. Um unsere Web Component zu konfigurieren, damit sie Attribute akzeptiert und darauf reagiert, fügen wir eine statische Eigenschaft observedAttributes hinzu, die die Attribute zurückgibt, auf die unsere Web Component hört.

static observedAttributes = ["color"];

Mit diesem Platzhalter können wir eine Lifecycle-Methode attributeChangedCallback hinzufügen, die ausgeführt wird, wenn eines der in observedAttributes aufgeführten Attribute gesetzt oder aktualisiert wird.

attributeChangedCallback(name, oldValue, newValue) {
  if (name === "color") {
    this.update();
  }
}

Jetzt aktualisieren wir unsere update-Methode, um sie tatsächlich zu verwenden

update() {
  this.valSpan.innerText = this._currentValue;
  this.valSpan.style.color = this.getAttribute("color") || "black";
}

Zuletzt fügen wir unsere increment-Eigenschaft hinzu

increment = 1;

Einfach und bescheiden.

Die Zähler-Komponente in Svelte verwenden

Lassen Sie uns verwenden, was wir gerade gemacht haben. Wir gehen in unsere Svelte-App-Komponente und fügen etwas wie das hier hinzu

<script>
  let color = "red";
</script>

<style>
  main {
    text-align: center;
  }
</style>

<main>
  <select bind:value={color}>
    <option value="red">Red</option>
    <option value="green">Green</option>
    <option value="blue">Blue</option>
  </select>

  <counter-wc color={color}></counter-wc>
</main>

Und es funktioniert! Unser Zähler wird gerendert, inkrementiert, und das Dropdown aktualisiert die Farbe. Wie Sie sehen können, rendern wir das Farb-Attribut in unserer Svelte-Vorlage und wenn sich der Wert ändert, übernimmt Svelte die Arbeit, setAttribute auf unserer zugrunde liegenden Web Component-Instanz aufzurufen. Hier ist nichts Besonderes: Das ist dasselbe, was es bereits für die Attribute *jeder* HTML-Elemente tut.

Bei der increment-Prop wird es ein wenig interessant. Das ist *kein* Attribut auf unserer Web Component; es ist eine Prop auf der Klasse der Web Component. Das bedeutet, sie muss auf der Instanz der Web Component gesetzt werden. Haben Sie etwas Geduld, denn die Dinge werden bald viel einfacher werden.

Zuerst fügen wir einige Variablen zu unserer Svelte-Komponente hinzu

let increment = 1;
let wcInstance;

Unsere leistungsstarke Zähler-Komponente lässt Sie um 1 oder um 2 inkrementieren

<button on:click={() => increment = 1}>Increment 1</button>
<button on:click={() => increment = 2}>Increment 2</button>

Aber, *theoretisch* müssen wir die tatsächliche Instanz unserer Web Component erhalten. Das ist dasselbe, was wir immer tun, wenn wir einen ref mit React hinzufügen. Mit Svelte ist es eine einfache bind:this-Direktive

<counter-wc bind:this={wcInstance} color={color}></counter-wc>

Jetzt lauschen wir in unserer Svelte-Vorlage auf Änderungen an der Increment-Variable unserer Komponente und setzen die zugrunde liegende Web Component-Eigenschaft.

$: {
  if (wcInstance) {
    wcInstance.increment = increment;
  }
}

Sie können es in dieser Live-Demo ausprobieren.

Wir wollen das offensichtlich nicht für jede Web Component oder Prop tun, die wir verwalten müssen. Wäre es nicht schön, wenn wir increment einfach direkt auf unserer Web Component im Markup setzen könnten, wie wir es normalerweise für Komponenten-Props tun, und es, wissen Sie, *einfach funktionieren* würde? Mit anderen Worten, es wäre schön, wenn wir alle Verwendungen von wcInstance löschen und diesen einfacheren Code stattdessen verwenden könnten

<counter-wc increment={increment} color={color}></counter-wc>

Es stellt sich heraus, dass wir das können. Dieser Code funktioniert; Svelte übernimmt die gesamte Arbeit für uns. Schauen Sie es sich in dieser Demo an. Das ist das Standardverhalten für so ziemlich alle JavaScript-Frameworks.

Warum habe ich Ihnen dann den manuellen Weg gezeigt, die Prop der Web Component zu setzen? Zwei Gründe: Es ist nützlich zu verstehen, wie diese Dinge funktionieren, und vor einem Moment habe ich gesagt, dass dies für "so ziemlich" alle JavaScript-Frameworks funktioniert. Aber es gibt ein Framework, das, maddeningly, das Setzen von Web Component-Props nicht unterstützt, wie wir es gerade gesehen haben.

React ist ein anderes Biest

React. Das beliebteste JavaScript-Framework der Welt unterstützt keine grundlegende Interoperabilität mit Web Components. Dies ist ein bekanntes Problem, das nur bei React auftritt. Interessanterweise ist dies in Reacts experimentellem Branch behoben, wurde aber aus irgendeinem Grund nicht in Version 18 übernommen. Dennoch können wir den Fortschritt verfolgen. Und Sie können dies selbst mit einer Live-Demo ausprobieren.

Die Lösung ist natürlich die Verwendung eines ref, das Abrufen der Web Component-Instanz und das manuelle Setzen von increment, wenn sich dieser Wert ändert. Es sieht so aus

import React, { useState, useRef, useEffect } from 'react';
import './counter-wc';

export default function App() {
  const [increment, setIncrement] = useState(1);
  const [color, setColor] = useState('red');
  const wcRef = useRef(null);

  useEffect(() => {
    wcRef.current.increment = increment;
  }, [increment]);

  return (
    <div>
      <div className="increment-container">
        <button onClick={() => setIncrement(1)}>Increment by 1</button>
        <button onClick={() => setIncrement(2)}>Increment by 2</button>
      </div>

      <select value={color} onChange={(e) => setColor(e.target.value)}>
        <option value="red">Red</option>
        <option value="green">Green</option>
        <option value="blue">Blue</option>
      </select>

      <counter-wc ref={wcRef} increment={increment} color={color}></counter-wc>
    </div>
  );
}

Wie wir besprochen haben, ist die manuelle Programmierung für jede Web Component-Eigenschaft einfach nicht skalierbar. Aber nicht alles ist verloren, denn wir haben ein paar Optionen.

Option 1: Überall Attribute verwenden

Wir haben Attribute. Wenn Sie die obige React-Demo angeklickt haben, hat die increment-Prop nicht funktioniert, aber die Farbe hat sich korrekt geändert. Können wir nicht alles mit Attributen programmieren? Leider nein. Attributwerte können nur Strings sein. Das ist hier gut genug, und wir könnten mit diesem Ansatz noch einiges erreichen. Zahlen wie increment können in Strings umgewandelt und aus Strings konvertiert werden. Wir könnten sogar Objekte JSON-stringifizieren/parsen. Aber irgendwann müssen wir eine Funktion in eine Web Component übergeben, und dann wären wir ohne Optionen.

Option 2: Einpacken

Es gibt ein altes Sprichwort, dass man jedes Problem in der Informatik mit einer zusätzlichen Indirektionsebene lösen kann (außer dem Problem zu vieler Indirektionsebenen). Der Code zum Setzen dieser Props ist ziemlich vorhersehbar und einfach. Was wäre, wenn wir ihn in einer Bibliothek verstecken würden? Die schlauen Leute hinter Lit haben eine Lösung. Diese Bibliothek erstellt eine neue React-Komponente für Sie, nachdem Sie ihr eine Web Component übergeben und die benötigten Eigenschaften aufgelistet haben. Obwohl clever, bin ich kein Fan dieses Ansatzes.

Anstatt einer Eins-zu-Eins-Zuordnung von Web Components zu manuell erstellten React-Komponenten, bevorzuge ich *eine* React-Komponente, der wir unseren Web Component- *Tag-Namen* übergeben (counter-wc in unserem Fall) — zusammen mit allen Attributen und Eigenschaften — und diese Komponente rendert unsere Web Component, fügt den ref hinzu und ermittelt dann, was eine Prop und was ein Attribut ist. Das ist meiner Meinung nach die ideale Lösung. Ich kenne keine Bibliothek, die das tut, aber es sollte relativ einfach zu erstellen sein. Versuchen wir es mal!

Das ist die *Verwendung*, nach der wir suchen

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

wcTag ist der Web Component-Tag-Name; der Rest sind die Eigenschaften und Attribute, die wir weitergeben wollen.

So sieht meine Implementierung aus

import React, { createElement, useRef, useLayoutEffect, memo } from 'react';

const _WcWrapper = (props) => {
  const { wcTag, children, ...restProps } = props;
  const wcRef = useRef(null);

  useLayoutEffect(() => {
    const wc = wcRef.current;

    for (const [key, value] of Object.entries(restProps)) {
      if (key in wc) {
        if (wc[key] !== value) {
          wc[key] = value;
        }
      } else {
        if (wc.getAttribute(key) !== value) {
          wc.setAttribute(key, value);
        }
      }
    }
  });

  return createElement(wcTag, { ref: wcRef });
};

export const WcWrapper = memo(_WcWrapper);

Die interessanteste Zeile steht am Ende

return createElement(wcTag, { ref: wcRef });

So erstellen wir ein Element in React mit einem dynamischen Namen. Tatsächlich ist es das, was React normalerweise aus JSX kompiliert. Alle unsere Divs werden in createElement("div")-Aufrufe umgewandelt. Wir müssen diese API normalerweise nicht direkt aufrufen, aber sie ist da, wenn wir sie brauchen.

Darüber hinaus möchten wir einen Layout-Effekt ausführen und jede Prop durchlaufen, die wir unserer Komponente übergeben haben. Wir durchlaufen alle und prüfen, ob es sich um eine Eigenschaft handelt, indem wir mit in prüfen, die sowohl das Web Component-Instanzobjekt als auch seine Prototypenkette überprüft, was alle Getter/Setter erfasst, die auf dem Klassenprototyp landen. Wenn keine solche Eigenschaft existiert, wird angenommen, dass es sich um ein Attribut handelt. In beiden Fällen wird sie nur gesetzt, wenn sich der Wert tatsächlich geändert hat.

Wenn Sie sich fragen, warum wir useLayoutEffect anstelle von useEffect verwenden, liegt das daran, dass wir diese Aktualisierungen sofort ausführen wollen, bevor unser Inhalt gerendert wird. Außerdem beachten Sie, dass wir für unser useLayoutEffect kein Abhängigkeitsarray haben; das bedeutet, wir wollen diese Aktualisierung bei *jedem Rendern* ausführen. Das kann riskant sein, da React dazu neigt, *viel* neu zu rendern. Ich mildere dies ab, indem ich das Ganze in React.memo packe. Das ist im Wesentlichen die moderne Version von React.PureComponent, was bedeutet, dass die Komponente nur dann neu gerendert wird, wenn sich eine ihrer tatsächlichen Props geändert hat — und sie prüft dies über einen einfachen Gleichheitsvergleich.

Das einzige Risiko besteht darin, dass Sie, wenn Sie eine Objekt-Prop übergeben, die Sie direkt mutieren, ohne sie neu zuzuweisen, die Aktualisierungen nicht sehen werden. Aber das ist sehr abgeraten, besonders in der React-Community, daher würde ich mir darüber keine Sorgen machen.

Bevor wir fortfahren, möchte ich noch eine letzte Sache hervorheben. Möglicherweise sind Sie mit der Art und Weise, wie die Verwendung aussieht, nicht zufrieden. Wiederum wird diese Komponente so verwendet

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

Insbesondere gefällt Ihnen möglicherweise nicht, den Web Component-Tag-Namen an die <WcWrapper>-Komponente zu übergeben, und Sie bevorzugen stattdessen das oben genannte @lit-labs/react-Paket, das für jede Web Component eine neue individuelle React-Komponente erstellt. Das ist völlig in Ordnung und ich ermutige Sie, das zu verwenden, womit Sie sich am wohlsten fühlen. Aber für mich ist ein Vorteil dieses Ansatzes, dass er leicht zu *löschen* ist. Wenn morgen aus irgendeinem Wunder React die korrekte Web Component-Handhabung aus seinem experimentellen Branch in main integriert, könnten Sie den obigen Code von diesem ändern

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

...zu diesem:

<counter-wc ref={wcRef} increment={increment} color={color} />

Sie könnten wahrscheinlich sogar eine einzige Codemod dafür überall schreiben und dann <WcWrapper> ganz löschen. Eigentlich, streichen Sie das: Eine globale Suche und Ersetzung mit einem RegEx würde wahrscheinlich funktionieren.

Die Implementierung

Ich weiß, es scheint, als hätte es eine Reise gedauert, bis wir hier angekommen sind. Wenn Sie sich erinnern, war unser ursprüngliches Ziel, den Bild-Vorschau-Code, den wir in meinem letzten Beitrag betrachtet haben, in eine Web Component zu verschieben, damit sie in jedem JavaScript-Framework verwendet werden kann. Reacts Mangel an echter Interoperabilität hat viele Details in das Ganze gebracht. Aber jetzt, da wir eine gute Vorstellung davon haben, wie man eine Web Component erstellt und verwendet, wird die Implementierung fast antiklimaktisch sein.

Ich lasse die gesamte Web Component hier fallen und hebe einige der interessanten Bits hervor. Wenn Sie sie in Aktion sehen möchten, hier ist eine funktionierende Demo. Sie wechselt zwischen meinen drei Lieblingsbüchern in meinen drei Lieblingsprogrammiersprachen. Die URL für jedes Buch ist jedes Mal eindeutig, sodass Sie die Vorschau sehen können, obwohl Sie wahrscheinlich die Dinge in Ihrem DevTools-Netzwerktab drosseln möchten, um wirklich zu sehen, was vor sich geht.

Gesamten Code anzeigen
class BookCover extends HTMLElement {
  static observedAttributes = ['url'];

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'url') {
      this.createMainImage(newValue);
    }
  }

  set preview(val) {
    this.previewEl = this.createPreview(val);
    this.render();
  }

  createPreview(val) {
    if (typeof val === 'string') {
      return base64Preview(val);
    } else {
      return blurHashPreview(val);
    }
  }

  createMainImage(url) {
    this.loaded = false;
    const img = document.createElement('img');
    img.alt = 'Book cover';
    img.addEventListener('load', () =&gt; {
      if (img === this.imageEl) {
        this.loaded = true;
        this.render();
      }
    });
    img.src = url;
    this.imageEl = img;
  }

  connectedCallback() {
    this.render();
  }

  render() {
    const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
    syncSingleChild(this, elementMaybe);
  }
}

Zuerst registrieren wir das Attribut, an dem wir interessiert sind, und reagieren, wenn es sich ändert

static observedAttributes = ['url'];

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'url') {
    this.createMainImage(newValue);
  }
}

Dies bewirkt die Erstellung unserer Bildkomponente, die erst angezeigt wird, wenn sie geladen ist

createMainImage(url) {
  this.loaded = false;
  const img = document.createElement('img');
  img.alt = 'Book cover';
  img.addEventListener('load', () => {
    if (img === this.imageEl) {
      this.loaded = true;
      this.render();
    }
  });
  img.src = url;
  this.imageEl = img;
}

Als nächstes haben wir unsere Vorschau-Eigenschaft, die entweder unser Base64-Vorschau-String oder unser blurhash-Paket sein kann

set preview(val) {
  this.previewEl = this.createPreview(val);
  this.render();
}

createPreview(val) {
  if (typeof val === 'string') {
    return base64Preview(val);
  } else {
    return blurHashPreview(val);
  }
}

Dies leitet an die jeweilige Hilfsfunktion weiter, die wir benötigen

function base64Preview(val) {
  const img = document.createElement('img');
  img.src = val;
  return img;
}

function blurHashPreview(preview) {
  const canvasEl = document.createElement('canvas');
  const { w: width, h: height } = preview;

  canvasEl.width = width;
  canvasEl.height = height;

  const pixels = decode(preview.blurhash, width, height);
  const ctx = canvasEl.getContext('2d');
  const imageData = ctx.createImageData(width, height);
  imageData.data.set(pixels);
  ctx.putImageData(imageData, 0, 0);

  return canvasEl;
}

Und zuletzt unsere render-Methode

connectedCallback() {
  this.render();
}

render() {
  const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
  syncSingleChild(this, elementMaybe);
}

Und einige Hilfsmethoden, um alles zusammenzufügen

export function syncSingleChild(container, child) {
  const currentChild = container.firstElementChild;
  if (currentChild !== child) {
    clearContainer(container);
    if (child) {
      container.appendChild(child);
    }
  }
}

export function clearContainer(el) {
  let child;

  while ((child = el.firstElementChild)) {
    el.removeChild(child);
  }
}

Es ist etwas mehr Boilerplate, als wir bräuchten, wenn wir dies in einem Framework erstellen würden, aber der Vorteil ist, dass wir es in jedem gewünschten Framework wiederverwenden können — obwohl React vorerst einen Wrapper benötigt, wie wir besprochen haben.

Verschiedenes

Ich habe bereits Lit's React-Wrapper erwähnt. Aber wenn Sie Stencil verwenden, unterstützt es tatsächlich eine separate Output-Pipeline speziell für React. Und die guten Leute bei Microsoft haben etwas Ähnliches wie Lit's Wrapper erstellt, das an die Fast Web Component Library angehängt ist.

Wie ich erwähnt habe, werden alle Frameworks außer React das Setzen von Web Component-Eigenschaften für Sie handhaben. Beachten Sie nur, dass einige spezielle Syntaxformen haben. Zum Beispiel nimmt bei Solid.js <your-wc value={12}> immer an, dass value eine Eigenschaft ist, die Sie mit einem attr-Präfix überschreiben können, wie <your-wc attr:value={12}>.

Zusammenfassung

Web Components sind ein interessanter, oft unterschätzter Teil der Webentwicklungslandschaft. Sie können helfen, Ihre Abhängigkeit von einem einzelnen JavaScript-Framework zu reduzieren, indem Sie Ihre UI- oder „Leaf“-Komponenten verwalten. Während die Erstellung dieser als Web Components — im Gegensatz zu Svelte- oder React-Komponenten — nicht so ergonomisch ist, ist der Vorteil, dass sie breit wiederverwendbar sind.