Hardware-Review: Sony eBook-Reader PRS-T3

Ich bin kein Early Adopter. Weder hatte ich bisher einen E-Reader in meinem Besitz noch habe ich im Nachgang der Snowden-Veröffentlichungen groß etwas gegen meine Ausspionierbarkeit getan. Gerade letzteres betrachte ich als ein langfristiges Projekt; in einer Hau-Ruck-Aktion bei allem und jedem Online-Dienst den Anbieter wechseln bzw. zu einer selbstgehosteten Lösung umschwenken stelle ich mir sehr mühsam und mit Reibungsverlusten behaftet vor. Lieber mache ich sowas allmählich, dafür dann aber gründlich und nervenschonend. Der erste Schritt dabei muss auch gar nicht der Umbau vorhandener Infrastruktur sein, denn es reicht vielleicht erst mal, keine neue Stasi-Hard- und Software anzuschaffen. Und so ging es mir bei der E-Reader-Auswahl dann auch klar darum, ein möglichst simples Gerät zu wählen; eins das im Idealfall gar nicht nach Hause telefonieren oder sonstigen Fernsteuer-Schindluder treiben kann. Ein solches Teil zu finden, das am Ende dann auch noch als Reader taugen soll, war gar nicht einfach.

Wenn man sich durch die einschlägigen Review-Seiten gräbt, gibt es keinen Zweifel: die besten E-Reader fabriziert im Moment Amazon mit dem Kindle, im übrigen auch zum besten Preis. Da der Kindle aber auch im Stasi-Ranking weit vorn liegt, Amazon Ökosystem-Lock-In betreibt und auch darüber hinaus allerlei unschönes Zeug macht, suchte ich eine Alternative. Die diversen Probleme haben in Teilen auch einige Konkurrenzprodukte (wie z.B. erzwungene Online-Aktivierung), aber meist in nicht ganz so schlimm und nicht im gleichen Umfang. Meine Wahl fiel schließlich auf den PRS-T3 Reader von Sony (Sony-Produktseite, Amazon-Link), über den ich nach eingehenden Tests nichts schlechtes zu berichten weiß.

Der Reader macht die Basics alle richtig. Der Text ist gestochen scharf, ein sich leerender Akku ist nicht wahrnehmbar, es passt viel in den Speicher und das Gerät ist klein und leicht. Eine Abdeckung für den Bildschirm ist fest angebaut. Die Bedienung erfolgt primär via Touchscreen, wobei es zum Blättern noch ergänzende Hardware-Tasten gibt. An behandschuten Wintertagen sind diese wirklich sehr sehr nützlich. Der Touchscreen ist ok; smartphoneske Leistungen ergeben sich vermutlich allein schon aus der beschränkten Rechenpower des Geräts nicht. Zum Umblättern und zum Navigieren des UIs reicht es aber locker. Neben der Lese-Funktion gibt es natürlich auch einen Webbrowser und allerlei übrige Apps, die man samt und sonders in der Pfeife rauchen kann, weil das Gerät eben nur zum lesen gebaut ist. Reviews bemängeln die fehlende Hintergrundbeleuchtung, die heutzutage wohl Standard bei E-Readern ist. Mir hat sie bisher nicht gefehlt, aber ich habe auch keine Vergleichserfahrungen (außer mit Totholz-Büchern, ebenfalls ohne eingebaute Beleuchtung).

Zum Thema „Stasifrei und Spaß dabei“: das Gerät kann durchgehend mit abgeschaltetem Wlan betrieben werden, nach Hause telefoniert wird nicht. Neue E-Books lassen sich mit der freien Software Calibre bequem auf den per USB angeschlossenen Reader schaffen und verwalten. Als Bezugsquelle kann man jeden E-Book-Shop im großen weiten WWW nutzen. Der Reader spricht nativ EPUB, PDF und TXT, ergänzend kann Calibre verschiedenste Formate ineinander konvertieren (z.B. MOBI zu EPUB) und im Vorbeigehen auch DRM entfernen. Man ist also ungebunden und – soweit ich das beurteilen kann – unbeobachtet.

