Einstieg in die Entwicklung von Web-Apps mit Meteor
Seite 3: Mechanismen und Methoden
Publish/Subscribe und Methodenaufrufe
Um dem "Überall-Datenbank"-Gedanken entgegenzuwirken und nicht die gesamte Datenbank automatisch allen Clients zugänglich zu machen (autopublish-Flag), lassen sich sensible oder rechenintensive Operationen auch auf dem Server durchführen und die aktiven Clients über das Ergebnis benachrichtigen. Ähnlich wie beim allgemein bekannten Messaging-Verfahren mittels Topics heißt hier der Mechanismus "Publish/Subscribe". Der Server veröffentlicht (publish) das Ergebnis einer Datenbank-Abfrage beziehungsweise Berechnung in Form einer Kollektion, und alle interessierten Clients abonnieren (subscribe) die Events der Veröffentlichung. Auf diese Weise lässt sich verhindern, dass alle Änderungen einer Kollektion an alle Clients propagiert werden und alle Felder öffentlich sind.
Meteor.publish("players", function() {
return Players.find({}, {limit: 10, sort: {score: -1, name: 1}});
});
Das Selektor-Objekt als zweiter Parameter in der find-Anweisung filtert die zehn höchsten Punktstände und liefert die Spieler nach Punkten absteigend sowie nach Namen aufsteigend als Ergebnis zurück. In der Datei server.js ist der Befehl Meteor.publish für die Bekanntgabe der Kollektion an die Clients verantwortlich.
Der Client abonniert die mit dem Bezeichner players ausgestattete Kollektion via
Meteor.subscribe("messages");
Positiver Nebeneffekt: Es werden weniger Daten ausgetauscht. Änderungen im Highscore auf den Plätzen größer zehn spielen keine Rolle für die Darstellung im Client.
Eine weitere Methode, um Berechnungen beziehungsweise Logik vom Client auf den Server zu verlagern und über eine Art "Remote Procedure Call" (RPC) aufzurufen, sind Meteors sogenannte "Method Calls". Mit ihnen lassen sich auf dem Server eine oder mehrere Methoden definieren und als per Remote aufrufbar registrieren. Beispiele hierfür sind das zufällige Selektieren einer neuen Frage oder das Aktualisieren des Punktestandes:
Meteor.methods({
getRandomQuestion: function() {
var q = Questions.find().fetch();
q = _.shuffle(q);
return q[0];
},
updateScore: function(playerId, points) {
Players.update(playerId, {$inc: {score: points}});
}
});
Der Client benötigt keine weitere Konfiguration oder Bekanntmachung der Server-Methoden und kann sie direkt erreichen:
// async call
Meteor.call("getRandomQuestion", _onQuestionReceive);
Im Falle eines asynchronen Aufrufs wird als letzter Parameter eine Callback-Funktion übergeben, die nach dem Abarbeiten der eigentlichen Funktion aufzurufen ist. Ihr lässt sich entweder ein Ergebnis-Objekt (mit den Ergebnissen der Berechnung) oder im Fehlerfall ein Error-Objekt übergeben. So kann die Anwendung auf Fehler in der asynchronen Berechnung reagieren. Ein synchroner Aufruf ist ebenfalls möglich, es wird dann auf die Callback-Funktion verzichtet und der Rückgabewert der Funktion direkt einer Ergebnisvariablen zugeordnet.
Registriert man die rechnerferne Methode nicht nur auf dem Server, sondern mit gleichem Namen auch auf dem Client, lässt sich das Prinzip der "Latency Compensation" ganz leicht umsetzen. In dem Fall wird zwar die Methode auf dem Server aufgerufen und abgearbeitet, gleichzeitig aber ebenfalls die clientseitige Methode aufgerufen und ihr vorläufiges "Ergebnis" (zum Beispiel ein Platzhalter wie "Ihre Daten werden berechnet") an den Aufrufenden geliefert. Sobald das Ergebnis vom Server vorliegt, ersetzt Meteor automatisch das Client-Ergebnis mit diesem. So lassen sich pseudolatenzfreie Anwendungen simulieren und für den Anwender ist ein flüssigeres Arbeiten möglich.
Den Meteor lenken
Für das Implementieren von Views hat sich im JavaScript-Bereich das Arbeiten mit Templates in Verbindung mit dem MVVM-Entwurfsmuster (Model, View, ViewModel) etabliert. Meteor bringt Handlebars für diese Aufgaben mit. Laut Dokumentation ist es allerdings auch möglich, andere Templating-Engines einzubinden.
Handlebars-Templates werden in Dateien mit der Endung "html" abgelegt und mit den Tags <template name="..">..</template> definiert, innerhalb welcher normales HTML zu verwenden ist. Mit der Anweisung {{> name}} lassen sich Templates über ihren Namen an verschiedenen Stellen einbinden. In der Beispiel-Applikation sind sie in thematische Dateien organisiert, so liefert die wichtigste Vorlage mit dem Namen "pokerquiz.html" beispielsweise das Grundgerüst und ruft die anderen Bereiche auf.
Das Zusammenspiel zwischen Server, Client und der Template-Engine lässt sich gut am Ranking-Template, das einen Highscore darstellt, beobachten. Das Propagieren der Daten auf den Client wurde oben bereits angesprochen. Im Client steht die Kollektion zur Verfügung und lässt sich mit der Anweisung Template.ranking.players unter dem Namen players für das Template bekannt machen. Die Sortierung innerhalb der Kollektion ist analog zu der auf dem Server, damit sich Änderungen unmittelbar auswirken, noch bevor die Daten an ihn weitergereicht wurden.
Template.ranking.players = function () {
return Players.find({}, {sort: {score: -1, name: 1}});
};
In der Template-Datei ranking.html ist zuerst das Subtemplate ranking definiert, das im Haupt-Template eingebunden ist. Wesentliches Element ist die each-Anweisung in der Tabelle:
{{#each players}}
{{> player}}
{{/each}}
Sie iteriert über die oben definierte Players-Kollektion und ruft für jede Zeile das Subtemplate player auf. Es füllt die Tabelle mit Inhalt und baut für jeden Spieler eine Zeile mit dem Namen des Spielers und dessen aktuellen Punktestand auf.
<template name="player">
<tr>
<td>{{name}}</td>
<td class="text-right">{{score}}</td>
</tr>
</template>
Neben Schleifen sind Bedingungen sinnvoll, die Template-Bereiche je nach Zustand ein und ausschalten. Im Template "pokerquiz.html" findet sich ab Zeile 19 eine Verzweigung: Nur angemeldete Benutzer sehen Highscore, Quizfragen und Statusmeldungen. Im else-Zweig wird die Aufforderung zum Anmelden präsentiert:
{{#if currentUser}}
<div class="row-fluid">
<div class="span4">
{{> ranking}}
</div>
<div class="span8">
{{> questions}}
</div>
</div>
...
{{else}}
<div class="container"><div class="hero-unit">
<p>Please login in order to get in the play!</p>
</div></div>
{{/if}}
HTML live erleben
Meteor geht mit seinen "Live-HTML"-Features allerdings noch einen Schritt weiter. Mit der Funktion Meteor.render() erzeugt man HTML-Fragmente, die sich automatisch aktualisieren, sobald sich die Daten ändern, von denen sie abhängen (reaktives Vorgehen). Das folgende Beispiel zeigt den Einsatz:
var fragment = Meteor.render(
function () {
var name = Session.get("name") || "Anonymous";
return "<div>Hello, " + name + "</div>";
});
document.body.appendChild(fragment);
Meteor.render() wird eine anonyme Funktion übergeben, die definiert, wie ein HTML-Fragment aufgebaut ist. Die Daten liegen in diesem Fall in einer Session-Variablen vor, appendChild() fügt das Fragment dem aktuellen HTML-Dokument hinzu. Ändert sich nun der Inhalt der Variablen, aktualisiert sich das Fragment automatisch, denn die Session-Funktionen get() und set() sind reaktiv ausgelegt:
Session.set("name", "Bob");
Der Entwickler baut lediglich die Verknüpfung auf und erhält die Aktualisierung der Darstellung vom Framework geschenkt.