Man kann nicht kein JavaScript-Framework verwenden

Veröffentlicht am 7. November 2016

Ich war vor ein paar Wochen bei einer Firma zu Besuch, die für eines ihrer Produkte ein neues Webfrontend plant. Der JavaScript-Framework-Gretchenfrage stehe ich persönlich recht teilnahmslos gegenüber. Mein Eindruck ist, dass keins der populären Frameworks wesentlich schlimmer ist als die anderen populären Kandidaten. Entsprechend nutzlos ist es meistens, mich zu fragen, ob denn jetzt Angular, React, Aurelia oder Vue so sehr viel besser wäre. Aber im Fall dieser Firma hatte ich zu dieser Frage eine Meinung.

In besagter Firma gab es bei diesem Framework-Streit eine Fraktion, die für eine sogenannte „No-Framework“-Variante agitierte. Die Argumentation dieser Fraktion war, dass man sich sehr viel Lernaufwand und Stress sparen könnte, indem man einfach gar kein Framework verwendet, sondern sich ganz auf jQuery und native Features besinnt. Besonders dem wild vor sich hinmutierenden JS-Ökosystem stand man etwas skeptisch gegenüber und fürchtete sich vor bald schon nicht mehr gepflegten Frameworks und Dependencies. Dem Problem wollte man sich entziehen, indem man einfach kein Framework verwendet, dem die Weiterentwicklung versagt bleiben könnte.

Das Problem von „No-Framework“ ist, dass es diesen Ansatz für moderne Frontend-JS-Webapps gar nicht gibt und es etwas unaufrichtig ist, ihn als Null-Kosten-Alternative (Kosten im Sinne von Risiko, Einarbeitungszeit etc.) den populären JS-Frameworks gegenüberzustellen.

Was ist ein Framework? Ein Framework besteht aus Konventionen und Software, die bei der Arbeit im Rahmen dieser Konventionen assistiert. Verwendet man ein Framework, nimmt einem dieses im Idealfall die Arbeit an diesen zwei Punkten ab – Konventionen werden vorgegeben und passende Software wird bereitgestellt. Was passiert, wenn man kein Framework verwendet? Dann werden sich trotzdem Konventionen und Software herausbilden, die dann die Rolle des Frameworks übernehmen. Wer Programmcode schreibt, setzt naturgemäß Konventionen um und schafft Struktur und erzeugt über kurz oder lang sein eigenes Frameworks. Vielleicht ist dieses Framework ein dokumentiertes, getestetes Software-Modul, vielleicht ist aber nur ein Satz an über das Projekt verteilten, selbstgestrikten Funktionen mit Glue Code. Vielleicht sind die Konventionen in einem Styleguide festgehalten, vielleicht sind sie aber auch nur geheimes Templerwissen der wenigen Eingeweihten. Aber vorhanden sind Konventionen und Software in jedem Fall.

So gesehen kann man also nicht kein Framework verwenden. Man kann sich höchstens selbst eins schreiben. Macht man das gut, schreibt man ein explizites, getestetes, dokumentiertes Stück Software. Macht man es weniger gut, hat man sich am Ende trotzdem ein eigenes Framework geschaffen – nur eben ein implizites, das zwischen den eigentlichen Codezeilen lebt. Wer „No-Framework“ sagt, meint damit tatsächlich „ich schreibe mir selbst ein Framework“. Ich will auch gar nicht ausschließen, dass das unter entsprechenden Voraussetzungen eine gute Wahl sein kann. Nur wenn man die Nachteile der populären JS-Frameworks auflistet muss man diese gegen die Nachteile einer Eigenentwicklung (Zeitaufwand, Risiko usw.) aufwiegen und nicht einfach behaupten, man könnte auf Konventionen und Struktur verzichten oder die Entwicklung von Konventionen und Struktur wäre ein Selbstläufer.

Ein ironiefreier Anwendungsfall für new Number() in JavaScript

Veröffentlicht am 2. November 2016

