Erfahrungsbericht: Ein Workflow für ECMAScript 6

In den letzten Wochen habe ich an einem größeren JavaScript-Projekt herumgeschraubt, das ich versuchsweise mit so viel ECMAScript 6 wie möglich bestritten habe. Es ging mir einseits darum, mit dem neuen JavaScript-Standard echte, praktische Erfahrung zu sammeln und andererseits einen möglichest runden Workflow zu erarbeiten. Während ersteres sehr gut funktioniert hat (mein Gehirn leidet mittlerweile, wenn es ES5 schreiben muss) war letzteres sehr mühsam und tendenziell auch eher so mittel-erfolgreich. Wobei das auch daran liegen mag, dass ich neben ES6 auch Gulp und Browserify zu Felde geführt habe und mir vielleicht nur diese spezielle Technologie-Cocktail so auf den Magen schlägt.

Die folgenden Zeilen sollen kurz berichten, mit welchen Tools ich meinen ES6-Code für aktuelle Browser aufbereite, welche Alternativen es gibt und warum ich mich für welche Tool-Kombination entschieden habe. Ich würde jedem, der ein ähnliches Experiment vorhat, dringend empfehlen selbst ein wenig die Transpiler-Landschaft zu erkunden, denn die verschiedenen Tools unterscheiden sich durchaus recht stark.

Warum überhaupt schon ES6?

ECMAScript 6 ist noch kein in Stein gemeißelter, fertiger Standard, aber wenn man die Entwicklung verfolgt hat, ist klar, dass inhaltlich eigentlich alles gesagt worden ist. Neue Features werden bereits für ECMAScript 7 verplant und an Version 6 ändert sich schon länger nichts gravierendes mehr. Vieles in ES6 ist nicht wirklich neu, sondern fällt eher in die Kategorie „syntaktischer Zucker“. Destructuring ist das beste Beispiel:

// ES6-Code
var arr = [23, 42]
var [a, b] = arr;

// ES5-Entsprechung
var arr = [23, 42];
var a = arr[0],
    b = arr[1];

Auch Klassen sind nicht mehr als eine andere für Constructorfunktionen nebst Prototypen-Setup und selbst Generators lassen sich im Prinzip mit heutigem JavaScript abbilden. Und natürlich gibt es Tools, die ES6-Code nach ES5-Code übersetzen, so dass man heute schon große Teile von des neuen Standards nutzen kann, wenn man sich nur ein wenig anstrengt.

Den Sinn hinter dieser Übung sehe ich vor allem in der Gewöhnung an all die neuen Werkzeuge. Zwar sind ES6-Klassen im Prinzip exakt das gleiche wie Constructorfunktionen, aber in der Benutzung eben doch nicht ganz. Arrow Functions sind normale Funktionen mit lexikalischem this, sehen aber im Code aus wie von einem anderen Stern. Alte Bekannte wie var und function verschwinden ggf. völlig aus JavaScript-Code und zumindest bei mir dauert der Prozess, der Generators in meinen aktiven JavaScript-Wortschatz überführt, noch immer an. Es ist also alles in ES6 ziemlich anders und man muss sich dran gewöhnen.

Allerdings reden wir hier immer noch über experimentelle Technologie und ist kein völliger Selbstläufer, sich hierfür einen Workflow einzurichten. Ich bin zu einem für mich brauchbaren Ergebnis gekommen, aber andere haben sich komplett andere Workflows zusammengestrickt. Es ist also keinesfalls gesagt, dass die folgenden Zeilen die beste Lösung enthalten.

Was lässt sich heute mit ES6-Transpilern erreichen?

Es lassen sich sehr große Teile von ES6 nach ES5 übersetzen und es steht eine breite Palette an ES6-Transpilern zur Auswahl. Ein Tool, das mich komplett begeistert, habe ich nicht gefunden. Bei jedem Kandidaten gibt es an irgendwelchen Stellen Probleme. Die einen sind mir persönlich zu mächtig, die anderen können mir zu wenig und die nächsten unterstützen keine Source Maps. Hinzu kommt, dass so mancher Transpiler zwar gut funktioniert, die dazugehörigen Grunt/Gulp-Plugins aber defekt sind und die Autoren sich nicht für Pull Requests interessieren.

Viel von ECMAScript 6 ist einfach nur neue Syntax und entsprechend unproblatisch für Transpiler. Einige etwas kniffligere ES6-Features lassen sich zwar im Prinzip auch umsetzen, waren mir bei meinen Versuchen aber nicht geheuer. Features wie z.B. let oder Symbols erfordern von Transpilern sehr fortgeschrittene Trickserei, was nicht sonderlich lesbaren Quelltext produziert und sich auch nicht besonders angenehm anfühlt. Grundsätzlich gilt aber: wenn man nur will, kommt man mit ES6-Transpilern sehr weit und die fortgeschritteneren Tools (Traceur, es6-transpiler) bieten auch die Möglichkeit dazu.

Neben den unterstützten Features ist der Hauptunterschied zwischen den verschiedenen ES6-Transpilern ganz klar die Philosophie. Bei gleichem ES6-Input produzieren sie sehr unterschiedliche ES5-Ergebnisse, die zwar das gleiche Machen, aber einfach sehr unterschiedlich aussehen. Als Beispiel können wir uns mal diesen simplen ES6-Schnipsel ansehen:

class Foo {
  constructor(){
    this.bar = 42;
  }
  baz(){
    var someFn = () => this.bar; 
    return someFn();
  }
}