So richtig viel zu meckern habe ich über das Gerät also nicht, auch nicht nach drei drauf gelesenen dicken Wälzern. Auf Twitter habe ich allgemeines Entsetzen über die Barabarei wahrgenommen, die ein nicht-hintergrundbeleuchteter Reader darstellt, aber wie schon erwähnt: so richtig hat mir das bisher nicht gefehlt. Wer also jenseits vom Amazoniversum gepflegt und unbeobachtet E-Books lesen möchte, macht mit dem Reader von Sony meiner Meinung nach nicht viel verkehrt.

HTML5 Drag & Drop, Teil I: Drag & Drop für Dateien

Dieser Artikel ist Teil einer Serie:

  1. Teil 1: Dateien
  2. Teil 2: Elemente
  3. Teil 3: Drop Effect

Die Drag & Drop-API von HTML5 entstand, wie das bei Webstandards so üblich ist, durch Reverse Engineering aus einer bereits in einem Browser vorhandenen Technologie. Das wäre auch nicht weiter tragisch, wäre nur die Basis-Technologie in diesem Falle nicht dem Internet Explorer 5 (!) entsprungen. Und ja, sie ist genau so übel, wie man sich das vorstellt; die von PPK gewählt Bezeichnung steaming pile of bovine manure trifft es eigentlich ganz gut.

Nur es hilft alles Jammern nichts, denn es gibt keine Alternative. Zwar kann man mit diversen JavaScript-Bibliotheken schon seit jeher Drag-&-Drop-Funktionen in den Browser bringen, doch diese Lösungen erreichen nicht den Funktionsumfang von HTML5. Vor allem hat sich seit den Betrachtungen von PPK 2009 einiges in Browsern und Entwickler-Gehirnen getan, so dass mittlerweile tatsächlich sinnvolle Dinge mit dieser API möglich sind. Ohne native Browserunterstützung der HTML5-API ist es zum Beispiel nicht möglich, Dateien aus dem Dateisystem in die Webseite zu ziehen und zu verarbeiten. Trotz mißratener API ist das auch gar nicht mal so schwer.

Vorbereitungen

Wenn man eine Datei ins Browserfenster zieht, versteht der Browser das normalerweise als Navigationsanweisung, d.h. er leitet auf die lokale Adresse der gezogenen Datei weiter. Möchte man eine Datei in der Webapp verarbeiten, so muss man mit ein bisschen JavaScript, HTML und CSS nachhelfen. Man nehme etwas Markup für ein Drop-Zielelement …

<div class="dropzone">Drop!</div>

… garniere es mit CSS …

.dropzone {
  text-align: center;
  background: #EEE;
  border: 0.2em solid #000;
  padding: 3em;
}

… und fange die durch HTML5 spezifizierten Drag-&-Drop-Events ab. Hiervon gibt es eine ganze Menge, wobei für den Datei-Drop-Fall nur dragover und drop wirklich relevant sind:

$('.dropzone').on('dragover', function(evt){
});

$('.dropzone').on('drop', function(evt){
});

Soweit, so gut (siehe jsFiddle).

Das Lösen der Handbremse

Warum braucht man nun zwei Events für eine einfache Drop-Operation? Die API von Drag & Drop ist so gestaltet, dass in der Ausgangslage drop-Events nicht stattfinden, wenn man eine Datei auf einem Element droppt. Die Handbremse ist also standardmäßig angezogen. Die Überlegung dahinter war wohl, dass die meisten Elemente in einer Webseite eben keine validen Dropziele darstellen . Für diese Elemente soll weiterhin das normale Verhalten (Drop = Redirect) gelten und nur wenn man die Handbremse löst, soll etwas anderes passieren können. Lösen lässt sich die Bremse, indem man ganz einfach das dragover-Event abbricht:

$('.dropzone').on('dragover', function(evt){
  evt.preventDefault();
});

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  window.alert('Drop!');
});

Ohne das evt.preventDefault() im dragover-Event würde das drop-Event nicht feuern und das Alert würde nicht aufpoppen (Ausnahme: Chrome ist der einzige Browser bei dem dieses Lösen der Handbremse seit einiger Zeit nicht mehr nötig ist). Ein weiteres evt.preventDefault() im drop-Event selbst verhindert schließlich den standardmäßig stattfindenden Redirect. Und damit das Ganze auch im IE funktioniert, braucht es auch ein evt.preventDefault() im dragenter-Event (MSDN). Somit haben wir eine brauchbare Ausgangslage geschaffen, denn wir können zumindest mal Dateien in den Browser ziehen, ohne dass die Standardmechaniken greifen (siehe jsFiddle).

