Erste Schritte mit Webkomponenten zur Entwicklung eines Videochat-Widgets

June 06, 2016
Autor:in:
Phil Nash
Twilion

Erste Schritte mit Webkomponenten zur Entwicklung eines Videochat-Widgets


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Getting started with Web Components building a Video Chat widget. Während wir unsere Übersetzungsprozesse verbessern, würden wir uns über Dein Feedback an help@twilio.com freuen, solltest Du etwas bemerken, was falsch übersetzt wurde. Wir bedanken uns für hilfreiche Beiträge mit Twilio Swag :)

Moderne Anwendungen werden vorzugsweise mit komponentenbasierten UI-Bibliotheken entwickelt. Angular und React sind derzeit besonders beliebt, aber auch der unscheinbare Browser mit seinen nativen APIs wird weiterhin gerne zur Anwendungsentwicklung genutzt. Webkomponenten gibt es seit 2011. Sie wurden damals eingeführt mit dem Ziel, einen komponentenbasierten Ansatz für die Webplattform zu schaffen.

Unter den verschiedenen Bibliotheken, die für die Entwicklung von Webkomponenten zur Verfügung stehen, sind insbesondere Polymer von Google, aber auch X-Tag und Bosonic erwähnenswert. Um die Möglichkeiten der Plattform aufzuzeigen, werde ich demonstrieren, wie man mithilfe der heute in Browsern verfügbaren APIs eine Webkomponente entwickeln kann. Es gibt unzählige Beispiele des Typs „hello world“ für Webkomponenten, daher wagen wir uns heute an etwas Schwierigeres ran: ein mit Twilio Video erstelltes Videochat-Widget. Das Ergebnis sollte am Ende in etwa so aussehen:

Ein Video-Chat zwischen Phil und Marcos

Und wenn wir unsere Komponente fertig gestellt haben, brauchen wir nur den folgenden HTML-Code, um sie zu verwenden:

<link rel="import" href="/twilio-video.html">

<twilio-video identity="phil"></twilio-video>

Was genau sind eigentlich Webkomponenten?

Webkomponenten bestehen aus vier miteinander kombinierbaren Browsertechnologien: benutzerdefinierte ElementeHTML-ImporteShadow DOM und Vorlagen. Damit lassen sich wiederverwendbare Benutzeroberflächen-Widgets für ein bestimmtes Verhalten erstellen. Diese können eingesetzt werden, indem die Komponente importiert und ein benutzerdefiniertes Element in die HTML-Seite eingebettet wird (siehe oben). Wie diese Technologien sich kombinieren lassen, sehen wir beim Erstellen unseres Videochat-Widgets.

Wenn du in erster Linie wissen möchtest, wie die fertige Komponente aussieht und verwendet wird, kannst du einfach einen Blick in das GitHub-Repository werfen. Anderenfalls machen wir uns jetzt an die Arbeit.

Tools für die Aufgabe

Wir brauchen Folgendes, um unsere Videochat-Komponente zu erstellen:

Steht alles bereit? Dann können wir loslegen.

Einrichten unseres Servers

Ich habe für dieses Projekt einen einfachen Server eingerichtet, den du nun ausführen musst. Dazu solltest du als Erstes das Repository herunterladen oder klonen.

$ git clone https://github.com/philnash/twilio-video-chat-web-component.git
$ cd twilio-video-chat-web-component

Danach brauchst du Twilio-Anmeldedaten, damit die Anwendung funktioniert. Jetzt solltest du deine Twilio-Konto-SID aus deiner Benutzerkonto-Übersicht heraussuchen, ein Videokonfigurationsprofil generieren, die SID notieren und dann einen API-Schlüssel und ein API-Geheimnis generieren. Sobald du das alles erledigt hast, können wir diese Dinge dem Projekt hinzufügen.

Erstelle eine Kopie der Datei .env.example und gib ihr den Namen .env. Auf dem Terminal kannst du das wie folgt erledigen:

$ cp .env.example .env

Öffne .env und gib die erforderlichen Anmeldedaten ein.

Als Nächstes installierst du die Abhängigkeiten für das Projekt und startest den Server.

$ npm install
$ node index.js

Die Anwendung wird nun auf localhost:3000 ausgeführt. Öffne sie in Chrome, um die Seite zu sehen, mit der wir arbeiten werden.

Eine leere Seite wird angezeigt. Sie mag leer sein, aber es ist ein Anfang.

