Alle Jahre wieder sammeln sich in meiner Inbox genug Fragen zu Webtechnologien an, um einen Blogpost zu rechtfertigen. Dass es dieses Mal fast ausschließlich Fragen zu JavaScript und TypeScript sind, muss nicht sein – wenn ihr mir zu anderen Themen (oder eben diesen Themen) Fragen mailt oder mich auf Twitter anquatscht, werde ich sie beantworten und vielleicht eines Tages auch in einem Post wie diesem veröffentlichen!

Jens fragt: Bitte was? HTML-IDs legen globale JavaScript-Variablen an?!

Wenn ich einem HTML-Element eine ID gebe, kann ich das Element ohne document.getElementById() ansprechen? Einfach so, als globale Variable? Ist das Teil des Standards?

Ja, das ist Teil des HTML/DOM-Standards! Es handelt sich um den sogenannten named access on the Window object, der im Übrigen nicht nur mit IDs funktioniert. Dezent vereinfacht können als globale Variable verwendet werden:

  1. iframe-, embed-, form-, frameset-, img- und object-Elemente mit einem name-Attribut, dessen Wert als Variablenname fungiert
  2. Alle sonstigen Elemente mit einem id-Attribut, dessen Wert als Variablenname fungiert

Gibt es mehrere Kandidaten für einen Variablennamen (z.B. ein Iframe mit name="foo" und ein Div mit id="foo"), gilt folgende Rangfolge:

  1. Ist einer der Kandidaten ein Iframe mit passendem name, belegt es den Variablennamen; bei mehreren Iframe-Kandidaten belegt das erste Element im DOM den Namen
  2. Gibt es keinen passenden Iframe, kommen die Kandidaten aus Kategorie embed/form/frameset/img/object zum Zuge, wobei auch hier gilt, dass bei mehreren Kandidaten mit passendem name-Wert der Erste Kandidat im DOM verwendet wird
  3. Zuletzt kommen Kandidaten mit id-Attribut (bzw. der erste solche Kandidat im DOM) an Reihe.

Dass in einer Welt, die Funktionen wie document.querySelector() bietet, niemand dieses ausgesprochen fragwürdige „Feature“ verwenden sollte, versteht sich von selbst. Was macht es dann im HTML-Standard? Meine historischen Ausgrabungen legen nahe, dass es sich um ein altes Feature aus dem Internet Explorer handelt. Anderer Browser mussten, um alte IE-only-Seiten zu unterstützen, das Feature unter erheblichem Zähneknirschen auch implementieren und zack – fertig ist der Webstandard.

Es ist wichtig anzumerken, dass dieses „Feature“ keine Gefahr für andere globale Variablen darstellt. Ein window.foo = 42 wird nicht durch ein Iframe mit name="foo" überschrieben und dem eingebauten window.navigator droht keine Gefahr durch <div id="navigator">. Es handelt sich eher um eine Kuriosität als ein Problem.

Boris und Reinhard fragen: Wie einen Offline-Modus für eine PWA bauen?

Unsere Anwendung soll als Progressive Web App offline funktionieren. Das Laden der App funktioniert dank eines Service Worker auch schon problemlos. Aber wie können wir herausfinden ob der Nutzer on- oder offline ist, um zu entscheiden, ob Daten lokal oder auf dem Server gespeichert werden sollten?

Ob ein Nutzer online oder offline ist, kann man gar nicht herausfinden bzw. die Begriffe „online“ und „offline“ sind hier nicht besonders hilfreich. Landläufig versteht man unter „online“ eine Verbindung zum Internet, aber als Nutzer einer App interessiert mich das Internet™ an sich nicht; was ich eigentlich möchte, ist durch das Internet eine Verbindung zu einem bestimmten Endpunkt aufbauen und mit diesem Endpunkt einen spezifischen Satz Daten austauschen. Nicht nur braucht also die PWA eine Internetverbindung, sondern auch der Ziel-Server muss funktionsfähig und verbunden sein. Und zu allem Überfluss muss die Verbindung auch dergestalt sein, dass der Datenaustausch auch stattfinden kann, darf also z.B. nicht zu langsam sein.

Im Angesicht eines Web-Requests lautet die eigentliche Frage also nicht „bin ich online?“, sondern vielmehr „klappt dieser Request zu diesem Zeitpunkt?“. Und diese Frage lässt sich nur beantworten, indem man versucht, den fraglichen Request (und nicht etwa einen Ping-Request) sofort (und nicht etwa nach einem Ping) durchzuführen.

Ich würde für meine PWA ein Offline-First-Datenspeichern bauen. Wann immer der Nutzer in der App Daten abzuspeichern versucht, würde ich diese zuerst im lokalen Speicher ablegen und danach einfach versuchen, sie zum Server zu senden. Wenn das klappt, können die lokalen Daten bei Bedarf gelöscht werden. Und wenn nicht, dann probiert man es später einfach nochmal.

Mathias fragt: Wie kann man fortgeschrittenes TypeScript verständlich halten?

