Ein Leitfaden zu Custom Elements für React-Entwickler

Avatar of Charles Peters
Charles Peters am

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

Ich musste kürzlich eine Benutzeroberfläche erstellen und (zum ersten Mal seit langem) hatte ich nicht die Option, React.js zu verwenden, was heutzutage meine bevorzugte Lösung für Benutzeroberflächen ist. Also habe ich mir angesehen, was die integrierten Browser-APIs zu bieten hatten, und festgestellt, dass die Verwendung von Custom Elements (auch bekannt als Web Components) genau das Richtige für diesen React-Entwickler sein könnte.

Custom Elements können die gleichen allgemeinen Vorteile wie React-Komponenten bieten, ohne an eine spezifische Framework-Implementierung gebunden zu sein. Ein Custom Element gibt uns einen neuen HTML-Tag, den wir programmatisch über eine native Browser-API steuern können.

Lassen Sie uns über die Vorteile einer komponentenbasierten Benutzeroberfläche sprechen

  • Kapselung – Belange, die auf diese Komponente beschränkt sind, bleiben in der Implementierung dieser Komponente
  • Wiederverwendbarkeit – Wenn die Benutzeroberfläche in allgemeinere Teile aufgeteilt ist, lassen sie sich leichter in Muster zerlegen, die Sie wahrscheinlich wiederverwenden werden
  • Isolation – Da Komponenten so konzipiert sind, dass sie gekapselt sind, und damit erhalten Sie den zusätzlichen Vorteil der Isolation, der es Ihnen ermöglicht, Fehler und Änderungen einfacher auf einen bestimmten Teil Ihrer Anwendung zu beschränken

Anwendungsfälle

Sie fragen sich vielleicht, wer Custom Elements in der Produktion verwendet. Namentlich

  • GitHub verwendet Custom Elements für seine Modal-Dialoge, Autovervollständigung und Anzeigezeiten.
  • Die neue Web-App von YouTube ist mit Polymer und Web Components erstellt.

Ähnlichkeiten zur Component API

Beim Vergleich von React-Komponenten mit Custom Elements fand ich die APIs wirklich ähnlich

  • Es sind beides Klassen, die nicht „neu“ sind und eine Basisklasse erweitern können
  • Sie erben beide einen Mount- oder Rendering-Lebenszyklus
  • Sie nehmen beide statische oder dynamische Eingaben über Props oder Attribute entgegen

Demo

Lassen Sie uns also eine winzige Anwendung erstellen, die Details zu einem GitHub-Repository auflistet.

Screenshot of end result

Wenn ich dies mit React angehen würde, würde ich eine einfache Komponente wie diese definieren

<Repository name="charliewilco/obsidian" />

Diese Komponente nimmt eine einzelne Prop entgegen – den Namen des Repositorys – und wir implementieren sie so

class Repository extends React.Component {
  state = {
    repo: null
  };

  async getDetails(name) {
    return await fetch(`https://api.github.com/repos/${name}`, {
      mode: 'cors'
    }).then(res => res.json());
  }

  async componentDidMount() {
    const { name } = this.props;
    const repo = await this.getDetails(name);
    this.setState({ repo });
  }

  render() {
    const { repo } = this.state;

    if (!repo) {
      return <h1>Loading</h1>;
    }

    if (repo.message) {
      return <div className="Card Card--error">Error: {repo.message}</div>;
    }

    return (
      <div class="Card">
        <aside>
          <img
            width="48"
            height="48"
            class="Avatar"
            src={repo.owner.avatar_url}
            alt="Profile picture for ${repo.owner.login}"
          />
        </aside>
        <header>
          <h2 class="Card__title">{repo.full_name}</h2>
          <span class="Card__meta">{repo.description}</span>
        </header>
      </div>
    );
  }
}

Sehen Sie den Pen React Demo – GitHub von Charles (@charliewilco) auf CodePen.

Um dies weiter aufzuschlüsseln: Wir haben eine Komponente, die ihren eigenen Zustand hat, nämlich die Repo-Details. Anfangs setzen wir ihn auf null, da wir noch keine Daten haben, daher wird eine Ladeanzeige angezeigt, während die Daten abgerufen werden.