Im Moment tut sich dort noch nicht viel. Das ändern wir jetzt, indem wir eine Webkomponente erstellen.

HTML-Importe

Wenn man eine Komponente erstellt, befindet sich diese in einer eigenen Datei und kann importiert werden. Dadurch bleibt der gesamte Code schön verpackt. Wir erstellen jetzt zuerst den HTML-Import für unsere Komponente.

Leg dazu im öffentlichen Verzeichnis eine Datei mit dem Namen twilio-video.html an. Diese kann vorerst leer bleiben.

In public/index.html fügst du nun am Ende des Elements Folgendes hinzu:


  <link href="/css/app.css" rel="stylesheet">
  <link rel="import" href="/twilio-video.html">
</head>

Wenn du die Seite aktualisierst, passiert nichts! Okay, das stimmt nicht ganz. Öffne über die Registerkarte „Network“ die Konsole mit den Entwicklertools (Cmd + Opt + I auf einem Mac oder Strg + Umschalt + I unter Windows) und du siehst, dass wir unser neues HTML-Dokument geladen haben.

dev console

Um unseren HTML-Import optimal zu nutzen, erstellen wir nun unsere Komponente darin.

Benutzerdefinierte Elemente

Wir wollen das Erstellen eines Videochats so einfach gestalten, als würde man ein HTML-Element auf die Seite ziehen, das <twilio-video> Element. Wir fügen es nun der Datei index.html hinzu. Dazu ersetzen wir das <h1>

auf der Seite mit:

<twilio-video identity="phil"></twilio-video>

Wenn wir die Seite aktualisieren, sehen wir, dass der Titel verschwunden und durch unser benutzerdefiniertes Element ersetzt worden ist. Dieses Element ist allerdings noch nicht funktionstüchtig. Wir gehen nun in den HTML-Import und beginnen mit der Entwicklung unseres benutzerdefinierten Elements.

Als Erstes müssen wir unser Element registrieren. Dazu müssen wir JavaScript programmieren. In der Datei twilio-video.html schreiben wir:

<script>
  var TwilioVideoPrototype = Object.create(HTMLElement.prototype);
  document.registerElement("twilio-video", {
    prototype: TwilioVideoPrototype
  });
</script>

document.registerElement erhält den Namen des Elements, das wir registrieren möchten, in diesem Fall also „twilio-video“. Achtung, der Name muss einen Bindestrich enthalten, denn dadurch geben wir an, dass es sich um ein benutzerdefiniertes Element und nicht um ein browserdefiniertes Element handelt.

Darüber hinaus wird ein optionales Objekt mit Optionen benötigt. Dieses Objekt gibt an, worauf unser benutzerdefiniertes Element basiert. Wir nehmen prototype und geben einen neuen Prototyp auf der Basis des HTMLElement-Prototyps an. Dies ist das Basisobjekt für alle HTML-Elemente. Im weiteren Verlauf dieses Projekts erweitern wir diesen Prototyp.

Wenn du die Seite nun aktualisierst … passiert wieder nichts. Schließlich haben wir bislang nur ein leeres Element erstellt. Standardmäßig handelt es sich dabei um ein Inline-Element ohne Inhalt und Verhalten. Über den zuvor definierten Prototyp können wir jedoch sowohl Inhalt als auch Verhalten hinzufügen. Zunächst müssen wir uns allerdings kurz mit dem Lebenszyklus von benutzerdefinierten Elementen vertraut machen.

Der Lebenszyklus eines benutzerdefinierten Elements

Benutzerdefinierte Elemente sind mit verschiedenen Funktionen versehen, die während des Lebenszyklus eines Elements aufgerufen werden. Diese sogenannten Callbacks sind hilfreich, um Verhalten, Markup und Inhalt hinzuzufügen und zu entfernen. Hier sind sie:

  • createdCallback – zum Erstellen einer Instanz des Elements
  • attachedCallback – zum Einfügen der Instanz in das Dokument
  • detachedCallback – zum Entfernen der Instanz aus dem Dokument
  • attributeChangedCallback – zum Aktualisieren eines Attribut des Elements

Wir verwenden createdCallback, um den Inhalt und das Verhalten des Elements zu definieren, und detachedCallback, um beides wieder zu entfernen.

Bevor wir uns dem Inhalt unseres Elements widmen können, müssen wir uns allerdings genauer mit den beiden letzten Aspekten von Webkomponenten beschäftigen.

Vorlagen