Die unterschiedlichen Transpiler schaffen es, hieraus wirklich sehr unterschiedliche Ergebnisse zu produzieren.

Transpiler-Tools im Vergleich

Ernsthaft ausprobiert habe ich drei Tools: SweetJS-Makros für ES6, Traceur und ES6-Transpiler. Letzteres ist in meinem Workflow, ergänzt durch Regenerator, im Rahmen einen-Gulp-Tasks das Mittel der Wahl. Das heißt nicht, dass alles damit toll wäre, sondern nur, dass diese Kombination der beste Kompromiss aus Extra-Arbeit und Gewinn durch neue Features darstellt. Aber alle drei Tools haben so ihr Vorzüge.

SweetJS bietet Makros für JavaScript und das Modul es6-macros enthält mit Destructuring, Klassen und Arrow Functions nur wenige neue Features. Diese werden aber ausgesprochen stressfrei umgesetzt; man bindet einfach das Modul in seinen Grunt/Gulp-Task für SweetJS ein und schon funktioniert es anstandslos. Fummelige Konfiguration, halbgare Features oder irgendwelchen anderen Stress gibt es nicht; dafür gibt es aber nicht wahnsinnig viele neue Features. Die Beispiel-ES6-Klasse sieht mit SweetJS kompiliert wie folgt aus:

function Foo(){
  this.bar = 42;
}
Foo.prototype.baz = function baz(){
  var someFn = function (__fa_args){
      return this.bar;
    }.bind(this, typeof arguments !== 'undefined' ? arguments : undefined);
  return someFn();
};

Das ist nicht zu 100% ES6-konform (Klassen sollten eigentlich im Strict Mode sein), aber dafür extrem übersichtlich! SweetJS allgemein und vor allem die ES6-Makros sind sehr zu empfehlen, wenn man sich erst noch an den Transpiler-Gedanken gewöhnen muss. Die Tools machen nicht so wahnsinnig viel, sind leicht zu benutzen und der erzeugte Code ist übersichtlich.

Der Traceur-Compiler ist das genaue Gegenstück zu den schmalen Sweet-Makros. Traceur kann fast alles (u.A. Generators, Module und Promises), was man dem Tool auch anmerkt. Man kann sich einen Wolf konfigurieren, manches funktioniert eher mittelgut und Traceur-JavaScript funktioniert nicht ohne eine Runtime, die man vor dem Rest-Script einbauen muss. Die Runtime wird im generierten Code auch reichlich eingesetzt, so dass man aus Traceur-Output nicht immer schlau wird. Man sehe sich nur an, was Traceur aus unserer Beispielklasse fabriziert:

$traceurRuntime.ModuleStore.getAnonymousModule(function() {
  "use strict";
  var Foo = function Foo(){
    this.magicValue = 42;
  };
  ($traceurRuntime.createClass)(Foo, {
    bar: function(){
      var $__108 = this;
      var someFn = (function(){
        return $__108.magicValue;
      });
      return someFn();
    }}, {});
  return {};
});

Traceur scheint mir am ehesten ein Tool zum Experimentieren um des Experimentierens willen zu sein. Wenn man ein ES6-Programm auch zu benutzen gedenkt, weiß ich nicht, ob man immer die Runtime mitschleppen und die generierten Enigmas debuggen möchte. Als schnelle ES6-Demo ist allerdings kaum etwas besser geeignet, als die Traceur-Repl.

Irgendwo zwischen diesen beiden Extremen bewegt sich es6-transpiler. Es gibt nicht ganz so viele Features wie bei Traceur (z.B. fehlen Generators), dafür wird aber auch keine Runtime benötigt. Sehr schön ist, dass der Sourcecode nach dem Transpiling wirklich nur an den Stellen geändert ist, an denen neue ES6-Features verwendet wurden. Allerdings ist das auch nötig, denn als einziges Tool auf der Liste produziert es6-transpiler keine Source Maps.

var Foo = (function(){
  "use strict";
  var PRS$0 = (function(o, t){
    o["__proto__"] = {
      "a": t
    };
    return o["a"] === t;
  })({}, {});
  var DP$0 = Object.defineProperty;
  var GOPD$0 = Object.getOwnPropertyDescriptor;
  var MIXIN$0 = function(t, s){
    for(var p in s){
      if(s.hasOwnProperty(p)){
        DP$0(t, p, GOPD$0(s, p));
      }
    }
    return t;
  };
  var proto$0 = {};

  function Foo(){
    this.bar = 42;
  }
  DP$0(Foo, "prototype", {
    "configurable": false,
    "enumerable": false,
    "writable": false
  });
  proto$0.baz = function(){
    var this$0 = this;
    var someFn = function(){
      return this$0.bar
    };
    return someFn();
  };
  MIXIN$0(Foo.prototype, proto$0);
  proto$0 = void 0;
  return Foo;
})();

Das ist zwar viel Code, aber dafür auch recht klarer Code. Es gibt auch hier eine „Runtime“, die allerdings direkt im generierten Code zu finden ist. Das Rätselraten hält sich also Grenzen. Normalerweile ist der Runtime-Teil des generierten Codes so untergebacht (alles in einer Zeile), dass Zeilennummern von Quell- und Generiertem Code 1:1 aufeinanderpassen. Debuggen ist also trotz fehlender bzw. eingeschränkter Source Maps recht erträglich.