Während des React-Lebenszyklus verwenden wir fetch, um die Daten von GitHub abzurufen, die Karte einzurichten und nach Erhalt der Daten eine erneute Rendern mit setState() auszulösen. All diese verschiedenen Zustände, die die Benutzeroberfläche annimmt, werden in der render()-Methode dargestellt.

Definieren / Verwenden eines Custom Elements

Dies mit Custom Elements zu tun ist etwas anders. Wie die React-Komponente nimmt unser Custom Element ein einzelnes Attribut entgegen – wieder den Namen des Repositorys – und verwaltet seinen eigenen Zustand.

Unser Element wird so aussehen

<github-repo name="charliewilco/obsidian"></github-repo>
<github-repo name="charliewilco/level.css"></github-repo>
<github-repo name="charliewilco/react-branches"></github-repo>
<github-repo name="charliewilco/react-gluejar"></github-repo>
<github-repo name="charliewilco/dotfiles"></github-repo>

Sehen Sie den Pen Custom Elements Demo – GitHub von Charles (@charliewilco) auf CodePen.

Zunächst müssen wir nur eine Klasse erstellen, die von der HTMLElement-Klasse erbt, und dann den Namen des Elements mit customElements.define() registrieren, um ein Custom Element zu definieren und zu registrieren.

class OurCustomElement extends HTMLElement {}
window.customElements.define('our-element', OurCustomElement);

Und wir können es aufrufen

<our-element></our-element>

Dieses neue Element ist nicht sehr nützlich, aber mit Custom Elements erhalten wir drei Methoden, um die Funktionalität dieses Elements zu erweitern. Diese sind fast analog zu den Lifecycle-Methoden von React für ihre Component API. Die beiden wichtigsten Lifecycle-ähnlichen Methoden für uns sind disconnectedCallBack und connectedCallback, und da es sich um eine Klasse handelt, verfügt sie über einen Konstruktor.

Name Aufgerufen, wenn
Konstruktor Eine Instanz des Elements erstellt oder aktualisiert wird. Nützlich für die Initialisierung von Zuständen, die Einrichtung von Event-Listenern oder die Erstellung von Shadow DOM. Beachten Sie die Einschränkungen für das, was Sie im constructor tun können, in der Spezifikation.
connectedCallback Das Element wird in das DOM eingefügt. Nützlich für die Ausführung von Setup-Code, wie z. B. das Abrufen von Ressourcen oder das Rendern der Benutzeroberfläche. Im Allgemeinen sollten Sie versuchen, die Arbeit bis zu diesem Zeitpunkt zu verzögern
disconnectedCallback Wenn das Element aus dem DOM entfernt wird. Nützlich für die Ausführung von Cleanup-Code.

Um unser Custom Element zu implementieren, erstellen wir die Klasse und richten einige Attribute ein, die mit dieser Benutzeroberfläche zusammenhängen

class Repository extends HTMLElement {
  constructor() {
    super();

    this.repoDetails = null;

    this.name = this.getAttribute("name");
    this.endpoint = `https://api.github.com/repos/${this.name}`    
    this.innerHTML = `<h1>Loading</h1>`
  }
}

Durch das Aufrufen von super() in unserem Konstruktor ist der Kontext von this das Element selbst, und alle DOM-Manipulations-APIs können verwendet werden. Bisher haben wir die Standard-Repository-Details auf null gesetzt, den Repository-Namen aus dem Attribut des Elements abgerufen, einen Endpunkt zum Aufrufen erstellt, damit wir ihn nicht später definieren müssen, und, am wichtigsten, das initiale HTML auf eine Ladeanzeige gesetzt.

Um die Details des Repositorys dieses Elements zu erhalten, müssen wir eine Anfrage an die GitHub-API stellen. Wir verwenden fetch und, da dies Promise-basiert ist, verwenden wir async und await, um unseren Code lesbarer zu machen. Sie können mehr über die Schlüsselwörter async/await hier und mehr über die Fetch-API des Browsers hier erfahren. Sie können mir auch twittern, um herauszufinden, ob ich sie der Axios-Bibliothek vorziehe. (Hinweis: Das hängt davon ab, ob ich Tee oder Kaffee zum Frühstück hatte.)

Fügen wir nun dieser Klasse eine Methode hinzu, um GitHub nach Details zum Repository zu fragen.

class Repository extends HTMLElement {
  constructor() {
    // ...
  }