Möglicherweise ist nun die Versuchung groß, den HTML-Inhalt unseres benutzerdefinierten Elements in JavaScript zu generieren. Das ist allerdings sehr umständlich, und es gibt eine bessere Lösung. HTML-Vorlagen sind inaktive HTML-Elemente, die in eine Seite eingebettet und dann über JavaScript instanziiert werden können. Sie sind inaktiv, weil <script> (Skripte) innerhalb einer Vorlage nicht ausgeführt werden können und <link>s (Links) und Ressourcen wie <img> (Bilder) erst abgerufen werden, wenn die Vorlage instanziiert wird.

Wir richten den Inhalt, den wir in unserem Element benötigen, mithilfe einer Vorlage ein. Bei Empfang der Lebenszyklusfunktion createdCallback versehen wir das Element mit Inhalt. Wir beginnen mit einem <template>-Element oben in unserer Datei twilio-video.html.

<template id="twilio-video-template">
  <style type="text/css">
  :host {
    display: block;
  }
  #picture-in-picture {
    position: relative;
    width: 400px;
    height: 300px;
  }
  #caller {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
  }
  #caller video,
  #me video {
    width: 100%;
  }
  #me {
    position: absolute;
    width: 25%;
    bottom: 5%;
    right: 5%;
  }
  #hangup {
    position: absolute;
    bottom: 5%;
    left: 5%;
  }
  </style>

  <div id="picture-in-picture">
    <div id="caller"></div>
    <div id="me"></div>
    <button id="hangup">Hangup</button>
  </div>
</template>

Die Stile innerhalb der Vorlage werden alle auf das Element ausgerichtet. Da wir nun unser HTML erstellt haben können wir es dem benutzerdefinierten Element hinzufügen.

<script>
  var TwilioVideoPrototype = Object.create(HTMLElement.prototype);
  var importDoc = document.currentScript.ownerDocument;

  TwilioVideoPrototype.createdCallback = function() {
    var template = importDoc.getElementById('twilio-video-template');
    var clone = importDoc.importNode(template.content, true);
    this.appendChild(clone);
  }

  document.registerElement("twilio-video", {
    prototype: TwilioVideoPrototype
  });
</script>

Wir haben hier ein paar neue Dinge verwendet.

document.currentScript.ownerDocument bezieht sich auf die HTML-Datei, in die wir all dies schreiben, und nicht auf das Dokument für den Import. So können wir einfach auf Elemente in unserer Datei verweisen, wie z. B. die Vorlage.

Mit dem Dokument erhalten wir einen Verweis auf unsere Vorlage und können sie über importNode klonen. Dadurch wird in der Vorlage ein Klon des HTML erstellt. Das Klonen ist erforderlich, damit das ursprüngliche HTML in der Vorlage bleibt. Anschließend fügen wir den Klon unserem benutzerdefinierten Element hinzu.

Jetzt aktualisieren wir die Anwendung und prüfen den Seiteninspektor.

Wenn wir das benutzerdefinierte Element untersuchen, können wir alle seine Interna sehen. Nicht, was wir wollen.

In unserem benutzerdefinierten Element befindet sich nun Inhalt. Das Ganze ist jedoch noch nicht ideal, denn der Inhalt ist vom Rest der Seite zugänglich. Dies wollen wir verhindern, und das Schöne an Webkomponenten ist, dass wir ihr gesamtes Verhalten „einkapseln“ können. Das bringt uns zum letzten Aspekt von Webkomponenten: Shadow DOM.

Shadow DOM

Um Shadow DOM zu verstehen, sollte man es sich in Aktion ansehen. Öffne eine Seite mit einem HTML5-Videoelement. Falls dir keine einfällt, kannst du diese nehmen. Wenn du das Videoelement inspizierst, siehst du nur das <Video>-Tag und die darin enthaltenen Elemente.

Wenn wir ein HTML5-Videoelement untersuchen, wird nur das erwartete HTML, ein Quellelement und ein Fallback-Absatzelement angezeigt.

Da wir Chrome verwenden, öffnen wir jetzt die Einstellungen für die Entwicklertools, suchen das Kontrollkästchen „Show user agent shadow DOM“ und kreuzen es an.

Wenn wir die Einstellungen für die Entwicklertools öffnen, können wir das Shadow DOM des Benutzeragenten unter der Überschrift „Elements“ anzeigen. Schaut euch das an.

Wenn du das Video erneut prüfst, siehst du ein #shadow-root-Element. Du kannst es öffnen und das gesamte darin enthaltene HTML inspizieren.