Noch zwei Anmerkungen: die HTML5-Spezifikationen schreiben, dass das dragenter-Event für das Lösen der Handbremse zuständig zu sein habe; in allen Browsern außer dem IE erfüllt tatsächlich nur das dragover-Event diese Funktion. Im IE müssen beide Events abgebrochen werden. Das ebenfalls in den Spezifikationen zu findende dropzone-Attribut ist stand heute ein reiner Papiertiger.

Gezogene Dateien einlesen

Der Rest ist eigentlich relativ einfach. Im Event-Objekt des Drop-Handler gibt es eine Eigenschaft dataTransfer, die wiederum eine Eigenschaft files hat, die eine Liste der gedroppten Dateien darstellt. Dabei handelt es sich um eine ganz normale FileList mit File-Objekten wie in der File API spezifiziert – die Weiterverarbeitung der gezogenen Datei ist also mit den ganz normalen HTML5-Mitteln möglich. Um beispielweise den Inhalt einer Textdatei mit einem Alert-Fenster auszugeben pickt man sich aus der FileList die Datei heraus und liest sie ein:

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  var file = evt.originalEvent.dataTransfer.files[0];
  if(file.type === 'text/plain'){
    var reader = new FileReader();
    reader.readAsText(file);
    reader.addEventListener('load', function(){
      window.alert(reader.result);
    }, false);
  }
});

Dieser Code funktioniert, wenn nur eine Datei gezogen wird oder bei mehreren Dateien die erste Datei eine Plaintext-Datei ist. Das Prinzip auf mehrere Dateien auszudehnen ist ohne weiteres möglich, aber wegen der asynchronen Natur der FileReader-Objekte auch kein kompletter Selbstläufer – wir ersparen uns das an dieser Stelle. Alternativ wäre es natürlich auch möglich, die Dateien in Indexed DB zu speichern, mit XHR2 auf einen Server zu laden oder anderweitig zu verarbeiten.

Erwähnenswert ist, dass der Inhalt des dataTransfer-Objekts wirklich nur beim drop-Event auslesbar ist. Bei allen anderen Events befindet sich das Daten-Objekt im protected mode und die API behauptet, es würden gar keine Dateien gezogen. Das ist zur Zeit ein ziemliches Problem, denn eigentlich möchte man doch das Drop-Ziel hervorheben, wenn der Nutzer eine gültige Datei auf das Element zieht. Aktuell ist dies in keinem Browser möglich, lediglich Drop-Ziele an sich können hervorgehoben werden.

Drop-Ziele hervorheben

CSS Selectors Level 4 spezifiziert Pseudoklassen für Drag & Drop, ist jedoch noch weit davon entfernt in irgendwelchen Browsern aufzuschlagen. Zur Zeit führt der einzig gangbare Weg über JavaScript, speziell die Events dragenter und dragleave. Gestaltet man sich eine hübsche Klasse für gültige Dropziele …

.valid {
  background: #EFE;
  border: 0.2em solid #0F0;
}

… so kann man diese bequem in diesen beiden Events vergeben und wieder entfernen. Außerdem muss die Klasse auch nach einem erfolgten drop entfernt werden, da für Browser offenbar ein Ende der Drop-Operation kein Ende der Drag-Operation darstellt:

$('.dropzone').on('dragenter', function(){
  $(this).addClass('valid');
  evt.preventDefault();
});

$('.dropzone').on('dragleave', function(){
  $(this).removeClass('valid');
});

$('.dropzone').on('drop', function(evt){
  evt.preventDefault();
  // Rest der Drop-Logik
  $(this).removeClass('valid');
});

Fertig! War doch gar nicht mal so schwer.

Fazit

Mit der HTML5-API für Drag & Drop Dateien im Browser zu empfangen ist ohne weiteres möglich. Zwar gibt es ein paar Mängel (z.B. die fehlende Möglichkeit, Dropziele nur bei bestimmten Dateitypen hervorzuheben), aber für einfache wie Use Cases wie Reinziehen-Hochladen-Fertig reicht es allemal. Auch mit der Browserunterstützung sieht es gut aus; der limitierende Faktor dürfte eher sein, ob die HTML5-APIs zur Weiterverarbeitung der Dateien unterstützt werden.