  async getDetails() {
    return await fetch(this.endpoint, { mode: "cors" }).then(res => res.json());
  }
}

Als Nächstes verwenden wir die Methode connectedCallback und den Shadow DOM, um den Rückgabewert dieser Methode zu nutzen. Die Verwendung dieser Methode entspricht dem Aufruf von Repository.componentDidMount() im React-Beispiel. Stattdessen überschreiben wir den Wert null, den wir this.repoDetails anfänglich gegeben haben – wir werden ihn später verwenden, wenn wir beginnen, die Vorlage aufzurufen, um das HTML zu erstellen.

class Repository extends HTMLElement {
  constructor() {
    // ...
  }

  async getDetails() {
    // ...
  }

  async connectedCallback() {
    let repo = await this.getDetails();
    this.repoDetails = repo;
    this.initShadowDOM();
  }

  initShadowDOM() {
    let shadowRoot = this.attachShadow({ mode: "open" });
    shadowRoot.innerHTML = this.template;
  }
}

Sie werden feststellen, dass wir Methoden aufrufen, die sich auf den Shadow DOM beziehen. Abgesehen davon, dass es ein abgelehnter Titel für einen Marvel-Film ist, hat der Shadow DOM eine eigene reiche API, die sich lohnt anzusehen. Für unsere Zwecke wird er jedoch die Implementierung des Hinzufügens von innerHTML zum Element abstrahieren.

Jetzt weisen wir innerHTML dem Wert von this.template zu. Lassen Sie uns das jetzt definieren

class Repository extends HTMLElement {
  get template() {
    const repo = this.repoDetails;
  
    // if we get an error message let's show that back to the user
    if (repo.message) {
      return `<div class="Card Card--error">Error: ${repo.message}</div>`
    } else {
      return `
      <div class="Card">
        <aside>
          <img width="48" height="48" class="Avatar" src="${repo.owner.avatar_url}" alt="Profile picture for ${repo.owner.login}" />
        </aside>
        <header>
          <h2 class="Card__title">${repo.full_name}</h2>
          <span class="Card__meta">${repo.description}</span>
        </header>
      </div>
      `
    }
  }
}

Das war's so ziemlich. Wir haben ein Custom Element definiert, das seinen eigenen Zustand verwaltet, seine eigenen Daten abruft und diesen Zustand dem Benutzer widerspiegelt und uns gleichzeitig ein HTML-Element zur Verwendung in unserer Anwendung gibt.

Nachdem ich diese Übung durchgemacht hatte, stellte ich fest, dass die einzige erforderliche Abhängigkeit für Custom Elements die nativen APIs des Browsers sind, anstatt eines Frameworks zur zusätzlichen Verarbeitung und Ausführung. Dies macht die Lösung portabler und wiederverwendbarer, mit ähnlichen APIs wie die Frameworks, die Sie bereits lieben und nutzen, um Ihren Lebensunterhalt zu verdienen.

Es gibt natürlich auch Nachteile bei diesem Ansatz. Wir sprechen über verschiedene Browser-Unterstützungsprobleme und mangelnde Konsistenz. Außerdem kann die Arbeit mit DOM-Manipulations-APIs sehr verwirrend sein. Manchmal sind es Zuweisungen. Manchmal sind es Funktionen. Manchmal nehmen diese Funktionen einen Rückruf entgegen und manchmal nicht. Wenn Sie mir nicht glauben, schauen Sie sich das Hinzufügen einer Klasse zu einem HTML-Element an, das über document.createElement() erstellt wurde, was einer der Top-Fünf-Gründe für die Verwendung von React ist. Die grundlegende Implementierung ist nicht so kompliziert, aber sie ist inkonsistent mit anderen ähnlichen document-Methoden.

Die eigentliche Frage ist: Hebt sich das am Ende auf? Vielleicht. React ist immer noch ziemlich gut in dem, wofür es sehr, sehr gut konzipiert ist: dem virtuellen DOM, der Verwaltung des Anwendungszustands, der Kapselung und der Übergabe von Daten nach unten im Baum. Es gibt praktisch keinen Anreiz, Custom Elements innerhalb dieses Frameworks zu verwenden. Custom Elements hingegen sind einfach verfügbar, indem man eine Anwendung für den Browser erstellt.

Mehr erfahren