Regenerator aus dem Hause Facebook passt nicht ganz in die Reihe, da es sich nur ein einziges ES6-Feature kümmert: Generators. Das macht es auch auf recht angenehme und nachvollziehbare Weise (Talk zum Thema) und kann daher gut mit den SweetJS-ES6-Makros oder es6-transpiler kombiniert werden. Regenerator braucht eine eigene Mini-Runtime.

Mein aktueller Workflow

Für mein ES6-Projekt verwende ich Gulp, es6-transpiler und Regenerator. Der ES6-Task in meiner Gulpfile stellt sich aktuell wie folgt dar:

gulp.task('client-compile', function(){
  return browserify('foo.js', {
      debug: true
    })
    .transform(regeneratorify)
    .transform('es6-browserify')
    .require(regenerator.runtime.dev)
    .bundle()
    .on('error', function(err){
      gutil.log(gutil.colors.red('Browserify error:'), err.message);
      this.end();
    })
    .pipe(source('foo.dist.js'))
    .pipe(gulp.dest('./www'))
    .on('error', gutil.log);
});

Grundlage des Transpilier-Prozesses ist Browserify. Da bis vor kurzem noch nicht mal feststand, wie ES6-Module überhaupt aussehen sollen (mittlerweile ist das geklärt) und ich im Rahmen meines letzten Projekts durch RequireJS nachhaltig traumatisiert wurde, wollte ich mal einem Konkurrenz-Modulsystem eine Chance geben. Browserify nimmt normalerweise nur in NodeJS lauffähige CommonJS-Module und macht sie fit den Browser. Dabei wird grundsätzlich die gesamte Modulstruktur in eine einzige Datei kompiliert, was ja heutzutage aus Performance-Sicht ganz wünschenswert ist. Außerdem erlaubt es Browserify, Transformationsfunktionen über der Browserifizierung den Code zu jagen. An dieser Stelle lassen sich die ES6-Transpiler wunderbar einbauen.

Mit es6-browserify existiert bereits eine brauchbare Browserify-ES6-Transpiler-Transformationsfunktion. Diese kann einfach anhand ihres Modulnames via transform('es6-browserify') eingebunden werden. Da es allerdings in es6-transpiler keine Generator-Unterstützung gibt, muss vorher noch Regenerator den Code bearbeiten. Weil ich hierfür keine brauchbare Browserify-Transformationsfunktion gefunden habe, habe ich kurzerhand meine eigene gebaut:

function regeneratorify(file){
  var data = "";
  var stream = through(write, end);
  function write(buf){
    data += buf;
  }
  function end(){
    var rdata = regenerator(data);
    stream.queue(rdata);
    stream.queue(null);
  }
  return stream;
}

Die Einbindung der Funktion erfolgt anhand des Funktionsnamens via .transform(regeneratorify). Zu guter Letzt braucht Regenerator noch seine (kleine) Runtime. Diese wird mittels require(regenerator.runtime.dev) eingebunden, wobei regenerator das normale, mit require() geladene Regenerator-Modul ist.

Der Rest des Codes ist eigentlich nur Zeug, das Browserify und Gulp (zwei sehr eigenwillige Projekte) miteinander zu verbinden sucht. Es war ausgesprochen frustrierend die diversen teilweise sehr halbgaren und/oder kaputten Gulp- und Browserify-Plugins durchzuprobieren, bis ich schließlich zu dem hier beschriebenen Ergebnis gekommen bin.

Die Erfahrungen mit dem Workflow und ES6

Ist erst mal der Buildprozess eingerichtet und alles automatisiert, lebt es sich in der ES6-Welt ganz angenehm. Es gibt viele schöne Features, an die man sich aber auch erst mal gewöhnen muss … wenn man sie denn wirklich alle einsetzen möchte. Und ich glaube nicht, dass das der Fall ist.

Ich würde noch nicht empfehlen, das nächste anstehende JS-Projekt voll auf ES6 auszurichten. Rechnet man die Fummelei mit den Tools, den Lernaufwand und die bei größeren Programmen doch irgendwann spürbare Kompilierzeit von 2-3 Sekunden zusammen, so lohnt es sich in der Gesamtabrechnung einfach noch nicht, es sei denn man hat sehr spezielle Dinge vor. Manche Probleme und Programmierstile können von einigen ES6-Features durchaus profitieren. Generators machen Sequenzen zum Kinderspiel und wenn man viel funktional programmiert lohnen sich Arrow Functions durchaus. Man schreibt (a, b) => a +b einfach so sehr viel lieber als function(a, b){ return a +b; } und dem Code wird durch die fehlenden Schweifklammern sehr viel Grundrauschen entzogen.

Die Features, die meiner Erfahrung nach einen erheblichen Mehrwert bieten (Arrow Functions, Generators) sind jene, die man mit eher kleinen Tools (SweetJS-Makros, Regenerator) zum Einsatz bringen kann. Deshalb ist mein Eindruck: wenn heute schon ES6, dann lieber gezielt Einzel-Feature-Tools hernehmen, als sich den Stress eines vollständigeren Transpilers aufzubürden. Es gilt als auch für den neuen JavaScript-Standard die gute alte 80/20-Regel: 80% des Gewinns werden mit 20% des Einsatzes erzielt. Und mehr als 20% Einsatz sollte man sich im heutigen ES6-Ökosystem meiner Meinung nach nicht zumuten.

Fragen zu HTML5 und Co beantwortet 16 - Flexbox-Masonry, Block- und Inline-Elemente, File API, ES6-Klassen