Obwohl ich nicht erst seit gestern JavaScript schreibe, gibt es noch ein paar „Features“ in der Sprache, die ich nie in ernst gemeintem Code eingesetzt habe. Dabei gehöre ich nicht mal zur Spanische-Inquisition-Fraktion, die jeden, der mal new Function() geschrieben hat, in den Kerker werfen möchte – wenn etwas funktioniert und in einem speziellen Fall keine Nachteile hat, dann verwende ich es! Nur new Number() hatte ich noch nie eingesetzt … bis vor kurzem.

Wir erinnern uns: JavaScript kennt Wrapper-Objekt für die Datentypen String, Number und Boolean. Diese werden mit new String(x), new Number(x) und new Boolean(x) erzeugt. Durch diese Funktionsaufrufe wird der Parameter x in den jeweiligen Datentyp konvertiert (also zu String, Number und Boolean, je nachdem) und in ein Wrapper-Objekt eingepackt. Ohne new machen die Funktionen nur die Konvertierung und erzeugen keinen Wrapper. Das ist eigentlich immer die sinnvollere Variante, denn die Wrapper-Objekte sind zu nichts gut:

  • Die Wrapper-Objekte bieten keine zusätzliche wünschenswerte Funktionalität. Methoden wie z.B. toExponential() bei Number können auch auf den primitiven Werten verwendet werden – diese verhalten sich diesbezüglich immer wie ein Objekt, auch wenn sie selbst keins sind (denn sie wissen, was sie für ein Objekt sie wären, wenn sie eins wären).
  • Der typeof-Operator identifiziert jedes Wrapper-Objekt (egal ob für String, Number oder Boolean) als object, was korrekt, aber nicht hilfreich ist.
  • Alle Wrapper-Objekte sind ausnahmelos truthy, keins ist jemals falsy. Ja, new Boolean(false) ist truthy, denn alle Objekte sind truthy, auch wenn sie ein false wrappen oder einfach nur leer sind.

Demnach sollte eigentlich niemand jemals new Number() verwenden. Aber kürzlich sah ich mich mit folgender API eines Third-Party-Moduls konfrontiert:

connectToServer({
  reconnectionAttempts: <number>
});

Mein Ziel war, einmal einen Verbindungsversuch zum Server zu unternehmen und nach erstmaligem Fehlschlagen sofort aufzugeben, um die Applikation in den permanenten Offline-Modus zu versetzen. Ich wollte also genau 0 reconnectionAttempts haben. Die Implementierung der connectToServer()-Funktion sah allerdings so aus:

export default function connectToServer(options){
  if(!options.reconnectionAttempts){
    options.reconnectionAttempts = 9000;
  }
  ...
}

Dieser Code lässt 0 reconnectionAttempts nicht zu! Der Wert 0 wäre falsy, was dazu führen würde, dass die Bedingung if(!options.reconnectionAttempts) zutrifft und reconnectionAttempts mit irgendeinem sehr hohen Standardwert ersetzt wird:

connectToServer({
  reconnectionAttempts: 0 // tatsächlich 9001 Verbindungsversuche
});

Das ist natürlich ein Bug im Code der connectToServer()-Funktion. Bis dieser repariert ist, ist new Number(0) eine gute Zwischenlösung, denn das Wrapper-Objekt ist truthy, taugt aber trotzdem auch als Zahl 0. Wo immer etwas zahlenhaftes mit einem Number-Objekt unternommen wird (Addition, nicht-strikter Vergleich usw.) wird die gewrappte Zahl aus dem Objekt mittels valueOf() „ausgepackt“.

connectToServer({
  reconnectionAttempts: new Number(0) // Wirklich kein zweiter Verbindungsversuch!
});

Die Nachteile des Wrapper-Objekts werden an dieser Stelle zum Vorteil! Es könnten freilich auch Dinge schiefgehen – ein strikter Vergleich new Number(0) === 0 wäre z.B. false, aber für das, was ich mit besagter Library vorhabe, funktioniert es.

