Vortragsrundreise zu Progressive Web Apps mit Stationen in Essen, Halle/Saale, Wiesbaden, Hamburg und Düsseldorf

Veröffentlicht am 17. Mai 2018

Ich werde in den nächsten Wochen eine kleine Vortragstour durch diverse JS-User-Groups u.Ä. machen und ihr solltet alle vorbeikommen! Das Thema meines Talks sind natürlich Progressive Web Apps und die damit verbundenen Buzzwords. Anhand einer kleinen Beispiel-App werde ich zeigen, wie Service Worker, Manifest und Konsorten eingesetzt werden, ohne auf ein bestimmtes Framework beschränkt zu sein. Ob ihr fancy Webapps schreibt oder herkömmliche PHP-CMS-Webseiten betreibt ist egal, für jeden ist was dabei!

Die folgenden Termine stehen fest:

  • 24. Mai: Webworker Ruhr in Essen (ab 19 Uhr im Unperfekthaus, Friedrich-Ebert-Str. 18, 45127 Essen)
  • 28. Mai: Webmontag Halle in Halle (Saale) (ab 19:00 Uhr im MMZ Halle, Mansfelder Str. 56, 06108 Halle)
  • 30. Mai: RheinMainJS in Wiesbaden (ab 19:00 Uhr bei AOE, Kirchgasse 6, 65185 Wiesbaden)
  • 14. Juni: Angular Meetup Hamburg in Hamburg (ab 19:00 Uhr an einem noch festzulegenden Ort)
  • 17. Juli: Webworker NRW in Düsseldorf (ab 19:00 Uhr bei Sipgate, Gladbacher Straße 74, 40219 Düsseldorf)

Falls auch ihr ein Meetup habt, bei den ich mal vorbeischauen soll, schreibt mir eine E-Mail!

Fragen zu HTML5 und Co beantwortet 24 - HTML-Kommentare, Indexed DB, JavaScript-Regex, HTML5-Buch

Veröffentlicht am 3. Mai 2018

Nach längere Pause wird es mal wieder Zeit, Webtech-Fragen zu beantworten, die mich via E-Mail oder Twitter erreicht haben. An Fragen herrschte schon länger kein Mangel, ich kam bloß nicht zu schreiben … bis jetzt!

Falls euch euch Fragen zu HTML, CSS und JS unter den Nägeln brennen, dürft ihr sehr gern zu Vergrößerung meines Blogpost-Backlogs beitragen! Ich antworte immer zeitnah, es sind nur die Veröffentlichungen, die immer ein wenig Zeit brauchen.

Wo dürfen Kommentare in HTML stehen?

Wo dürfen in einer HTML5-Seite Kommentare vorkommen? Darf sich vor <html> ein Kommentar befinden?

In einem standardkonformen HTML-Dokument dürfen Kommentare an folgenden Stellen vorkommen:

  • direkt vor dem Doctype
  • direkt nach dem Doctype (also vor <html>)
  • innerhalb des <html>-Elements überall wo auch Text vorkommen kann, d.h. zwischen, vor und nach HTML-Tags
  • nach dem <html>-Element

Nicht erlaubt sind Kommentare also im Wesentlichen innerhalb eines Tags, so wie etwa hier:

<p <!--class="foo"--> ></p>

Das wäre ungültiges HTML, liefert trotzdem ein anzeigbares HTML-Dokument. Der Parser macht aus dem obigen Code <p>-Element mit einem Attribut „<!--class“ (mit dem Wert foo) sowie einem Attribut „--“ (ohne Wert). Die unerwartete Spitzklammer nach dem Tag-Namen läuft in der Parser-Logik als unexpected-character-in-attribute-name parse error und hat zur Folge, dass das Zeichen in den Attribut-Namen eingebaut wird. Am Ende erhält das <p>-Element noch „>“ als Text-Inhalt, denn die Spitzklammer für das Ende unseres geplanten „Kommentars“ wird als Ende des öffnenden <p>-Tags interpretiert.

Man sieht also: auch wenn das Markup ungültig ist, kommt am Ende ein Dokument mit vorhersagbarem Inhalt heraus. Der HTML-Parser ist eben ein ziemlicher Müllschlucker – egal was man hineinsteckt, er macht ein Dokument daraus.

Wie kann man Indexed DB verwenden?

Ich versuche gerade Indexed DB zu verstehen und habe dazu auch deine Artikel gelesen. Da ich allgemein mit JavaScript und der Webentwicklung noch nicht so fit bin, verstehe ich die Events nicht. Was genau passiert da und wie funktioniert das?

Willkommen im Club! Fast niemand versteht Indexed DB. Meiner Ansicht nach ist Indexed DB für sich genommen eine der unbrauchbarsten aller Browser-APIs ist, die seit mit HTML5 eingeführt wurden. Indexed DB ist sehr low-level, sehr kleinteilig, sehr komplizit und hat exakt null Komfortfeatures.

