Finger weg vom function-Keyword!

Veröffentlicht am 14. August 2018

Das function-Keyword ist in modernem JavaScript ein Code Smell und man sollte es nicht mehr verwenden. Es einzusetzen ist nicht direkt ein schlimmer Fehler, aber seine Nutzung steht meines Erachtens unter dringendem Rechtfertigungsdruck. function-Functions haben fast keine wünschenswerten Fähigkeiten, dafür allerhand Legacy-Anhängsel, mit denen man sich eigentlich nicht mehr herumschlagen möchte. Stattdessen sollte man in so gut wie jedem Fall zu Arrow Functions oder zur Klassensyntax greifen.

Warum function-Functions mal nützlich waren

Dass JavaScript lange Jahre überhaupt zu gebrauchen war, lag unter anderem daran, dass function-Functions so unglaublich vielseitig sind. In Abwesenheit anderer Features (wie Klassen) übernahmen function-Functions in althergebrachtem JS gleich vier Rollen auf einmal:

  • Als normale Funktion foo() verwendet fungieren function-Functions als normale Funktionen bzw. Prozeduren
  • Mit new aufgerufene function-Functions funktionieren als Constructor-Funktionen
  • Mit new aufgerufene function-Functions dienen, wenn ihre prototype-Eigenschaft entsprechend bestückt sind, als eine Art Klassendeklaration (d.h. als der Ort, in dem Objekt-Methoden gesammelt werden)
  • Als Property eines Objekts aufgerufene function-Functions (z.B. obj.foo()) fungieren als Methode dieses Objekts.

Bemerkenswert, was ein einzelnes Sprachkonstrukt so alles leisten kann! Je nachdem wie ein Funktionsaufruf formuliert wird, kann die Funktion verschiedene Rollen einnehmen. Das Ganze funktioniert (unter anderem) indem jede Funktonsaufrufformulierung den Wert der in jeder Funktion verfügbaren magischen Variable this ändert. Aber genau damit fangen die Probleme von function-Functions an.

Die Nachteile von function-Functions

Der größte Haken an function-Functions ist, dass ihr Verhalten von der Formulierung des Funktionsaufrufs abhängt! Das gleiche Funktonsobjekt kann als foo(), obj.foo() und new foo() aufgerufen werden, obwohl es vermutlich für exakt einen dieser Einsatzzwecke ausgelegt wurde. Das Problem lässt sich einhegen, indem man den Strict Mode verwendet und in seine Funktionen Code einbaut, der die nicht eingeplante Aufrufvarianten entweder unterstützt oder mit Exceptions quittiert. Im besten Fall entsteht dabei unnötiger, fehleranfälliger Boilerplate-Code und im schlimmsten Fall macht sich niemand die Mühe.

// Diese Mühe machen sich die Wenigsten
function MyClass () {
  if (!(this instanceof MyClass)) {
    throw new Error("'new' fehlt");
  }
}

// Diese Mühe macht sich niemand
function myFunc () {
  if (this && this !== window) {
    throw new Error("Keine Klasse oder Methode");
  }
}

Ein weiterer Nachteil: selbst wenn man sich von OOP und Vererbung fernhält, muss man sich als Autor von function-Functions immer noch mit der Existenz von OOP-Features herumschlagen. Auch als normale Funktionen auslegte Funktionen haben, wenn sie function-Functions sind, noch immer die klassischen JS-OOP-Features this und prototype im Gepäck. Gleiches gilt für lästige Legacy-Anhängsel wie arguments, die man im Angesicht moderner Alternativen einfach nicht mehr braucht. In function-Functions sind sie aber stets verfügbar, nur in Arrow Functions nicht.

Zu guter Letzt kommen function-Functions in zwei Varianten daher: Funktionsdeklaration und Funktionsausdruck.

// Funktionsdeklaration
function foo () {}

// Funktionsausdruck
const foo = function () {}

Diese beiden Definitionen einer Funktion namens foo sind fast, aber nicht exakt gleich, da nur Funktionsdeklarationen gehoisted werden. Dadurch können sie aufgerufen werden, bevor sie im Code vorkommen. Ein wirklich notwendiges Feature ist das nicht, aber es ist eine weitere valide (überflüssige) Funktionsvariante mit subtilen Eigenheiten, deren Existenz wertvolle Gehirnkapazität belegt. Aber das muss alles nicht sein!

Alternativen zur function-Function

Statt sich mit den komplizierten function-Functions herumzuschlagen kann man sich mit gezielter Wahl alternativer Sprachmittel das Leben sehr viel leichter machen. So sind beispielsweise für „normale“ Funktionen Arrow Functions das eigentliche Mittel der Wahl. Sie haben kein eigenes this und können daher nicht als Objekt-Methode missbraucht werden. Die prototype-Eigenschaft fehlt und ein Aufruf-Versuch via new wird mit einer Exception quittiert.

const myFunc = () => {
  console.log("this", this);
  console.log("arguments", arguments);
};

myFunc.prototype;
// > undefined

