Asynchrone Programmierung mit Promises

Seite 4: Einsatz

Inhaltsverzeichnis

Die Funktion im nächsten betrachteten Codebeispiel ähnelt der jQuery-Funktion jQuery.get, hat allerdings einen Nachteil: Der Response Body ist weiterhin händisch auszulesen. Das muss jedoch nicht sein und lässt sich ebenfalls mittels Promises abstrahieren, wie das Folgende zeigt.

var readBody = function(response) {
var deferred = Q.defer();

var data = [];
response.setEncoding('utf8');
response.on('data', function(chunk) {
data.push(chunk);
});
response.on('end', function() {
deferred.resolve(data.join());
});
response.on('error', function() {
var message = 'Failed reading body: ' + error;
deferred.reject(new Error(message));
});

return deferred.promise;
};

Zumindest auf Promise-Seite sollte das Listing keine Überraschungen beinhalten. Es wird, wie zuvor, ein Promise-Objekt erzeugt und der Status in Abhängigkeit zu den vorhandenen Events verändert. Als Parameter erwartet das Programm eine HTTP Response und das Promise wird mit dem Response Body in Form eines String aufgelöst. Zu guter Letzt fehlt lediglich der Aufruf der beiden Funktionen und deren Kombination:

get('https://api.github.com/rate_limit')
.then(function(response) {
console.log('Got response with status code %d',
response.statusCode);
return readBody(response);
})
.then(function(body) {
console.log('Response body: ', body);
})
.fail(function(error) {
console.error('Fehler: ', error);
})
.done();

Auch hier nutzt man wieder die Option, einen Wert über then zu transformieren. Die HTTP Response, die das Resultat des GET-Promise darstellt, wird mittels readBody in einen String umgewandelt. Der String, der den Response Body darstellt, gibt das Programm im zweiten then-Handler auf der Konsole aus.

Promises bilden den Status einer Operation als Wert ab und lassen sich daher sehr gut in Frameworks nutzen. Schöne Beispiele hierfür sind AngularJS und EmberJS: Beide Frameworks sind MV*-Frameworks, dienen also unter anderem zum Generieren und Aktualisieren von HTML, Reaktion auf Nutzereingaben und Routing. Letzteres beschreibt in dem Fall die Abbildung einer URI auf eine Programmlogik, was in der Regel mit einer Änderung der dargestellten Informationen einhergeht.

So könnte die Programmlogik für den Pfad / dafür verantwortlich sein, dem Benutzer eine Willkommensnachricht zu präsentieren. Der Pfad /movies könnte wiederum eine Liste bekannter Filme darstellen. Bei Single-Page-Applikationen, also solchen Anwendungen, die das anzuzeigende HTML auf der Client-Seite generieren, sind zur Darstellung der bekannten Filme Informationen (in der Regel in JavaScript Object Notation) vom Server zu beziehen. Nach erfolgreichem Herunterladen, ließe sich das zugehörige HTML erzeugen und der Pfad im Browser des Nutzers auf /movies ändern.

AngularJS implementiert ein solches Verhalten. Es gibt demnach Routen (oder Pfade), zu denen erst dann gewechselt werden kann, wenn die nötigen Daten vorhanden sind. Das erfolgreiche Laden der Daten ist somit eine Vorbedingung für den Wechsel auf einen Pfad. An der Stelle zeigt sich das Potenzial von Promises: Statt einen Request zum Bezug der Daten abzusetzen und abhängig vom Resultat einen Routenwechsel anzustoßen, ist es möglich, lediglich den Request zu initiieren. Das Framework erkennt anhand des Status der Promises selbstständig, ob der Routenwechsel stattfinden kann (Promise ist fulfilled und HTTP Status liegt im 200er Bereich) oder eine Fehlermeldung anzuzeigen ist. Effektiv lässt sich das mit dem Framework AngularJS wie folgt umsetzen:

function MoviesController(moviesResponse) {
// Controller Logik...
}

MoviesController.resolve = {
moviesResponse: function($http) {
return $http.get('/movies');
}
};

$routeProvider.when('/movies', {
controller: MoviesController,
template: 'movies.html',
resolve: MoviesController.resolve
});

Die ersten drei Zeilen zeigen einen sehr einfachen AngularJS-Controller. Er definiert eine Abhängigkeit moviesResponse, die AngularJS injiziert, sollte dem Framework eine Ressource mit diesem Namen bekannt sein. Da das im Beispiel nicht der Fall ist, ist zu definieren, um welchen Wert es sich handelt. Dafür kommt ein Controller-spezifischer Resolver zum Einsatz, der sich bei der Routendefinition angeben lässt. Letztere ist ab Zeile elf zu sehen. Hier ist lediglich der Verweis auf MoviesController.resolve von Interesse. MoviesController.resolve ist eine Map, die Ressourcen-Namen auf Funktionen zur Auflösung der Ressource abbildet.