Als bizarres Extra kommt in meinem Fall noch hinzu, dass ich TypeScript statt JavaScript verwende und das TS-Typsystem (berechtigterweise) ein Problem damit hat, wenn man ein Number-Objekt dorthin schiebt, wo ein Number-Primitive erwartet wird. Also ergänze ich meinem Code noch durch eine Type Assertion, die dem Typsystem einredet, das Number-Objekt sei ein Number-Primitive. Im Endeffekt sieht der Code schon so aus, als hätte hier jemand unter dem Einfluss potenter Drogen gestanden:

connectToServer({
  // Mit dem Brecheisen wird ein an sich nutzlos-gefährlicher Wrapper
  // um eine 0 in eine API gesteckt, die das alles nicht haben will.
  reconnectionAttempts: new Number(0) as number
});

Bis eines fernen Tages mal die connectToServer()-Funktion repariert ist, ist das meine eigentlich ganz zufriedenstellende Zwischenlösung. Was lernen wir daraus?

  1. Es lohnt sich, auch die bizarreren oder gar gefährlichen Teile einer Programmiersprache gut zu kennen. Sei es, um genau zu wissen, warum man sie nicht verwendet oder, wenn es hart auf hart kommt, um sie einmal im Jahr doch aus dem Keller zu holen um ein ganz bestimmtes Problem zu lösen.
  2. Schräg aussehender Code kann zwei Ursachen haben: entweder weiß jemand überhaupt nicht, was er da schreibt, oder dieser jemand weiß sehr genau Bescheid. Die letztgenannte Variante, so selten sie auch sein mag, darf man bei der Code-Lektüre nie ganz ausschließen.

Bleibt zu hoffen, dass der Bug in der connectToServer()-Funktion bald repariert wird.

Script-Elemente von einem Dokument ins nächste importieren

Veröffentlicht am 8. September 2016

Dieser Artikel befasst sich mit einem Problem aus den Randbezirken der täglichen Praxisrelevanz, aber da ich daran ganz schön herumtüfteln musste, kann die Nachwelt sich vielleicht nach der Lektüre der nächsten paar Zeilen ein wenig Arbeit ersparen. Das Problem tauchte auf, als ich einen Bug in meiner kleinen Web Component namens html-import bemerkte. Die Komponente verwende ich, um in meinen HTML-basierten Präsentationen eine clientseitige Importfunktion umzusetzen, ohne dass ich JavaScript zu schreiben brauche. In einer gegebenen Präsentation A möchte ich buchstäblich <html-import src="praesentationB.html#SlideIdFoo"> schreiben können, statt Copy & Paste zu betreiben. Die Komponente funktioniert wie folgt:

  1. Die im src-Attribut angegebene URL wird mit fetch() geladen
  2. Der HTML-Content wird in ein neues Dokument geparsed
  3. Entweder der komplette Inhalt des Dokuments oder ein bestimmtes Element werden in das importierende Dokument importiert (document.importNode())
  4. Der importierte Inhalt wird im Dokument nach dem importierenden <html-import>-Element eingehängt

Dazu kommt allerlei Hexerei für ein Promise auf dem <html-import>-Element und rekursive Imports. Und alles funktionierte ganz hervorragend, bis mir auffiel, dass importierte <script>-Elemente im Firefox nicht ausgeführt wurden, während sie in Chrome funktionierten …

Nach längerer Recherche kann ich nur vermuten, dass Firefox ganz einfach aus Sicherheitsgründen streikt. Ich habe beim Durchforsten der HTML5- und DOM-Spezifikationen nicht herausgefunden, ob das vorgeschrieben oder zulässig ist, aber es scheint einfach so zu sein, dass der Firefox das aus einem fremden Dokument importierte Script nicht vertrauenswürdig findet. Was also tun?