Ich würde ja mal wieder etwas anderes bloggen, aber so lange ihr mich weiter mit Fragen zu HTML5 und Co bombardiert, muss ich die ja schweren Herzens zuerst beantworten. Das soll nicht heißen, dass ich nicht noch viel mehr Fragen gebrauchen könnte! Wenn euch etwas zu HTML5, CSS3, JavaScript, Web Components oder ähnlichem auf dem Herzen liegt, schreibt mir eine Frage per E-Mail oder via Twitter.

Masonry-Layout mit Flexbox?

Kann man mit Flexbox Masonry Layout machen?

Es gibt tatsächlich die Möglichkeit, etwas in ungefähr dieser Richtung zu machen. Grundsätzlich dient Flexbox weiterhin dem einfachen Anordnen von Boxen und ist damit eher unfancy. Allerdings ist es so flexibel, dass das komplette Aufüllen eines vorhandenen Raumes unter Berücksichtigung des Inhalts machbar ist. Der Clou ist: der vorhandene Raum ist immer eine Zeile und Flexboxen können umbrechen. Um diese Umbrüche zu aktivieren, muss in einem Flex-Container die Eigenschaft flex-wrap auf wrap gesetzt werden:

.wrapper {
  display: flex;
  flex-wrap: wrap;
}

In diesem Container können nun allerlei Kindelemente mit unterschiedlichen Maß-Verhalten platziert werden. Die Maß-Verhalten ergeben sich auf Basisgröße auf der Flex-Achse (flex-basis) sowie den Wachs- und Schrumpffaktoren (flex-grow, flex-shrink). Provoziert man durch zusätzliche Mindestbreiten (min-width) der Kindelemente Umbrüche, so gilt die durch den Umbruch neu entstandene Zeile als komplett neuer Raum, auf dem sich die nächsten paar Flexboxen breit machen – bis zum nächsten Umbruch. Das „breit machen“ erfolgt dabei auf der Kreuzachse, so dass die Elemente auch jeweils auf der vertikalen Mitte der Achse zentriert werden bzw. sich über die komplette Höhe der Zeile erstrecken können. Das Ergebnis ist dann tatsächlich einigermaßen Masonry-Layout-Like.

Block- und Inline-Elemente in HTML5

Kann ich eigentlich beim W3C oder woanders schnell herausfinden, welches Element in HTML5 ein Inline- und welches ein Blockelement ist?

Die Unterteilung in die zwei Kategorien „Block“ und „Inline“ gibt es in HTML5 nicht mehr. Stattdessen gibt es sieben neue Kategorien: Metadata, Flow, Sectioning, Heading, Phrasing, Embedded und Interactive. Elemente können in mehrere dieser Kategorien fallen. Das <a>-Element ist z.B. gleichzeitig Bürger der Kategoien Flow, Phrasing, Interactive und Palpable, während das <nav>-Element in Flow, Sectioning und Palpable zuhause ist.

Die Kategorien werden in HTML5 vorrangig genutzt, um festzulegen, welches Element in welchem anderen Element vorkommen darf (content model). Vor HTML5 galt die einfache Regel „Block-Elemente dürfen Block- und Inline-Elemente, Inline-Elemente aber keine Block-ELemente enthalten“. In HTML5 sollen aber auch Konstruktionen wie die folgende erlaubt sein:

<a href="/foo"> <!-- "Inline-Element" -->
  <h1>Foo</h1>  <!-- "Block-Element" -->
</a>

Um (unter anderem) dies punktuell zu ermöglichen ohne ein generelles Alles-In-Alles-Verschachteln zu erlauben, hat man die Kategorien einfach etwas feiner aufgegliedert. Alles was vor HTML5 an Element-Verschachtelungen möglich war, geht auch weiterhin – und eben ein kleines bisschen mehr.

HTML5 File API: File- in Blob-Objekte verwandeln?

Wie kann man File-Objekte in Blob-Objekte konvertieren? Braucht man dazu den FileReader?

Eigentlich sollte diese Umwandlung nicht nötig sein, denn File-Objekte sind nur ein Spezialfall bzw. eine „Unterklasse“ von Blob. Das kann man der IDL-Box (IDL = Interface Definition Language) an der entsprechenden Stelle in den Spezifikationen entnehmen. Jede API die Blobs verarbeitet sollte auch mit Files klarkommen:

var blob = new Blob(['Hallo Welt'], { type: 'text/plain' });
var file = new File(['Hallo Welt'], 'dateiname.txt');

// FileReader schlucken Files und Blobs gleichermaßen

var reader1 = new FileReader();
reader1.readAsText(blob);
reader1.onload = function(evt){
  window.alert(evt.target.result); // > 'Hallo Welt'
};

var reader2 = new FileReader();
reader2.readAsText(file);
reader2.onload = function(evt){
  window.alert(evt.target.result); // > 'Hallo Welt'
};

Sollte eine API oder Library auf Biegen und Brechen Files nicht annehmen wollen, so kann man ein File-Objekt via slice()-Methode in einen Blob verwandeln:

var blob = file.slice();

Das sollte aber eigentlich wirklich niemals nötig sein.

Privates in ECMScript 6-Klassen

Wenn Klassen in ES6 nur syntaktischer Zucker für Prototypen sind, gibt es dann auch keine Encapsulation mit private usw?

Etwas ähnliches wie private Objekteigenschaften sind in ES6 möglich, aber nicht durch Klassen allein. Vielmehr kann man Klassen (oder auch normale Constructorfunktionen bzw. Objekte) mit Symbols kombinieren und somit so-gut-wie-private Eigenschaften schaffen. Ein Symbol ist ein spezielles Objekt, das als Property-Name fungieren kann; etwas, das in ES5 nur Strings machen konnten:

let foo = {};
let property = Symbol();
foo[property] = 42;

console.log(foo[property]); // > 42

Jedes Symbol-Objekt ist einzigartig, so dass Namenskollisionen prinzipbedingt ausgeschlossen sind. Das ist praktisch um ein Objekt automatisiert mit Eigenschaften zu befüllen, kann aber auch für einen gesteigerten Grad an Privatheit genutzt werden  denn wer keinen Zugriff auf ein Symbol hat, kann auf die damit identifizierte Eigenschaft nicht ohne weiteres zugreifen:

let foo = {};

(function(){
  let property = Symbol();
  foo[property] = 42;
})();
  
// Wie an die 42 kommen?

Wer es drauf anlegt, kommt mit Object.getOwnPropertySymbols() an die Liste der auf einem Objekt verwendeten Symbols. So gesehen sind diese Eigenschaften nicht im Wortsinne privat, doch die als privat und als öffentlich gedachten Eigenschaften sind klar getrennt. Ein versehentlicher (oder auch nur leichtfertiger) Zugriff auf als nicht für die Öffentlichkeit gedachte Eigenschaften ist ausgeschlossen – und das ist ja der eigentliche Use Case, den es zu lösen gilt.

Ob es in ES6 (oder später) auch richtige private Eigenschaften geben wird, ist unklar. De facto existieren private Symbols bereits in den Spezifikationen, doch sie allgemein verfügbar zu machen, hätte allerlei Komplikationen zur Folge. Aktuell werden normale (d.h. einzigartige, aber nicht wirklich private) Symbols als gut genug betrachtet.

Weitere Fragen?

Auch eure Fragen zu HTML5, JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach eine E-Mail schreiben oder Twitter bemühen und ein bisschen Geduld mit der Antwort haben. Ansonsten kann man mich natürlich auch als Erklärbär zu sich kommen lassen.

Fragen zu HTML5 und Co beantwortet 15 - Web Components versus Performance und Async, CSS-Variablen, Data-URLs

Neue Antworten auf neue Fragen zu Webtechnologien braucht das Land? Kein Problem! Falls ihr noch mehr Fragen zu HTML5, CSS3, JavaScript, Web Components oder ähnlichem beantwortet haben wollt, schreibt sie mir per E-Mail oder gebt die Frage per Twitter ab.

Sind Web Components schlecht für die Performance?

Sind Web Components nicht eine Katastrophe für die Performance? So spaltet sich doch die komplette Seite in hundert Einzel-Downloads auf, jede Komponente lädt jedes Mal neu jQuery … oder gibt es da einen Trick?

Heutzutage ist das möglicherweise tatsächlich ein Problem, das sich aber in Zukunft von selbst in Luft auflösen wird. Das Doppel-Download-Problem besteht in Browsern mit nativer Unterstützung für Web Components gar nicht erst, da hier Doppel-Requests automatisch dedupliziert werden (so liest man jedenfalls allerorten; in konkreten Specs habe ich nichts gefunden, das das verlangt). Im Polymer-Polyfill passiert das einfach anhand des Ressourcen-Pfades, die native Implementierung soll auch identische Ressourcen aus unterschiedlichen Quellen deduplizieren können.

Dass Web Components dann immer noch zu vielen Einzel-Requests führen, ist in nicht all zu ferner Zukunft ein Feature und kein Bug. Mit HTTP/2 bzw. SPDY als Netzwerkprotokoll hat man, anders als beim HTTP 1.1 von heute, keinen Vorteil mehr, wenn man Ressourcen zusammenfasst. Im Gegenteil: wenn man sein Frontend in viele kleine Teile aufsplittet, hat man den Vorteil, dass bei der Änderung einer einzigen Icongrafik der Nutzer nicht mehr das komplette Bild-Sprite, sondern wirklich nur eine einzige Winzdatei neu herunterladen muss. Anders gesagt übernimmt HTTP/2 das Zusammenfassen von Dateien auf Protokollebene und Webentwickler müssen es nicht mehr selbst machen. Und nur die üblichen Verdächtigen (d.h. IE und Safari) können das nicht bereits heute. Mehr Infos zum Thema gibt es in einem epischen Slide Deck aus der Feder von Performance-Papst Schepp.

Die Web-Component-Performance-Problematik löst sich also im Laufe der Zeit von selbst. Bis dahin kann man sich mit Vulcanize, einem Tool zum Zusammenfassen von HTML-Imports inklusive aller Ressourcen in eine einzige Datei, behelfen.

CSS-Variablen und JavaScript

Kann man native CSS-Variablen (Custom Properties) via JavaScript ändern?

Man wird das machen können, allerdings ist noch nicht klar wie genau das im endgültigen Standard funktionieren wird. Das was im aktuellen Working Draft steht, hat aktuell nicht viel mit dem zu tun, was man in heutigen Browsern vorfindet. Zur JavaScript-API wird sogar nur angesagt, dass es eine a custom-property API to be defined in the future geben wird – nichts Konkretes weit und breit.

Dass es aber zumindest im Prinzip eine solche API geben kann, zeigt diese simple Demo (läuft nur im Firefox mit aktiviertem CSS-Variablen-Flag), die noch eine alte API aus früheren Spezifikationen verwendet. Wie es in Zukunft gehen wird, ist noch unklar, aber irgendwie wird es wohl funktionieren.