Betrachtet man nur den Datei-Drop, sieht die HTML5-API gar nicht so schlimm aus. Das ändert sich, sobald man versucht auch DOM-Knoten ziehbar zu machen und in Dropzielen zu empfangen. In 90% der Fälle ist man hier mit einer Non-HTML5-Lösung (also z.B. einem beliebigen jQuery-Plugin) besser beraten, falls man nicht etwas sehr bestimmtes vorhat – was genau das ist, sehen wir dann im nächsten Teil.

Erklärbär-Termine für Februar und März 2014

Auch im neuen Jahr startet Erklärbär Tours wieder einige Expeditionen in das Reich von HTML5, CSS3 und JavaScript. Falls ihr dabei sein wollt, habe ich die folgenden Termine für euch im Angebot:

  • 3. - 5. Februar in München: HTML5-Schulung bei der Open Source School. Mein bewährtes drei­tä­gi­ges HTML5-Standardprogramm stattet die Teilnehmer im Druckbetankungsverfahren mit so gut wie allem aus, was man zu HTML5 wissen muss. Von semantischem Markup bis hin zu Canvas-Frameworks ist alles dabei. Geboten wird ein großer Praxisanteil, kleine Arbeitsgruppen und ein Buch gibt es obendrein.
  • 6. und 7. Februar in München: CSS3 bei der Open Source School. Mein zweitä­gi­ges CSS3-Standardprogramm katapultiert die Teilnehmer in das CSS3-Zeitalter, in dem Webfonts und Farbverläufe fließen. Auch hier steht einen großer Praxisanteil mit überschaubaren Arbeitsgruppen auf dem Plan und mindestens ein CSS3-E-Book gibt es als Bonus.
  • 5. - 7. März in Unterhaching: halbtägige Workshops ECMAScript 5 und Web Workers im Rahmen der JavaScript Days 2014.

ECMAScript 6: Generators

Generators sind eins der Features von ECMAScript 6, die erhebliche Auswirkungen auf die tägliche JS-Programmierpraxis haben werden. So werden durch Generators vor allem asynchrone Operationen zum Kinderspiel; also genau jene Teile des JS-Alltags, die heutzutage im besten Fall nur mit lästiger Fummelei verbunden sind und im schlimmsten Fall mit dem Abgleiten in die callback hell enden. Da mittlerweile die ersten Browser und auch Node.js Generators implementieren, lohnt sich ein gründlicher Blick auf dieses neue Werkzeug bereits heute, denn wie gesagt: asynchrone Programmierung wird durch Generators extrem vereinfacht. Alles, was vorher in einer ünübersichtlichen Callback-Verschachtelung endete, kann mit dem neuen ES6-Feature und einem altbekannten JS-Pattern in übersichtlichen Sequenzen ausgedrückt werden. Klingt sehr kompliziert, ist es aber eigentlich gar nicht.

Was ist ein Generator?

Das ECMAScript-Wiki beschreibt Generators wie folgt:

First-class coroutines, represented as objects encapsulating suspended execution contexts (i.e., function activations).

In irdischer Sprache formuliert handelt es sich um pausierbare Funktionen. Diese Funktionen (bzw. die aus ihnen erstellten Objekte) können eine Anzahl von Werten ausgeben und ihrerseits mit neuen Werten gefüttert werden. Jede Ausgabe eines Werts pausiert die Funktion, jede Fortsetzung der Funktion bietet die Möglichkeit, einen neuen Wert in die Funktion einzugeben.

Ein Generator ist das Produkt einer generator function, einer JS-Funktion mit einer speziellen Syntax:

// Generator Function
var genFn = function*(){
};

// Generator
var gen = genFn();

Hier ist genFn die generator function und gen der Generator. Eine generator function also ist eine Vorlage, die einen Generator produziert. Sie unterscheidet sich durch zwei Merkmale von einer herkömmlichen Funktion; neben dem * hinter function kann in ihr yield anstelle bzw. als Ergänzung zu return verwendet werden.

Aus dem Generator …