Allerdings kommt dieses Design nicht von ungefähr. Das Extensible Web Manifesto postuliert:

To enable libraries to do more, browser vendors should provide new low-level capabilities that expose the possibilities of the underlying platform as closely as possible.

Low-Level-APIs wie Indexed DB mögen für sich genommen nicht für Webapp-Programmierung zu gebrauchen sein, bieten aber ein Fundament für Libraries. Libraries lassen sich leichter implementieren und austauschen, um eine Vielzahl von Use Cases abzudecken. Standards sind wesentlich schwieriger zu entwickeln, weswegen man lieber einen Low-Level-Standard entwirft, auf dem sich ein Ökosystem von Libraries bilden kann, als dass man versucht, mit einem Webstandard alle möglichen Use Cases abzudecken.

Statt sich mit Indexed DB herumzuschlagen lohnt es sich viel mehr, Libraries auf Indexed-DB-Basis zu verwenden. Eine kleine Auswahl:

  • localForage bietet einen einfachen Key-Value-Store auf Basis von Indexed DB, inkl. Unterstützung für Promises
  • Dexie.js ist ein Komfort-Wrapper um Indexed DB mit vielen Features, aber auch vielen Erleichterungen und einer gut durchdachten API
  • PouchDB ist eine JS-Implementierung von Apache CouchDB und kann sich dank des Sync-Protokolls mit „echten“ CouchDB-Instanzen (und anderen kompatiblen Datenbanken) synchronisieren.

Wenn man im Browser Daten speichern möchte, sollte man dringend zu einer solchen Library greifen und gar nicht erst versuchen, Indexed DB selbst zu verwenden

Komische Arrays bei String.prototype.match()

Ich bin gerade auf eine Sache gestoßen die ich mir nicht erklären kann. Folgendes Beispiel:

var text  = "xxx";
var m1 = text.match("x");
var m2 = text.match(/x/g);

Bei beiden Aufrufen wird mir ein Array zurückgegeben. Beim ersten jedoch nur ein Ergebnis gefunden, bei dem anderen natürlich mehrere. Wenn nur ein Ergebnis gefunden wird, dann hat das Array scheinbar zusätzliche Properties (input und index):

m1: Array[1]
  0: "x",
  index: 0,
  input: "xxx",
  length: 1,
  __proto__: Array[0]
m2: Array[3]
  0: "x",
  1: "x",
  2: "x",
  length: 3,
  __proto__: Array[0]

Im ECMAScript-Standard wird das bei der Funktion RegExp.prototype.exec(string) auch so beschrieben.

Erstmal verstehe ich nicht ganz, wie auf einmal ein Array noch „geheime“ Extra-Properties besitzen kann. Im Prinzip ist mir schon klar dass das Array auch nur ein Objekt ist und daher beliebige Properties bekommen kann, aber warum nutzt man dann ein Array? Und zum anderen … warum gibt es diese Properties nicht, wenn mehrere Ergebnisse gefunden werden? Es ist total verwirrend! Warum werden bei einem Ergebnis Properties hinzugefügt, die auf einmal nicht vorhanden sind, sobald es mehrere Ergebnisse gibt? Und warum werden überhaupt irgendwelche Properties auf ein Array gesteckt?

Ich würde das Phänomen so betrachten, dass String.prototype.match() unterschiedliche Fragen beantwortet, je nachdem, was für einen Regulären Ausdrück man hineinsteckt. Im Fall von m1 wird aus dem String ein „normaler“ Regex gebaut, bei m2 ist es ein Regex mit Global-Flag. Diese beiden Regulären Ausdrücke lassen die Funktion zwei unterschiedliche Fragen beantworten. Ohne Global-Flag wird ermittelt ob ein es ein Match gibt und wenn ja, wo dieses Match im String zu finden ist – daher die Property index. Technisch ist das so gelöst, dass String.prototype.match() an dieser Stelle an RegExp.prototype.exec() delegiert. Mit Global Flags werden hingegen alle Matches geliefert – und zwar die Matches selbst, nicht Informationen über die Matches. Das Verhalten ergibt sich allein allein aus dem Fehlen oder Vorhandensein des Global-Flags, nicht aus der Anzahl der sich ergebenden Ergebnisse.

Beispiel:

var text  = "xyx";
var m1 = text.match("y");  // > Ein Treffer MIT Extra-Properties
var m2 = text.match(/y/g); // > Ein Treffer OHNE Extra-Properties