Data-URLs mit JavaScript laden

Ajax-Requests auf Data URLs werden von der Same Origin Policy unterbunden. Kann man Data-URIs noch anders (also ohne XHR) laden?

Mit der File API geht das. Man nehme die Data-URL, packe sie in einen Blob, lese diesen mittels eines FileReaders aus und schon ist das Ergebnis da:

var dataUrl = 'data:text/plain,Hallo Welt';
var dataString = dataUrl.split(',')[1];
var mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0];
var blob = new Blob([dataString], {type: mimeString});
var reader = new FileReader();
reader.readAsText(blob);
reader.onload = function(evt){
  window.alert(evt.target.result);
};

Wenn man aber schon zur File API und zu Blobs greift, kann man auch weiterhin XHR verwenden. Es gibt Blob-URLs die aufgrund einer Ausnahme in der der Same Origin Policy auch von XHR abgerufen werden können. So klappt es dann auch ohne File Reader und mit XHR:

var dataUrl = 'data:text/plain,Hallo Welt';
var dataString = dataUrl.split(',')[1];
var mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0];
var blob = new Blob([dataString], {type: mimeString});
var blobUrl = window.URL.createObjectURL(blob);
$.get(blobUrl, function(result){
  window.alert(result);
});

Der einzige Browser in dem das nicht klappt, ist der Internet Explorer: der macht bekanntlich die Same Origin Policy falsch.

Asynchron ladende Web Components?

Wenn ich viele Web Components via HTML-Import in meine Seite lade, ist das nicht gut für die Performance. Kann ich hier auch das async-Attribut verwenden? Funktionieren die Komponenten dann noch?

Laut Spezifikation für HTML Imports wird es das async-Attribut für <link>-Elemente geben, so dass das Laden von externen Ressourcen nicht mehr blockiert. Die im Rahmen von Web Components selbst erstellten Elemente funktionieren dann auch problemlos. Es ist nicht erforderlich, den HTML-Parser vor Erreichen des noch anzumeldenden Elements „abzufangen“, da die Mechanik von document.registerElement() ohnehin nur ein nachträgliches Upgrade von schon durch den Parser als unbekannt verarbeiteten Elementen vornimmt.

Die Herausforderung hierbei ist natürlich die Zeitspanne bis ein Element geladen und einsatzbereit ist. Durch umsichtige Programmierung und passendes Styling gilt es, nerviges Geflacker zu unterbinden; entsprechende Callbacks und CSS-Pseudoklassen helfen dabei. Polymer feuert ein hilfreiches polymer-ready-Event auf window, sobald alle Elemente beim Browser angemeldet sind.

Weitere Fragen?

Auch eure Fragen zu HTML5, JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach eine E-Mail schreiben oder Twitter bemühen und ein bisschen Geduld mit der Antwort haben. Ansonsten kann man mich natürlich auch als Erklärbär zu sich kommen lassen.

Web Components erklärt, Teil 3: native HTML-Elemente erweitern

Die bisherigen Teile dieser Serie haben gezeigt, was Web Components sind und wie man mit ihnen eigene HTML-Elemente erfinden kann. Seither sind Web Components auch ganz gut im allgemeinen Bewusstsein der Frontend-Entwickler angekommen, was man vor allem an der auf Twitter aufkommenden Kritik merkt. Eine besonders häufig formulierte Befürchtung ist, dass Web Components jedes etablierte HTML-Element durch eine unsemantische Eigenkonstruktion ersetzen könnten. In diesem Szenario würde z.B. aus <a href="https://www.google.de"> ohne guten Grund ein Monstrum der Marke <polymer-anchor data-href="https://www.google.de">, das in wichtigen Details (z.B. Barrierefreiheit oder Browserunterstützung) nicht an das Original heranreicht.

Es ist in der Tat nicht auszuschließen, dass demnächst ohne komplett überzeugenden Grund zusätzliche Varianten von <a> erfunden werden, aber es ist definitiv nicht nötig, dabei das Rad stets komplett neu zu erfinden und auf die Fähigkeiten der Originale zu verzichten. Web Components beinhalten einen Mechanismus, der es erlaubt vorhandene Elemente mit wenig Aufwand um neuen Fähigkeiten zu ergänzen. Mit normalem DOM-Code ist das etwas knifflig, mit Polymer jedoch ein Kinderspiel.

Der Extend-Mechanismus

Wie wir im ersten Teil der Serie gelernt haben, ist eine der neuen Technologien rund um Web Components die Funktion document.registerElement(). Mit ihr kann man beim Browser komplett selbst erfundene Elemente anmelden:

document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    methodBar:{
      value: function(){
        console.log('Hallo Welt!');
      }
    }
  })
});

An document.registerElement() übergibt man neben dem Namen des neuen Elements (hier x-foo) auch ein Konfigurationsobjekt, das unter anderem den Prototypen für das neue Element festlegt (hier ein neues Objekt, das seinerseits HTMLElement.prototype als Prototypen hat). Als Prototyp kann jedes HTML-Element fungieren, so dass man sein selbstgebautes Element zu z.B. einem Sonderfall von <a> könnte, einfach indem man HTMLAnchorElement.prototype statt HTMLElement.prototype verwendet. Außer einer veränderten Prototypen-Kette bringt das allein aber nicht viel und <x-foo> wird dadurch nicht zu einem anklickbaren Link.