Wird auf einem Generator-Objekt die next()-Methode aufgerufen, wird der Code der erstellenden generator function ausgeführt, bis das erste yield erreicht wird. Dieses neue Schlüsselwort ist wie return, in dem Sinne dass es einen Wert zurückgibt. Allerdings ist mit dem ersten yield die Funktion nicht beendet, sondern nur an exakt dieser Stelle pausiert. Am besten versteht man das an einem super-simplen (und entsprechend nutzlosen) Beispiel:

// Generator Function
var genFn = function*(){
  yield 1;
  yield 2;
  yield 3;
};

// Generator
var gen = genFn();

Ein Aufruf von gen.next() führt dazu, dass die generator function ausgeführt wird, allerdings nur bis zum ersten yield. Das yield gibt vergleichbar mit return einen Wert zurück. Im Unterschied zu return beendet yield allerdings die weitere Ausführung der Funktion nur vorläufig. Die Funktion merkt sich, an welcher Position im Code das yield stattgefunden hat und beim nächsten Aufruf von next() macht sie an genau dieser Stelle weiter. Man kann also gen.next() drei mal aufrufen und bekommt jedes mal einen anderen Wert, weil der Reihe nach alle drei yield drankommen:

var genFn = function*(){
  yield 1;
  yield 2;
  yield 3;
};
var gen = genFn();

gen.next(); // > { value: 1, done: false }
gen.next(); // > { value: 2, done: false }
gen.next(); // > { value: 3, done: false }
gen.next(); // > { value: undefined, done: true }
gen.next(); // > Exception: Generator has already finished

Der Wert, den next() zurückgibt, ist ein Objekt, dass nebem dem Wert in der Eigenschaft value auch ein done-Flag enthält, das angibt, ob der Generator komplett abgearbeitet wurde. Nach drei Aufrufen von next() sind alle yield drangekommen und entsprechend gibt es keinen Wert mehr, sondern nur noch undefined mit dem Hinweis done: true. Ein weiterer Aufruf next() wird mit einer Exception quittiert.

Mit Generatoren lassen sich allerlei Dinge anstellen, die man anders in JavaScript nicht auf die Reihe bekommt. Eine endlose (wirklich endlose) Zahlenreihe zu repräsentieren ist mit ihnen zum Beispiel ein Kinderspiel:

var genFn = function*(){
  var i = 0;
  while(true){
    yield i++;
  }
};
var gen = genFn();

Hier kann man gen.next() aufrufen bis man grün wird, die Zahlenreihe endet nie. Gleichmaßen führt die Generator-Funktion selbst nicht zu einer Browser-Blockade, da die while-Schleife nach jedem Durchlauf mit yield verlassen und erst bei gen.next() wieder betreten wird. Somit ist auch klar, warum wir hier von generators sprechen – die generator function beschreibt eine Sequenz und jedes Mal wenn wir das aus ihr produzierte Objekt anstoßen, wird ein entsprechender Wert generiert.

Für Dinge wie endlose Zahlenreihen brauchen wir auch nicht mehr zum Thema Generator zu wissen. Wer aber jenseits der Fibonacci-Zahlen noch Use Cases für den Browser-Alltag sucht, sollte aber wissen, dass man sich ein Generator auch gerne mit neuen Inputs füttern lässt.

… in den Generator

Was yield von return unterscheidet ist neben der eingebauten Hier-Gleich-Weitermachen-Funktionalität auch, dass man von außen an die Stelle von yield einen Wert in den Generator hineinwerfen kann. Im folgenden Codebeispiel sorgt der erste Aufruf von next() dafür, dass die Funktion bis zum ersten yield kommt, wo 23 zurückgegeben wird. Der nächste next()-Aufruf erhält mit 42 einen Wert, und da die Funktion direkt nach dem vorherigen yield fortgesetzt wird, wird dieser Wert an die Stelle des vorher zurückgebenen gesetzt; im Prinzip steht dann dort die Zeile i = 42. Entsprechend ist das, was schließlich mit return zurückgegeben wird, der Wert 42.

var genFn = function*(){
  var i = yield 23;
  return i;
};
var gen = genFn();

// Ausführung bis zum yield; 23 wird durch yield ausgegeben
gen.next();   // > { value: 23, done: false }

// Fortsetzung ab dem yield; 42 wird an die Stelle des yield eingegeben
gen.next(42); // > { value: 42, done: true }