Firefox hat ein Problem mit dem importierten Script-Element, aber nicht mit dem Script-Inhalt. Die Lösung besteht also darin, ein neues Script-Element im importierenden Dokument zu erstellen, den Script-Inhalt (mit der text-Property, nicht mit innerHTML) und/oder das src-Attribut vom Original im Fremd-Dokument zu übernehmen und das neue Script nach dem Original im Original-Dokument einzufügen. Dann funktioniert das Script im Firefox, läuft aber in Chrome zweimal (denn dort funktionieren Original und Klon gleichermaßen). Hier gibt es nun zwei Möglichkeiten:

  1. In meinem Fall, in dem ich Inhalt aus einem Fremd-Dokument übernehme, kann ich ganz einfach das Original-Script aus dem Dokument heraushalten. Nach dem Anlegen des Klons wird das Original einfach verworfen statt ins Dokument eingehängt.
  2. Falls das mal nicht so einfach möglich sein sollte, kann man das Original-Script auch deaktivieren. Hierzu einfach als type-Attribut etwas angeben, das nicht auf der HTML5-Liste der Script-MIME-Types steht.

Das neue Script-Element wird asynchron ausgeführt, aber das wäre beim direkten Import des Originals nicht anders.

Wie sich die ganze Angelegenheit in IE und Safari o.Ä. darstellt, habe ich noch nicht getestet. Auffällig fand ich, dass das für mich das erste Mal seit langem war, dass ich eine Frage nicht durch irgendeine Spezifikation zumindest teilweise beantwortet bekommen habe. Normalerweise kann man sich heutzutage darauf verlassen, dass man in HTML5 u.Ä. zumindest so etwas wie einen klar definierten Soll-Zustand vorfindet. Empirische Browser-Forschung ist jedenfalls bei mir zum Ausnahmefall geworden. Das war früher mal gaaanz anders …

Fragen zu HTML5 und Co beantwortet 23 - Semikolons, Flexbox-Breiten, Formulare

Veröffentlicht am 23. August 2016

Wieder haben mich viele neue Fragen zu HTML5, CSS3 und JavaScript über die diversen Kanäle erreicht. Dass es nicht zu mehr Blogposts kommt, ist allein meine Schuld … aber genug gejammert, ran an die neusten Leserfragen!

Unterschied width/flex-basis

Was ist bei einem Flexbox-Layout der Unterschied zwischen width und flex-basis? Gibt es überhaupt einen?

Beim Bau eines 0815-Spaltenlayouts ist es so gut wie egal, ob man width oder flex-basis verwendet – bei der einfachen Anordnung von Boxen nebeneinander führen beide Eigenschaften zum gleichen Ergebnis. Allerdings gibt es durchaus Unterschiede, die je nach Use Case durchaus ins Gewicht fallen können:

  • Vielleicht offensichtlich, aber dennoch erwähnenswert: flex-basis funktioniert nur im Flexbox-Kontext, width greift hingegen immer. Wenn man eine wiederverwertbare Komponente gestaltet, die mal mit und mal ohne Flexbox verwendet werden soll, ist das ein Argument für width.
  • Der Effekt von flex-basis ist von der gewählten flex-direction abhängig. Während width immer die horizontale Ausdehnung eines Elements steuert, kann flex-basis, wenn flex-direction auf column oder column-reverse steht, für die Höhe zuständig sein.
  • Während width auch bei absolut positionierten Elementen funktioniert, ist das bei Flex-Items nicht der Fall
  • Die praktische Abkürz-Eigenschaft flex fasst flex-basis mit flex-shrink und flex-growzusammen. Für width gibt es nichts Entsprechendes.

Hier eine kleine Demo der Gemeinsamkeiten und Unterschiede.

Semikolons nach Funktionsdeklarationen

Warum muss ich in JavaScript bei Funktionsdeklarationen kein Semikolon verwenden? Ich weiß Semikolons sind optional, aber bei Funktionsdeklarationen sehe ich so gut wie nie Semikolons. Warum ist das so?

Die ECMAScript-Spezifikationen verlangen tatsächlich nach Funktionsdeklarationen kein Semikolon (wenn eins da ist, stört es aber nicht). Zur Einordnung: das hier ist eine Funktionsdeklaration …