Wenn wir nun das Video-Element untersuchen, befinden sich ein Shadow Root und eine ganze Menge Divs, Eingaben und anderes HTML darin.

Das ist das Shadow DOM. Es sorgt dafür, dass die interne Struktur unseres benutzerdefinierten Elements vertraulich bleibt, und wir aktualisieren unser benutzerdefiniertes Element, um es zu verwenden. Dazu müssen wir die folgende Änderung an unserem JavaScript vornehmen.


  TwilioVideoPrototype.createdCallback = function() {
    var template = importDoc.getElementById('twilio-video-template');
    var clone = importDoc.importNode(template.content, true);
    var shadowRoot = this.createShadowRoot();
    shadowRoot.appendChild(clone);
  }

Statt den Vorlagen-Klon an das Element selbst anzuhängen, erstellen wir ein Shadow Root für das Element. Anschließend hängen wir unseren Vorlangen-Klon daran an. Nach dem Aktualisieren der Seite können wir uns unser neues Shadow Root ansehen.

Mit der Aktualisierung des JavaScript sehen wir jetzt ein Shadow Root für unser benutzerdefiniertes Element im Inspektor.

Der Videochat

Das Zusammenstellen unserer Webkomponente hat einiges an Vorarbeit erfordert. Jetzt müssen wir nur noch unseren Videochat implementieren. Falls du Sams Blog-Beitrag zu Erste Schritte mit dem JavaScript Video SDK gelesen hast oder die Anwendung JavaScript Video Quickstart durchgegangen bist, wird dir das Meiste bekannt vorkommen. Wir fügen die Twilio-Videoskripte am Anfang unserer Komponente hinzu:

<script src="https://media.twiliocdn.com/sdk/js/common/v0.1/twilio-common.min.js"></script>
<script src="https://media.twiliocdn.com/sdk/js/conversations/v0.13/twilio-conversations.min.js"></script>

Innerhalb des <script>-Elements, mit dem wir gearbeitet haben, brauchen wir die folgenden Funktionen:

fetchToken nutzt die Fetch API, um ein Zugriffstoken über unseren Node.js-Server zu erstellen, die JSON-Antwort zu parsen und ein Promise zurückzugeben.

  TwilioVideoPrototype.fetchToken = function(identity) {
    return fetch("/token?identity=" + identity).then(function(data){
      return data.json();
    });
  }

createClient richtet mit dem vom Server abgerufenen Token einen AccessManager ein, instanziiert einen Conversations.Client und wartet auf eingehende Verbindungen.

  TwilioVideoPrototype.createClient = function(obj) {
    var accessManager = new Twilio.AccessManager(obj.token);
    this.conversationsClient = new Twilio.Conversations.Client(accessManager);
    return this.conversationsClient.listen();
  }

setupClient wird ausgeführt, sobald der Conversation Client auf eingehende Verbindungen wartet. Der Setup Client wartet auf eingehende Einladungen.

  TwilioVideoPrototype.setupClient = function() {
    this.conversationsClient.on("invite", this.inviteReceived.bind(this));
  }

Bei Empfang einer Einladung wird inviteReceived aufgerufen, wenn wir die Einladung annehmen. Dadurch wird wiederum ein Promise zurückgegeben.

  TwilioVideoPrototype.inviteReceived = function(invite){
    invite.accept().then(this.setupConversation.bind(this));
  }

Nach der Auflösung des Promise rufen wir setupConversation auf. Es werden die Elemente in unserem benutzerdefinierten Element angezeigt, der lokale Media Stream wird eingeblendet, es wird auf Klicks von der Auflegtaste gewartet, und es werden hergestellte und getrennte Verbindungen anderer Teilnehmer verarbeitet.

  TwilioVideoPrototype.setupConversation = function(conversation) {
    this.currentConversation = conversation;
    conversation.localMedia.attach(this.me);
    this.chat.classList.remove("hidden");
    this.hangup.addEventListener("click", this.disconnect.bind(this));
    conversation.on("participantConnected", this.participantConnected.bind(this));
    conversation.on("disconnected", this.disconnected.bind(this));
  }

Nach dem Empfang des Ereignisses participantConnected zeigen wir auch den Media Stream des neuen Teilnehmers.

  TwilioVideoPrototype.participantConnected = function(participant) {
    participant.media.attach(this.caller);
  }