Ein Generator kann also nicht nur einen Wert zurückgeben und dann pausieren, wir können auch bestimmen mit welchem Wert der Generator weitermacht. Dieses Feature erlaubt es, aus einem Generator einen Wert auszuwerfen, den Wert zu transformieren und ihn dann wieder in den Generator hineinzustecken, der dann mit dem transformierten Wert fortfährt. Man könnte zum Beispiel einen Generator Objekte auswerfen lassen, die von der Außenwelt auf einen Wert reduziert und wieder in den Generator hineingeworfen werden:

var genFn = function*(){
  var sum = yield [1, 2, 3];
  var factor = 2;
  return sum * factor;
};

var gen = genFn();

var arr = gen.next().value;
var sum = arr.reduce(function(x, y){
  return x + y;
});
var result = gen.next(sum).value; // > 12

Der Generator wirft ein Array aus, das außerhalb des Generators zu einer einzigen Zahl reduziert und wieder in Generator hineingesteckt wird. Da dieses Hineinstecken genau an der Stelle des yield passiert, könnte man sagen dass sich der Ausdruck sum = yield [1, 2, 3] in sum = neuerWert verwandet; so erhält sum als Wert die Zahl, die außerhalb des Generators aus dem Arrays errechnet wurde. Durch diese Zuweisung an sum kann der Generator den außen errechneten Wert dann weiterverwerten, ihn mit dem factor multiplizieren und das finale Endergebnis ausgeben.

Besonders sinnvoll mag dieses Beispiel nicht erscheinen, was aber vor allem daran liegt, dass es eine ganz wesentliche Eigenschaft von Generators gar nicht benutzt; die Pause-Funktion! Das Reduzieren des Arrays könnte ja theoretisch auch eine lang dauernde, asynchrone Operation sein. Wenn dies der Fall wäre, würde das aber den Code der generator function gar nicht berühren:

var genFn = function*(){
  var sum = yield [1, 2, 3];
  var factor = 2;
  return sum * factor;
};

var gen = genFn();

var arr = gen.next().value;

// Asynchrones errechnen des Wertes aus dem Array
setTimeout(function(){
  var sum = arr.reduce(function(x, y){
    return x + y;
  });
  var result = gen.next(sum).value; // > 12
}, 1000);

Die durch die generator function beschriebene Sequenz bleibt, wie sie ist; dass anderswo asynchrone Operationen passiert, ist egal, denn nach dem yield ist der Generator schließlich pausiert. Er wartet auf das nächste next(), bevor er seine Berechnung zum Ende bringt. Wenn man das zuende denkt, könnte man Code formulieren, der eine generator function so verarbeitet, dass der Code der Funktion lediglich eine Abfolge von asynchronen Operationen darstellt und die Aufrufe von next() automatisch passieren. In unserer täglichen Arbeit schreiben wir also nur noch so etwas …

async(function*(){
  var wert1 = yield macheWasAsynchrones();
  var wert2 = yield macheWasAnderesAsynchrones();
  whatever(wert1 + wert2);
});

… und die async()-Funktion kümmert sich um den Rest. Wir können uns damit von der callback hell für immer verabschieden und es ist noch nicht mal schwer: wir müssen nur das bisher über Generators gelernte mit althergebrachten Promises kombinieren.

Rein-Raus-Ajax via Generator

Startet man einen Ajax-Request mit jQuerys $.get() so gibt diese Funktion ein Objekt zurück, das die asynchrone Operation kapselt. An dieses Objekt lassen sich Callbacks hängen, die feuern sobald die Ajax-Operation abgeschlossen ist.

var operation = $.get('/api/foo');
operation.then(function erfolgCallback(data){
  console.log('Yay!', data);
}, function failCallback(){
  console.log('Ups!');
});

Diese Objekte gibt es nicht nur bei Ajax-Requests und nicht nur in jQuery, sondern sehr viele JS-Libraries können derartiges liefern. Diese promises genannten Objekte abstrahieren alle Arten von asynchrone Operationen hinter einer immergleichen API; sie alle produzieren Objekte mit einer then()-Methode und triggern Callbacks wenn die hinter den Kulissen ablaufende Operationen beendet werden. Objekte und asynchrone Operationen? Das klingt nach einem Job für einem Generator! Die generator function müsste einfach Promises auswerfen …