Kennst du Strategien, um TypeScript möglichst lesbar und verständlich zu halten, wenn man fortgeschrittene Features wie Mapped Types und Generics verwendet?

Es stimmt, dass fortgeschrittene TypeScript-Features für Uneingeweihte ziemlich kryptisch daherkommen können und dass unsachgemäßer Eingriff viel kaputt machen kann … aber das gilt eigentlich für alle möglichen fortgeschrittenen Features in allen möglichen Programmiersprachen. Und dass fortgeschrittene Features für nichtfortgeschrittene Nutzer ein Problem darstellen, ist etwas, um das wir grundsätzlich nicht herumkommen. Entsprechend denke ich, dass sich hier „nur“ die üblichen Software-Entwicklungs-Maßnahmen für den Umgang mit Komplexität ergreifen lassen:

  1. YAGNI: ich versenke gelegentlich Zeit in die ausgefeilt-generische Typ-Formulierung von Dingen, die am Ende in der Codebase in genau einer Form zum Einsatz kommen. Das ist dumm und ich sollte das nicht tun. Und auch sonst niemand. Fortgeschrittene Features sind weniger häufig wirklich nötig, als man glaubt.
  2. Automatisierung: die Typinferenz vom TypeScript hilft sehr beim Einsatz von Generics. Intelligent ausgelegte Funktionen machen nicht nur ihren Job, sondern sind auch benutzerfreundlich und bei TypeScript ist hierbei die Typinferenz von Generics als ein zusätzliches Kriterium neben Funktionalität und Lesbarkeit zu berücksichtigen. Wenn das mal nicht klappt, weil die Typinferenz eine bestimmte Konstruktion einfach nicht versteht, hilft vielleicht Abstraktion weiter: vor einen mit Generics gespickten Funktionsaufruf könnte man 2-3 nicht-generische Fassaden stellen, die für alle tatsächlichen Use Cases ausreichen (siehe YAGNI).
  3. Abstraktion: eine gute Typ-Transformation ist wie eine gute Funktion, also generisch, gut getestet und dem UNIX-Prinzip folgend. Und wie eine gute Funktion muss eine gute Typ-Transformation, wenn einmal geschrieben, auch von jenen verwendet werden, die nicht in der Lage wären, die Funktion/Transformation zu schreiben. Sie können als Black Boxes einfach benutzt werden und stören im Arbeitsalltag nicht, vor allem nicht, wenn sie gut dokumentiert in einem eigenen Modul wohnen. Stichwort Modul …
  4. Modularisierung: es lohnt sich, eine Library von TypeScript-Transformationen aufzubauen, die auch Nichtnerds einfach benutzen können – quasi ein Lo-Dash für Typen. Mit type-zoo und typelevel-ts gibt es solche Libraries von Dritten, aber es spricht nichts dagegen, auch einen eigenen Typ-Werkzeugkoffer zusammenzustellen und ihn versioniert und dokumentiert griffbereit, aber aus dem Weg zu halten.

Meine persönliche TypeScript-Philosophie ist, dass ich fortgeschrittene Features nur dann verwende, wenn sie mir wirklich viel Arbeit ersparen oder signifikante Sicherheitszuwächse bieten. Und das ist nur gegeben, wenn die entsprechenden Codeschnipsel nicht im Weg herumstehen und dabei die Typinferenz aufmotzen. Abgefahrenes TypeScript sollte man nur schreiben, wenn man als direkte Folge weniger abgefahrenes (oder mühsames) TypeScript schreiben kann.

Daniel fragt: ist die Methoden-Syntax für Objekt-Literale gut oder böse?

Ich habe deinen Artikel gelesen, in dem es darüber ging, dass man function-Funktionen nicht nutzen sollten und finde ihn super! Aber wie sieht es bei Funktionen in Objekt-Literalen aus? Arrow Functions, Methoden-Syntax oder doch function-Keyword?

Für Unterbringung von Funktionen im Objekt-Literal gibt es drei Möglichkeiten:

const obj = {
  foo () { return 42; },
  bar: () => { return 42; },
  baz: function () { return 42; },
}

Von diesen drei Kandidaten würde ich die Arrow-Function-Variante bar zuallererst verwerfen. Funktionen auf Objekten sollten im Normalfall schon als Methoden nutzbar sein und das klappt nicht mit dem lexikalischen this der Arrow Functions. Die verbleibenden Kandidaten foo () {} und baz: function () {} führen, wie der Babel-Output zeigt, zum gleichen Ergebnis, beide sind in letztendlich normale function-Funktionen (und müssen das auch sein).

Und was nimmt man nun? Ich persönlich greife zur Methoden-Syntax foo () {} weil sie einfach kürzer als das function-Keyword und in vielen Fällen weniger falsch als die Arrow-Function-Variante ist. Die Methoden-Syntax erlaubt außerdem die vollumfängliche Verbannung des function-Keyword aus meinem Code, was im Sinne der Code-Konsistenz vielleicht noch das stärkste Argument ist.

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.