myFunc();
// > "this" window {}
// > ReferenceError: arguments is not defined

new myFunc()
// > TypeError: myFunc is not a constructor

Eine Arrow Function ist eine wahre Funktion, nichts anderes – und die Befreiung von Legacy-Features wie arguments ist inklusive

Wer statt einfacher Funktionen eher Objekte und Methode braucht, ist mit einer Klasse am besten beraten. Nicht nur herrscht in Klassen standardmäßig Strict Mode, auch führt ein Aufruf ohne new zu einer Exception. In den Klassen notierte Methoden profitieren ebenfalls vom Strict Mode und können nur als Objekt-Methoden aufgerufen werden, ansonsten hat ihr this den Wert undefined:

class MyClass {
  foo () {
    console.log(this);
  }
}

const instance = new MyClass();
const foo = instance.foo;

instance.foo();
// > MyClass {}

foo();
// > undefined

MyClass()
// > TypeError: Class constructor MyClass cannot be invoked without 'new'

Klassen haben zwar wie function-Functions eine Deklarations- und eine Ausdruckssyntax, aber da erstere nicht gehoisted wird, ist das wirklich ein rein syntaktisches Detail:

// funktioniert nicht
new FooExpression();
const FooExpression = function () {};

// funktioniert!
new FooDeclaration();
function FooDeclaration () {}

// funktioniert nicht
new BarExpression();
const BarExpression = class {};

// funktioniert auch nicht
new BarDeclaration();
class BarDeclaration {}

Es zeigt sich: Klassen und Methoden sind präzise Werkzeuge um Objekte und ihre Methoden zu formulieren – und nichts anderes!

Verbleibende Use Cases für function-Function

Es gibt nach meinem Kenntnisstand zwei Fälle, in denen function-Functions das Mittel der Wahl sind. Der erste Fall betrifft TypeScript, wo die Syntax das Überladen der Typsignaturen von Funktionsdeklarationen (d.h. function-Functions), nicht aber von Arrow Functions zulässt:

// Überladen ist mit Arrow Functions nicht möglich
function foo <T> (input: T[], selector: (item: T) => 0 | 1): [ T[], T[] ];
function foo <T> (input: T[], selector: (item: T) => 0 | 1 | 2): [ T[], T[], T[] ];
function foo <T> (input: T[], selector: (item: T) => number): T[][] {
  // Implementierung
}

Fall zwei ist das Patchen von Prototypen. Hier braucht es eine Funktion, die mit this umgehen kann, aber außerhalb einer Klasse formuliert werden kann. Das kann nur eine function-Function sein:

SomeClass.prototype.newMethod = function () {
  // Implementierung
};

Letzteres ist schon ziemlich nah an der Grenze zum Hack angesiedelt. Unter Umständen nützlich bzw. nötig, aber ganz sicher kein Alltags-JavaScript.

Fazit

function-Functions sind an sich keine Katastrophe. Da sie aber viele verschiedene Use Cases auf einmal abdecken und diverse Legacy-Features mit sich herumschleppen, während es gleichzeitig pro Use Case eine einfachere, spezifischere Funktionssyntax ohne Legacy-Feature gibt, gibt es kaum noch einen Grund, function-Functions einzusetzen! In so gut wie jedem Fall sind Arrow Functions oder Klassen die bessere Wahl, da sie für ihre spezifischen Use Cases die spezifischeren Werkzeuge sind und sich in der Verwendung als weniger fehleranfällig erweisen. In heutigem JavaScipt steht jede function-Function unter Rechtfertigungsdruck.

Service Worker und Notification-Icons im Offline-Modus

Veröffentlicht am 17. Juli 2018

Viele Features in Service Workers wurden nicht extra neu spezifiziert, sondern sind Adaptionen existierender Standards. So ist z.B. Message Passing ein in jedem Browser schon vorhandenes Feature, das einfach auch in Service Workers zur Verfügung gestellt wird. Warum würde man das Rad auch neu erfinden, wenn man doch einfach vorhandene Funktionalität übernehmen kann! Allerhöchstens werden beim Vorliegen triftiger Gründe Teile eines Features nicht unterstützt. Das sehr praktische URL-Objekt ist im Service Worker verfügbar, wenn auch ohne die Methoden createObjectURL() und revokeObjectURL(), in denen die Spec-Schreiber nur schwer lösbare Garbage-Collection-Probleme für Service Worker sehen. Das ist auch recht gut nachvollziehbar. Eine Object-URL ist eine URL auf ein JavaScript-Objekt und damit, als theoretisch ewig gültige Referenz, schon in normalen Scripts nur schwer korrekt einzusetzen. Da sich das Problem im Kontext des Service-Worker-Lifecycle erheblich vergrößern würde, wird auf Object-URL-APIs einfach verzichtet. Schön und einfach für die Specs, aber, wie sich zeigen wird, nicht so schön und einfach für die Webapps!

Die Notification-API für per JS angestoßene native Notifications wurde ebenfalls in den Service Worker übernommen und funktioniert dort fast wie in normalen Websites auch. Wo man normalerweise einfach einen Constructor aufruft …