var genFn = function*(){
  $('#Alpha').text(yield $.get('/api/alpha'));
  $('#Beta').text(yield $.get('/api/beta'));
  $('#Gamma').text(yield $.get('/api/gamma'));
};

… die durch externen Code aufgelöst und deren Resultate zurück in den Generator gegeben werden:

function async(genFn){
  var gen = genFn();
  var resume = function(promise){
    return promise.done(function(text){
      var next = gen.next(text);
      if(!next.done){
        return resume(next.value);
      }
    });
  };
  return resume(gen.next().value);
}

Fertig! (funktioniert Stand Ende 2013 in Chrome mit aktiviertem Experimentelles-JS-Flag und Firefox-Vorabversionen)

Die async()-Funktion nimmt eine generator function und erstellt aus ihr den Generator gen. Die resume()-Funktion nimmt ein Promise (bezogen aus gen.next().value) und hängt dort mittels done() einen Callback an. Feuert der Callback, wird dessen erster Parameter in den Generator hereingesteckt und sofern der Generator nicht schon sein letztes yield hinter sich hatte das nächste Promise in resume() gesteckt. So fertigt async() Schritt für Schritt die Generator-Sequenz ab, ganz ohne dass der Autor der Sequenz auch nur einen Moment lang über Callbacks nachdenken müsste, vorausgesetzt die yields im Generator geben immer Promises zurück.

In ihrer jetzigen Form ist die async()-Funktion freilich noch ausbaufähig; zum Beispiel hat sie noch keine Zeile für den Fall, dass eine der Async-Operationen fehlschlägt. Fehlerbehandlung nachzurüsten ist aber kein Problem, denn die Generator-API hat neben next() noch ein paar weitere nützliche Funktionen.

Weitere Generator-Funktionen

Neben next() bieten Generators auch eine throw()-Methode. Diese erlaubt es, einen Wert in den Generator zu werten, an der Stelle des letzten yield eine Exception auslöst. So kann man problemlos Fehler, die in asynchronen Operationen außerhalb des Generators passieren an die das Problem auslösende Stelle im Generator zurückführen: und schon funktioniert try-catch mit asynchronem Code!

var genFn = function*(){
  $('#Alpha').text(yield $.get('/api/alpha'));
  $('#Beta').text(yield $.get('/gibt/es/nicht/404')); // Exception tritt hier auf
  $('#Gamma').text(yield $.get('/api/gamma'));
};

function async(genFn){
  var gen = genFn();
  var resume = function(promise){
    return promise.done(function(text){
      var next = gen.next(text);
      if(!next.done){
        return resume(next.value);
      }
    }).fail(function(err){
      gen.throw(err.status);
    });
  };
  return resume(gen.next().value);
}

try {
  async(genFn);
} catch(e){
  console.log(e);
}

Außerdem können Generators mit den in ES6 ebenfalls neuen neuen for-of-Schleifen benutzt werden (Demo):

var genFn = function*(){
  var i = 0;
  while(i < 100){
    yield i++;
  }
};

var gen = genFn();

for(var num of gen){
  console.log(num);
}

Die for-of-Schleife ist ein universeller Iterationsmechanismus, mit dem in ES6-JS nicht nur Generators, sondern alle möglichen Sorten von Objekten verarbeitet werden können. Das Iterator-Konzept ist allerdings ein eigenes Thema für einen späteren Zeitpunkt – spannender dürfte aktuell die Frage sein, wie es denn um die Implementierung von Generators in heutigen JavaScript-Engines bestellt ist.

Unterstützung für Generators

Native und weitgehend standardkonforme Unterstützung für Generators findet sich Stand Ende 2013 in Chrome (vorausgesetzt der Exterminelles-JavaScript-Flag wurde in den Einstellungen aktivieren), in Firefox-Vorabversionen und in mit dem Parameter --harmony gestartenen Node-Umgebungen. Außerdem kann die Generator-Syntax nebst allen APIs vom Traceur-Compiler in JavaScript übersetzt werden, das jeder heutige Browser versteht. Erwartungsgemäß vervielfacht sich durch den Übersetzungsprozess die Code-Menge aber im Prinzip gilt: Generators funktionieren!

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