Um wirklich (ohne das Rad neu zu erfinden) eine Abwandlung von <a> zu erstellen, muss man einen etwas anderen Weg gehen. Zum Einen muss man bei der Element-Anmeldung dem Konfigurationsobjekt eine extends-Eigenschaft mitgeben, die den Tag des Elements angibt, von dem wir einen Sonderfall anlegen möchten:

document.registerElement('x-foo', {
  prototype: Object.create(HTMLAnchorElement.prototype, {
    methodBar:{
      value: function(){
        console.log('Hallo Welt!');
      }
    }
  }),
  extends: 'a'
});

Wichtig ist zum Anderen, dass der Prototyp des Elements in extend auch der Prototyp des Objekts in prototype ist. Die Benutzung des neuen Elements erfolgt dann nicht über einen neuen Tag <x-foo>, sondern das Element wird im is-Attribut eines normalen <a>-Elements angegeben:

<a is="x-foo" href="https://www.google.de">Google</a>

Dieses Element ist ein fast ganz normaler Link, nur ergänzt durch unsere Erweiterung in Form der JavaScript-Funktion methodBar(). Die Spezifikationen für Custom Elements nennen solche Konstruktionen type extensions. An sich funktioniert unsere Erweiterung jetzt auch, aber richtig toll ist das Ergebnis noch nicht:

  1. Die rohe API von document.registerElement() ist äußerst unbequem, v.A. das Setup der Prototypen-Kette ist ausgesprochen mühsam
  2. Alles, was über den Einbau einer kleinen JS-Extramethode herausgeht (z.B. Shadow DOM) würde richtig anstrengend werden
  3. Mangels Polyfills funktioniert der bisher gezeigte Code nur in den allerneuesten Browser (d.h. aktuellem Chrome)

Zum Glück ist das Erweitern von Elementen mit Polymer ein Kinderspiel. Es ist sogar so einfach, dass wir uns für den nächsten Schritt direkt vornehmen können, eine durchaus realitäsnahe Erweiterung für <a>-Elemente zu schreiben.

Type Extensions mit Polymer

QR-Codes (nicht lachen!) sind nichts anderes als Links in Bildform. Also macht es Sinn, sie in HTML wie folgt einzubinden:

<a is="qr-code" href="https://www.google.de">Google</a>

Ein solcher Link sollte statt des Link-Texts einen QR-Code anzeigen. Ergänzend sollte man, da es sich ja um Bilder handelt, optionale height- und width-Attribute angeben können. Wenn sich eins der Größen-Attribute oder das href-Attribut ändert, sollte der Code neu berechnet werden. Davon abgesehen sollte sich das Element wie ein herkömmlicher Link verhalten: es muss anklickbar sein, sich per Tastatur bedienen lassen und generell keine schwerwiegenden Nachteile gegenüber einem herkömmlichen Link haben.

Eine gut funktionierende QR-Code-Library ist auf Github schnell gefunden und wird zusammen mit dem üblichen Boilerplate-Code für ein neues Polymer-Element in eine HTML-Datei geschrieben:

<script src="qrcode.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">

<polymer-element name="qr-code">

  <template>
    <span id="Code"></span>
  </template>

  <script>
    Polymer('qr-code', {
    });
  </script>

</polymer-element>

Das „Element“ qr-code, das wir hier neu anlegen, wird hinterher als Wert im is-Attribut von <a>-Elementen fungieren. Das <span>-Element im Shadow-DOM-Template nimmt den erzeugten QR-Code auf.

Der Prototyp unseres qr-code-Elements braucht als erstes eine Methode zum erstellen neuer QR-Codes. Diese Objekte, erstellt durch die QR-Library, werden in der qrcode-Eigenschaft der Element-Instanz gespeichert (this.qrcode). Eingesetzt wird diese Methode wenn das Element bereit ist, d.h. der ready-Callback feuert:

Polymer('qr-code', {

  createCode: function(){
    return this.qrcode = new QRCode(this.$.Code, {
      text: this.getAttribute('href'),
      width: this.getAttribute('width') || 128,
      height: this.getAttribute('height') || 128
    });
  },

  ready: function(){
    this.createCode();
  }

});

Um auf Attribut-Änderungen reagieren zu können, bedienen wir uns des attributeChanged-Events. Wenn sich nur das href-Attribut ändert, können wir die makeCode()-Methode von QRCode-Objekten nutzen; nur bei Änderungen der Maße muss eine komplett neue QRCode-Instanz erstellt werden:

Polymer('qr-code', {

  ...

  attributeChanged: function(attr, oldVal, newVal){
    if(attr === 'href'){
      this.qrcode.makeCode(newVal);
    }
    if(attr === 'width' || attr === 'height'){
      this.createCode();
    }
  }

});

Stand jetzt ist unser Werk immer noch ein normales Custom Element. Und wie machen wir jetzt eine <a>-Erweiterung daraus? Es könnte einfacher nicht sein:

<polymer-element name="qr-code" extends="a">
  ...
</polymer-element>

Das ganze komplizierte Prototypen-Setup übernimmt Polymer und unsere Element-Erweiterung funktioniert einfach!

Selbstgebaute Custom Elements lassen sich in Polymer auch erweitern, wenn auch auf etwas andere Art und Weise. Was eigentlich gar nicht geht, sind Mehrfacherweiterungen.

Mixins statt Mehrfacherweiterungen