Warum „mißbraucht“ man hier ein Array? Ich nehme an, dass man als Ergebnis von String.prototype.match() eine Liste von Ergebnissen haben wollte, und da gab es vor ECMAScript 6 nur die Wahl zwischen Pest und Cholera. Entweder man baut sich ein Pseudo-Array wie das berüchtigte Arguments-Objekt, dem all die praktischen Array-Methoden fehlen oder man erweitert eben ein Array, das alle Features hat – aber dann eben auch einiges, was ein Array nicht haben sollte. Das ist beides nicht optimal (und letzteres ist ein warnendes Beispiel dafür, warum man for-in-Schleifen nicht mit Arrays verwenden sollte). In ES6 würde man vermutlich ein normales Objekt bauen und einen Iterator implementieren, aber als damals String.prototype.match() eingeführt wurde war das eben noch nicht möglich.

Ist das HTML5-Buch noch aktuell?

Würdest du dein HTML5 Buch von 2010 bei einem Umzug selbst noch mal einpacken oder meinste dass 8 Jahre später die Entwicklung schon zu sehr weiter gegangen ist?

Das erste Kapitel über die Hintergründe von HTML5 hat vielleicht noch historischen Wert, aber ansonsten kann man das Buch aufgrund von Fossilität getrost in die Tonne treten.

Weitere Fragen?

Habt ihr auch dringende Fragen zu Frontend-Technologien? Nur her damit! Alle Fragen in diesem Post wurden mir per E-Mail oder Twitter gestellt und ihr könnt das genau so machen! Einfach über einen der genannten Kanäle anschreiben oder gleich das komplette Erklärbären-Paket kommen lassen.

PostMessage zwischen Service Worker und Client(s)

Veröffentlicht am 11. April 2018

Ein Service Worker ist nur ein Worker und entsprechend sollte der Nachrichtenaustausch via postMessage() und MessageEvent eine Kleinigkeit sein – möchte man meinen! Tatsächlich ist die ganze Angelegenheit beim Service Worker etwas weniger trivial als bei anderen Workern, was an ein paar besonderen Umständen liegt:

  1. Eine Seite/App wird immer von einem Service Worker kontrolliert …
  2. … der zu einem gegebenen Zeitpunkt ggf. noch inaktiv sein könnte …
  3. … und nicht der einzige vorhandene Worker sein muss …
  4. … aber seinerseits mehrere Seiten/Apps (Clients) kontrollieren könnte.

Diese Gemengelage führt zu einer etwas asymmetrischen API, die zwar logisch, aber nicht direkt offensichtlich ist.

Eine Seite/App kann immer nur einen Service Worker haben. Allerhöchstens könnte es verschiedene Versionen geben, wenn sich z.B. nach der Installation eines Updates ein neuer Worker inaktiv in Warteposition hinter dem aktuellen, aktiven Worker steht. Daher kann es für eine Seite/App nur eine Service-Worker-Message-Quelle geben, die unter navigator.serviceWorker anzuzapfen ist:

navigator.serviceWorker.addEventListener("message", (evt) => {
  window.alert(`Nachricht vom SW: ${ evt.data }`);
});

Es kann natürlich sein, dass gar kein Service Worker aktiv ist oder es überhaupt niemals einen Service Worker für die Seite/App geben wird, aber das ist für das Registrieren eines Event Listeners nicht relevant. Wenn es keine Message-Events gibt (entweder mangels Nachrichten oder mangels Senders), wird der Handler einfach nie aufgerufen. Es ist, als würde man an einen Briefkasten aufstellen – das kann man machen, auch wenn man niemals einen Brief erhalten wird.

Anders sieht es beim Senden von Nachrichten aus. Hier wird ein Ziel benötigt und dieses Ziel muss in der Lage sein, Nachrichten anzunehmen. Das heißt, dass wir darauf warten müssen dass ein Service Worker aktiv wird und dann auch explizit diesen als Adressaten auswählen müssen. In Code bedeutete das:

// Nachrichten zum SW senden
// 1. warten bis der SW aktiv ist
navigator.serviceWorker.ready
  .then( (registration) => {
    // 2. Zugriff auf aktiven SW erhalten
    if (registration.active) {
      // 3. Nachricht senden
      registration.active.postMessage(23);
    }
  });

Das Promise unter navigator.serviceWorker.ready liefert eine ServiceWorkerRegistration, sobald es einen aktiven Service Worker gibt. Das ServiceWorkerRegistration-Objekt enthält Properties für Service Worker in verschiedenen Stadien: installing, waiting und active. Eine Seite/App wird immer von exakt einem Worker kontrolliert, aber wenn sich neben dem aktivem Worker beispielsweise grade ein Update installiert, sind trotzdem zwei Worker vorhanden. Eine Message muss immer an einen Worker gehen, also rufen wir dort postMessage() auf.

Natürlich spricht auch nichts dagegen, einem nicht-kontrollierenden Worker eine Nachricht zu senden oder mehrere Nachrichten an mehrere Worker zu verteilen:

// Nachrichten zum SW senden
// 1. warten bis der SW aktiv ist
navigator.serviceWorker.ready
  .then( (registration) => {
    // 2. Zugriff auf aktiven SW erhalten
    if (registration.active) {
      // 3. Nachricht senden
      registration.active.postMessage(23);
    }
    // 4. Zugriff auf SW für bereits installiertes Update erhalten
    if (registration.waiting) {
      // 5. Nachricht senden
      registration.waiting.postMessage(42);
    }
  });

Aufseiten des Workers ist das Empfangen von Nachrichten recht simpel: das message-Event auf dem globalen Objekt fängt alle Meldungen aller verbundener Seiten/Apps ein. In der source-Eigenschaft des Event-Objekts befindet sich das Client-Objekt, das die sendende Seiten/App repräsentiert. Ein solches Client-Objekt implementiert seinerseits postMessage(), so dass sich Nachrichten leicht zurücksenden lassen:

// Nachrichten aus der Webseite empfangen
self.addEventListener("message", (evt) => {
  const client = evt.source;
  client.postMessage(`Pong: ${ evt.data }`);
});

Zugriff auf alle übrigen Clients gibt es über die asynchrone Methode self.clients.matchAll(), so dass sich eine Nachricht von einem Client in den Worker an alle anderen Clients weiterverteilen lässt:

self.addEventListener("message", async (evt) => {
  const messageSource = evt.source;
  const clients = await self.clients.matchAll();
  for (const client of clients) {
    if (client !== messageSource) {
      client.postMessage(`Message from ${ messageSource.id }: ${ evt.data }`);
    }
  }
});

Und schon funktioniert der Nachrichtenaustausch zwischen Client(s) und Service Worker problemlos! Eigentlich ist das ganze Wirrwarr nur eine Konsequenz aus der grundsätzlichen Funktionsweise von Service Worker und somit hoffentlich nur auf den ersten Blick leicht irritierend.

Conditional CSS mit Pseudo-Booleans

Veröffentlicht am 29. März 2018

Wie dem einen oder anderen Twitter-Verfolger aufgefallen sein mag, ringe ich zur Zeit mit einem Projekt rund um per JavaScript generiertes CSS. Besagter CSS-Generator soll im Prinzip aus einem gegebenen Input immer den gleichen Output liefern, aber es soll auch ein paar an- und abschaltbare Features geben. Da stellt sich die Frage: wie sieht die bestmögliche API zum An- und Abschalten aus? Natürlich wäre es möglich, der JavaScript-Funktion ein paar das Design bzw. CSS betreffende Konfigurations-Parameter zu verpassen, aber da es in meinem Fall wirklich um die Feinjustierung von CSS geht, passt mir das nicht wirklich ins Konzept. Lieber würde ich dafür CSS-Variablen (die es ja schon länger gibt) und Booleans einsetzen, zum Beispiel so:

/* Pseudo-CSS */

body {
  --show-border: true;
}

.foo {
  if (var(--show-border)) {
    border: 1px solid red;
  }
}

Natürlich funktioniert der obige Code in dieser Form nicht und entsprechende Funktionalität in CSS einzubauen wäre auch nach aktuellem Diskussionsstand alles andere als trivial. Dennoch kann man dem Traum von CSS-Booleans mit Hilfe von Custom Properties recht nahe kommen – zumindest für bestimmte Fälle:

/* Echtes CSS */
body {
  --show-border: 1; /* Alternativ 0 für "false" */
}

.foo {
  border-color: red;
  border-style: solid;
  border-width: calc(1px * var(--show-border));
}

So einfach kann man es sich zurecht tricksen: die gewünschte Rahmenbreite wird entweder mit 1 multipliziert und taucht damit unverändert auf oder verschwindet durch die Multiplikation mit 0. Ein Standardwert, der greift, wenn die „Boolean-Property“ nicht gesetzt ist, kann über den Fallback-Parameter der var-Funktion umgesetzt werden:

/* Echtes CSS */
body {
  --show-border: 1;
}

.foo {
  border-color: red;
  border-style: solid;
  border-width: calc(1px * var(--show-border, 0));
}

Hier wird also standardmäßig keine Border (bzw. eine Border mit 0px Breite) angezeigt.

Im Detail ist das natürlich nicht das gleiche wie echte Booleans, denn es werden ja auch im False-Fall (bzw. im 0-Fall) bestimmte border-Properties gesetzt. Man sieht nur dann 0px Breite nicht direkt etwas davon. Der Trick würde spätestens auffallen, sobald Kindelemente des betroffenen Elements border-Properties erben oder die Kaskade ins Spiel kommt. Für viele Fälle ist das aber akzeptabel. Für meinen CSS-Generator reicht es zumindest für einige der konfigurierbaren Anzeige-Optionen – und das, was nicht mit CSS-Fake-Booleans herbei getrickst werden kann, wandert dann eben in die JavaScript-API.