// In normalem JavaScript
const myNotification = new Notification("Master caution", {
  body: "Main B bus undervolt!",
  icon: "img/icon192.png",
});

… hat man im Service Worker eine Methode auf der aktuellen SW-Registration zur Verfügung …

// In Service-Worker-Code
self.registration.showNotification("Master caution", {
  body: "Main B bus undervolt!",
  icon: "img/icon192.png",
});

… aber das Grundprinzip ist identisch: einen Titel, einen Text und eine URL zu einem Icon angeben und schon taucht eine Notification auf! Allerdings gibt es beim Punkt „URL zu einem Icon“ ein handfestes Problem.

Service Worker haben viele Use Cases, aber der wichtigste ist sicher, Webapps offline zum Funktionieren zu bringen. Der Service Worker klemmt sich dazu als clientseitiger Proxy zwischen die Webapp und das WWW und ist in der Lage, von der App abgesetzte Requests auf die eine oder andere Weise zu beantworten.

Für Offline-Support würde der Service Worker eingehende Requests aus seinem Offline-Cache beantworten. Wenn eine Ressource mal nicht im Cache ist oder sich ein Request nicht sinnvoll offline abbilden lässt, kann der Service Worker die Anfrage aber auch einfach aus dem WWW beantworten. Aus dem Cache kommt eine Ressource, wenn sie dort über die entsprechende JS-API herausgekramt wird. Aus dem WWW wird eine Ressource geladen, wenn der Request im Service Worker über eine der dafür üblichen APIs abgesetzt wird. Das wäre z.B. fetch() oder aber auch …

self.registration.showNotification("Master Caution", {
  body: "Main B bus undervolt!",
  icon: "img/icon192.png",
});

Houston, wir haben ein Problem! Die Notification-API nimmt für Icons nur URL-Strings entgegen. Diese URLs führen immer zu WWW-Requests, werden also auch im Offline-Betrieb garantiert nicht aus dem Cache bedient, selbst wenn die entsprechenden Icons dort lagern (denn Reqests aus dem Service Worker heraus führen immer ins Web, nie in den Cache). Eine URL auf eine aus dem Cache geladene Ressource lässt sich auch nicht so einfach basteln, da ja URL.createObjectURL() im Service Worker aus gutem Grund nicht verfügbar ist.

Alles verloren? Nicht ganz! Zwar ist URL.createObjectURL() nicht verfügbar, aber es gibt auch noch Data-URLs. Der Unterschied: während eine Object-URL ein Daten-Objekt referenziert, enthält eine Base64-codierte Data-URL die Daten selbst! Es gibt also keine Garbage-Collection-Komplikationen. Eine Data-URL auf einen Blob lässt sich mit der extrem archaischen FileReader-API erzeugen:

const reader = new FileReader();
reader.onloadend = () => { /* reader.result verwenden */ };
reader.readAsDataURL(blob);

Da die FileReader-API dem kreidezeitlichen XMLHttpRequest-Objekt ähnelt, lohnt es sich, sie hinter einem Promise zu verbergen. Die folgende Funktion nimmt eine URL entgegen und liefert, wenn es für die URL einen Cache-Eintrag gibt, ein Promise auf eine Data-URL mit dem Cache-Eintrag als Inhalt zurück. Gibt es keinen Cache-Eintrag für die URL, liefert das Promise die Input-URL zurück:

const asCacheUrl = (url) => {
  return new Promise( async (resolve) => {
    const response = await caches.match(url);
    if (!response) {
      return resolve(url);
    }
    const blob = await response.blob();
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.readAsDataURL(blob);
  });
}

Eine hilfreiche Ergänzung ist die folgende Funktion, die für ein Array von URLs ein Promise auf ein Array von Data-URLs liefert:

const asCacheUrls = (urls) => Promise.all(urls.map( (url) => asCacheUrl(url) ));

Das Ganze in eine Notify-Funktion eingebaut und schon haben auch im Offline-Modus abgesetzte Notifications Icons und Bilder!

const notify = async (title, data = {}) => {
  if (self.registration && self.Notification.permission === "granted") {
    const [ icon, badge ] = await asCacheUrls([
      "img/icon192.png", "img/badge.png"
    ]);
    const options = Object.assign({ icon, badge, }, data);
    const notification = self.registration.showNotification(title, options);
    return notification;
  }
}

Schon sieht es gar nicht mal mehr so schlimm aus!

Diese Funktion holt nur für die Felder icon und badge die Daten aus dem Cache. Um das Ganze für alle in Notifications verwendbaren Ressourcen durchzuführen, müsste ein Script sämtliche Felder des Optionen-Objekts von Notifications untersuchen und für die relevanten Einträge die Ressourcen aus dem Cache fischen. Ob es sich lohnt, eine ausgefeilte Library zum Lösen dieses Problemchens zu stricken, oder ob wir lieber warten sollten, bis sich die Spezifikationen darum kümmern, sei dahingestellt.

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.