Mehrfacherweiterungen nach dem Muster <a is="foo bar baz"> gibt es nicht. Das ist auch einigermaßen nachvollziehbar, denn letztlich laufen Erweiterungen immer noch auf Prototypen-Ketten hinaus und da ist klar, dass ein Element in der Kette nicht mit mehr als einem anderen verbunden sein kann. Aber das als zweites Argument in die Polymer()-Funktion gesteckte Konfigurationsobjekt kann natürlich ein aus mehreren Objekten zusammengesetztes Objekt sein – Mixins sind hier die beste Lösung.

Nehmen wir einmal an, wir wollten ein Set von Social-Media-Buttons bauen. Jeder Social-Media-Button muss eine URL kennen, für die er Tweet-Links, Like-Schaltflächen o.Ä. bereitstellen soll. Es macht Sinn, diese Funktionalität in eine entsprechende Social-Media-Button-„Basisklasse“ auszulagern:

<polymer-element name="social-media-button" attributes="url">
<script>
  Polymer('social-media-button', {
    url: null,
    ready: function(){
      this.url = this.url || window.location.href;
    }
  });
</script>
</polymer-element>

Erweiterungen dieses allgemeinen Social-Media-Buttons könnten dann konkrete Facebook- und Twitter-Buttons sein. Diese sind keine Type Extensions sondern eigenständige Elemente, da die Custom-Elements-Spezifikation Type Extensions nur für native Elemente vorsieht. Polymer behilft sich, indem aus selbstdefinierten Elementen abgeleitete selbstdefinierte Elemente einfach per Prototypen-Kette hintereinandergeschaltet werden und so komplett neue Elemente entstehen, die die gleichen Fähigkeiten wie ihre „Basisklassen“ haben.

<polymer-element name="twitter-button" extends="social-media-button" attributes="via">
  <template>
    <iframe allowtransparency="true" frameborder="0" scrolling="no"
            src="https://platform.twitter.com/widgets/tweet_button.html?url={{encodedUrl}}&amp;via={{via}}"
            style="width:130px; height:20px;"></iframe>
  </template>
  <script>
    Polymer('twitter-button', {
      via: 'sir_pepe',
      encodedUrl: null,
      observe: {
        url: 'encode'
      },
      encode: function(){
        this.encodedUrl = encodeURIComponent(this.url);
      }
    });
  </script>
</polymer-element>


<polymer-element name="facebook-button" extends="social-media-button" attributes="share">
  <template>
    <iframe allowTransparency="true" frameborder="0" scrolling="no"
            src="//www.facebook.com/plugins/like.php?href={{encodedUrl}}&amp;width&amp;layout=standard&amp;action=like&amp;show_faces=false&amp;share={{share}}&amp;height=35&amp;appId=263047413871308" style="border:none; overflow:hidden; height:35px;" ></iframe>
  </template>
  <script>
    Polymer('facebook-button', {
      share: true,
      encodedUrl: null,
      observe: {
        url: 'encode'
      },
      encode: function(){
        this.encodedUrl = encodeURIComponent(this.url);
      }
    });
  </script>
</polymer-element>

Beide Elemente haben große Unterschiede im Shadow DOM und dezent andere Features (via-Attribut beim Twitter-Button, share-Attribut beim Facebook-Widget) aber sie haben auch ein gemeinsames Feature: beide erzeugen (und verwenden) eine encodedUrl-Eigenschaft. Wenn nicht alle Social-Media-Buttons diese Eigenschaft benötigen, hat sie in der „Basisklasse“ nichts verloren, andererseits ist es auch etwas blöd, diesen Code doppelt vorliegen zu haben. Die Lösung besteht darin, die gemeinsamen Teile des Konfigurations-Objekts in ein eigenes Objekt auszulagern …

var encodedUrlMixin = {
  encodedUrl: null,
  observe: {
    url: 'encode'
  },
  encode: function(){
    this.encodedUrl = encodeURIComponent(this.url);
  }
};

… und dieses Objekt mit den individuellen Konfigurations-Objekten der Buttons zu kombinieren. Hierfür kann jede beliebige Mixin-Funktion verwendet werden; Polymer bringt in Form von Platform.mixin() auch eine mit, die so gut wie jede andere ist:

<polymer-element name="twitter-button" extends="social-media-button" attributes="via">
  <template>
    ...
  </template>
  <script>
    Polymer('twitter-button', Platform.mixin({
      via: 'sir_pepe',
    }, encodedUrlMixin));
  </script>
</polymer-element>


<polymer-element name="facebook-button" extends="social-media-button" attributes="share">
  <template>
    ...
  <script>
    Polymer('facebook-button', Platform.mixin({
      share: true
    }, encodedUrlMixin));
  </script>
</polymer-element>

Und schon funktionieren beide Buttons! Der Schlüssel zu zwischen Komponeten geteilter Funktionalität ist also die Kombination von normaler „Vererbung“ und flexiblen Mixins.

Fazit

Werden dank Web Components viele komische neue Dinge als zweifelhafte HTML-Element umgesetzt werden? Vermutlich. Als unsemantische Div-Suppe muss das Ganze allerdings nicht enden, da, wie wir gesehen haben, sich auch einfach native Elemente erweitern lassen. Und für komplexere Funktionalitäts-Transplantationen stehen Mixins bereit, die ein ganz eigener Teil der Web-Component-Welt werden können. Es ist also alles halb so wild.

Folgt mir!

Kauft mein Zeug!

Cover der HTML5-DVD
Infos auf html5-dvd.de, kaufen bei Amazon

Cover des HTML5-Buchs
Infos auf html5-buch.de, kaufen bei Amazon

Cover des CSS3-Buchs
Infos auf css3-buch.de, kaufen bei Amazon