Wenn ein Teilnehmer die Verbindung trennt, verbergen wir den gesamten Chat, entfernen unseren lokalen Media Stream und warten nicht mehr auf Ereignisse von der Auflegtaste.

  TwilioVideoPrototype.disconnected = function() {
    this.chat.classList.add("hidden");
    this.currentConversation.localMedia.detach();
    this.hangup.removeEventListener("click", this.disconnect.bind(this));
  }

Wird die Auflegtaste gedrückt, beenden wir den Anruf. Diese Funktion greift auch, wenn das Element von der Seite entfernt wird. Wir prüfen daher, ob es zum betreffenden Zeitpunkt ein Live-Gespräch gibt.

  TwilioVideoPrototype.disconnect = function() {
    if(this.currentConversation){
      this.currentConversation.disconnect();
      this.currentConversation = null;
    }
  }

Die Funktion createdCallback, die wir eben gestartet haben, richtet nun die Vorlage ein, fügt sie dem Shadow Root hinzu und überprüft das Shadow Root auf die Elemente, die wir in den Funktionen oben verwendet haben. Mithilfe von this.getAttribute("identity") prüft sie auch das Identitätsattribut der Komponente. Vorhin habe ich das Element als <twilio-video identity="phil"></twilio-video> definiert. Es ruft also die Identität „phil“ ab und sendet sie an den Server, um über fetchToken ein Zugriffstoken für diese Identität zu generieren.


  TwilioVideoPrototype.createdCallback = function() {
    var template = importDoc.getElementById("twilio-video-template");
    var clone = importDoc.importNode(template.content, true);
    var shadowRoot = this.createShadowRoot();
    shadowRoot.appendChild(clone);

    var identity = this.getAttribute("identity") || "example";

    this.me = shadowRoot.getElementById("me");
    this.caller = shadowRoot.getElementById("caller");
    this.chat = shadowRoot.getElementById("picture-in-picture");
    this.hangup = shadowRoot.getElementById("hangup");

    this.fetchToken(identity).
      then(this.createClient.bind(this)).
      then(this.setupClient.bind(this)).
      catch(function(err) {
        console.log(err);
      });
  }

Abschließend gibt es die Funktion detachedCallback, die die Verbindung zu Live-Gesprächen trennt und dafür sorgt, dass der Conversation Client nicht mehr auf eingehende Verbindungen wartet.

  TwilioVideoPrototype.detachedCallback = function() {
    this.disconnect();
    this.conversationsClient.unlisten();
  }

Wir fügen all dies dem Element <script> unserer Komponente hinzu, aktualisieren die Seite und warten auf einen eingehenden Anruf. Ich habe das Ganze schön einfach aufgesetzt, du musst nur http://localhost/caller.html öffnen. Es gibt keine Benutzeroberfläche auf dieser Seite, aber es wird ein Anruf an deine Komponente generiert (sofern du die Identität „phil“ beibehalten hast; falls du sie geändert hast, kannst du die Zeile conversationsClient.inviteToConversation("phil"); in caller.html anpassen und deine gewählte Identität eintragen).

Wenn die Seite lädt, erhältst du auf jeder Seite eine Zugriffsanfrage für dein Video und dein Mikrofon. Wenn du die Zugriffsanfrage genehmigst, wird die Verbindung hergestellt, und du siehst, wie die Videochat-Webkomponente aktiviert wird.

Das Video verbindet sich und du kannst deinem Freund winken.

Den vollständigen Code für diese Webkomponente findest du auf GitHub.

Reduzieren, wiederverwenden, recyceln

Wir haben mit weniger als 130 Zeilen HTML, CSS und JavaScript – und ohne ein Framework zu nutzen – eine wiederverwendbare Webkomponente erstellt, die eingehende Videoanrufe empfangen kann. Mit den beiden folgenden Zeilen Code (und einem /token-Endpunkt zur Generierung von Zugriffstoken) können wir diese Webkomponente beliebig wiederverwenden.

<link rel="import" href="/twilio-video.html">

<twilio-video identity="phil"></twilio-video>

Okay, derzeit funktioniert sie nur in Chrome. Aber es sind Polyfills verfügbar, mit denen wir die Webkomponente in jedem Browser ans Laufen bringen (bitte die minimalen Unterschiede bei der API berücksichtigen).

Ich würde mich freuen, von dir zu hören, wie du Webkomponenten einsetzt. Hast du eigene entwickelt oder verwendest du die von jemand anderem? Melde dich über die Kommentare unten oder schreib mir auf Twitter oder per E-Mail.