function foo(){
  return 42;
}

… und im Vergleich dazu ein Funktionsausdruck, nach dem man (wenn man mal die automatische Semikolon-Einfügung ignoriert) ein Semikolon haben sollte:

var foo = function(){
  return 42;
};

Und warum ist das so? Semikolons trennen in JavaScript Statements voneinander. Funktionsdeklarationen sind aber keine Statements, sondern fallen in die Kategorie Declaration. Der beobachtbare Hauptunterschied ist, dass Funktionsdeklarationen evaluiert werden, bevor das Script tatsächlich ausgeführt wird. Das kann man daran erkennen, dass Funktionsdeklarationen aufgerufen werden können, bevor sie im Code definiert werden, was mit einem Funktionsausdruck nicht klappt:

// Function Declaration
foo(); // Klappt
function foo(){
  window.alert(23);
}

// Function Expression
bar(); // Klappt nicht
var bar = function(){
  window.alert(42);
};

Lange Rede, kurzer Sinn: der Job, den Semikolons in JavaScript machen, wird von Funktionsdeklarationen nicht benötigt.

Formulardaten lokal speichern

Ich möchte in ein Formular eingegebene Daten lokal speichern. Muss ich dafür ein jQuery-Plugin benutzen oder geht das auch mit HTML5-Bordmitteln?

Mit FormData gibt es eine recht einfache API um die eigegebenen Daten aus einem Formular zu extrahieren. Das von new FormData(someFormElement) zurückgegebene Element hat eine entries() Methode, die einen Iterator über die Name-Wert-Paare der Formulardaten zurückgibt – und von da aus ist der Weg zum speicherbaren JSON nicht mehr weit:

document.querySelector("input[value=Speichern]").addEventListener("click", () => {
  // Iterator mit [name, value]
  const data = new FormData(document.querySelector("form")).entries();
  // Daten als Objekte in einem Array
  const serialized = Array.from(data, ([name, value]) => ({ name, value }));
  // JSON, bereit zum Speichern in DOM Storage!
  const json = JSON.stringify(serialized);
  console.log(json);
});

So einfach kann es sein! Wichtig ist: FormData-Objekte können ohne Serialisierung direkt von XMLHttpRequest.send() verschickt werden und die IndexedDB kann zumindest die aus dem Iterator erzeugten Arrays speichern, ganz ohne JSON.

Warum funktioniert mein Custom Error nach dem Submit-Event nicht?

Ich habe ein Formular gebaut, das beim Submit-Event ein paar Dinge validiert und im Fehlerfall mit setCustomValidity() HTML5-Validierungsfehler auf den Feldern auslösen soll. Das scheint aber in keinen Browser zu funktionieren. Was ist da los?

Wenn man mit setCustomValidity() Felder als ungültig markieren möchte, muss man das vor dem Submit-Event erledigen. Der Sinn von setCustomValidity() ist das Festlegen des Fehler-Zustandes eines Feldes, nicht an Anzeigen der Fehlermeldung– das erledigt der Browser beim bzw. kurz vor dem Abschicken des Formulars selbst. Ein kleiner Auszuug aus dem Form submission algorithm von HTML5:

When a form element form is submitted from an element submitter (typically a button), optionally with a submitted from submit() method flag set, the user agent must run the following steps:

  1. If [...] the constraint validation concluded that there were invalid fields and probably informed the user of this [...] fire a simple event named invalid at the form element and then abort these steps.
  2. [...] then fire a simple event [...] named submit, at form.

Man sieht: die Validierung findet vor dem Submit-Event statt. Deshalb sollte sollte setCustomValidity() dann passieren, wenn sich der Inhalt eines Feldes ändert (z.B. bei change- oder keyup-Events).

Weitere Fragen?

All diese Fragen wurden mir per E-Mail oder Twitter gestellt und auch eure Fragen zu HTML(5), CSS(3), JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach über einen der genannten Kanäle anschreiben oder gleich das komplette Erklärbären-Paket kommen lassen.