Die Javascript-Engines moderner Browser sind leistungsfähig genug, um die Anwendungslogik zu übernehmen. Wie auf dem Server beschleunigen auch hier Frameworks die Entwicklung. Ein herausragendes Beispiel für diese Gattung ist Backbone.js, das ein lokales Datenmodell umsetzt.
Seit mehr alseinem halben Jahrzehnt gibt es Webseiten, die ohne Reload auf Benutzereingaben reagieren. Im Zuge der Ajax-Revolution sind die einst lausigen Javascript-Interpreter der Browser so weit gereift, dass sie die verbreiteten Server-seitigen Skriptsprachen in vielen Benchmarks überflügeln [1]. Zahlreiche Javascript-Frameworks [2] sorgen zudem dafür, dass das Programmieren Client-seitigen Codes flott von der Hand geht.
Es liegt also nahe, Teile der Anwendungslogik auf den Client zu verlagern, wo sie den Webserver nicht belasten. Dass sich dabei sogar Datenbank-typische Funktionalität in Javascript effizient umsetzen lässt, zeigt dieser Artikel anhand einer Sportdatenbank (Abbildung 1). Die Beispielanwendung speichert mehrere Datensätze zu sportlichen Aktivitäten wie Joggen, Radfahren oder Schwimmen und kann die zurückgelegte Strecke und die verbrannten Kilojoule berechnen.

Abbildung 1: Mit dem Javascript-Framework Backbone.js gelingen einfache Datenbankanwendungen wie diese Sportdatenbank rein Client-seitig im Browser.
Als Basis verwendet sie das Javascript-Framework Backbone.js [3]. Der vollständige Quellcode der Anwendung steht unter [4] zum Download bereit. Zeilenangaben im Text beziehen sich größtenteils auf die darin enthaltene und im Artikel nicht abgedruckte Datei »app.js« , die den Javascript-Code enthält.
Das MIT-lizenzierte Javascript-Framework Backbone.js entstand als Auskopplung grundlegender Javascript-Methoden aus dem Document-Cloud-Projekt [5], einem Webdatenbank-Service für eingescannte Dokumente. Die Document-Cloud-Software verwaltet Dokumente und Projekte sowie Anmerkungen in Javascript.
Daten als Rückgrat
Das Framework nach dem Architekturmuster “Model View Controller” (MVC) (Abbildung 2) dient zur effizienten Verwaltung größerer Datenmengen im Browser. Dafür stellt es die Klassen »Model« und »Collection« zur Verfügung, die sich wie einzelne Datensätze beziehungsweise wie eine ganze Datenbanktabelle verhalten.

Abbildung 2: Das MVC-Framework Backbone.js betont die Model-Komponente mit einer Server-Synchronisation sowie Aggregations- und Suchfunktionen.

Abbildung 3: Schachtel und Stapel: »Backbone.Model« und »Backbone.Collection« zusammengenommen verhalten sich wie eine Datenbanktabelle.
Backbone.js benötigt die Lowlevel-Utility-Sammlung Underscore.js [6] und die DOM-Bibliothek Jquery [7], die sich aber durch den leichtgewichtigen Nachbau Zepto [8] ersetzen lässt. In älteren Browsern, die die Methode »JSON.stringify()« nicht nativ unterstützen, ist die Hilfe der Kompatibilitätsschicht Json2 [9] gefragt. Als Container für einzelne Datensätze (Abbildung 3, rotes Segment) mit den Feldern »Sportart« , »Datum« , »Strecke« , »Energie« , »Startpunkt« und »Dauer« dient in der Beispielanwendung die Model-Klasse »ExercisePrototpye« (»app.js« , Zeilen 9 bis 43), die Listing 1 zeigt.
Listing 1
Model-Klasse ExercisePrototype
01 var ExercisePrototype = Backbone.Model.extend({
02 defaults:{sportart:'', datum:'', startpunkt:'', strecke:0, dauer:0, energie:0}
03
04 // siehe Kasten "Werte berechnen"
05 compute:function(){ [...] }
06 computeMethods:{ [...] }
07 )};
Alle Klassen von Backbone bringen zur Ableitung von Kindklassen die Methode »extend()« mit. Sie vereinfacht die Vererbung, für die in Javascript sonst mehrere Zeilen nötig sind [10]. Als Parameter akzeptiert »extend()« ein Javascript-Objekt, um dessen Attribute es die Backbone-Basisklasse erweitert. Bei den Attributwerten darf es sich auch um anonyme Funktionen handeln.
Getreu dem Model-View-Controller-Schema bettet die Beispielanwendung alle Methoden zur Datenberechnung in die Model- und Collection-Klassen ein. Dabei erzeugt der Code
var myModel = Backbone.Model.extend({methodA: function(){ [...] }})
beispielsweise die Methode »myModel.methodA()« .
Einigen Methoden- und Attributnamen weist Backbone eine feste Rolle zu: Das »defaults« -Attribut (»app.js« , Zeile 11) bewirkt, dass mit »var Exercise = new Prototypenserie()« frisch erzeugte Model-Instanzen die dort festgelegten Felder und Werte enthalten. Beim Instanzieren mit »new« rufen Models und Collections außerdem die »initialize()« -Methode auf, sofern sie definiert ist.
Sport-Kollektion
In der Sportdatenbank darf der Anwender durch die Datensätze blättern, neue Datensätze hinzufügen sowie bestehende löschen. Die Collection (Abbildung 3), die als Container für die Datensätze dient, ist in den wenigen Zeilen von Listing 2 definiert (»app.js« , Zeile 58).
Listing 2
Collection
01 var ExerciseCollectionPrototype = Backbone.Collection.extend({
02 model: Prototypenserie,
03 computeSums:function(){
04 /* siehe Kasten "Werte berechnen" */
05 }
06 });
07 var ExerciseCollection = new ExerciseCollectionPrototype();
Wieder kommt »extend()« zum Einsatz, um die Backbone-Klasse zu beerben. Das an »extend()« übergebene Objekt legt mit seinem Attribut »model« fest, dass »ExerciseCollection.add()« die Collection um ein Model der Klasse »ExercisePrototpye« erweitert. Dabei kommen die im Model als »defaults« definierten Felder und Startwerte zum Tragen. Damit steht das Grundgerüst der Datenbank, das die Übungseinheiten verwaltet: Die Collection »ExerciseCollection« sammelt Models vom Typ »Exercise« , der gerade offene Datensatz steht in »currentExercise« .
Mit besten Referenzen
Für das Verhältnis von Model und Collection ist zu bedenken, dass die Programmiersprache Javascript Objekte grundsätzlich als Referenzen weitergibt. Alle Veränderungen an einem Model fließen daher stets in die Collections ein, die das Model enthalten. Das gilt auch für die »destroy()« -Methode des Model: »Exercise.destroy()« verkürzt die »ExerciseCollection« um einen Eintrag.
Der Code »var Exercise = ExerciseCollection.get(n)« holt das Model, das den n-ten Datensatz enthält, aus der Collection. Über Getter- und Setter-Methoden lassen sich Felder auslesen oder verändern:
var sportart = Exercise.get('sportart');
Exercise.set({dauer:1, strecke:2.3});
In die Nähe vollwertiger Datenbanken rückt die Backbone-Collection mit ihrer »where()« -Methode: Der Ausdruck »Collection.where({Attribut:Wert, […]})« gibt ein Array aller Models in der Collection zurück, deren Feldwerte zum als Parameter übergebenen Hash passen.
Die Methode »pluck(Attribut)« liefert ein Array mit allen Werten für ein Feld aus allen Models der Collection und ähnelt damit der SQL-Anweisung »SELECT« . Außerdem erben Backbone-Collections 28 Underscore-Methoden, die über die Datensätze der Collection iterieren. Das Spektrum reicht von einem einfachen »each()« über »groupBy(function(){return Gruppierungskriterium})« bis zu den aus der funktionalen Programmierung bekannten »map()« und »reduce()« [11].
Gemischte Daten
Collections müssen nicht ausschließlich aus Models einer einheitlichen Klasse bestehen. Das »model« -Attribut garantiert dies zwar, wenn »Collection.add()« entweder ohne oder mit einem bloßen Attribute-Werte-Hash als Parameter aufgerufen wird. Im Allgemeinen akzeptiert »add()« aber auch Model-Instanzen als Parameter, die beliebige Felder enthalten dürfen. Anders als SQL-Datenbanken erzwingt »Backbone.Collection« also kein durchgängiges Datenschema.
Eine der wichtigsten Funktionen von »Backbone.Model« und »Backbone.Collection« ist der Abgleich mit einem externen Datenbankserver [12]. Die Beispielanwendung bindet allerdings das Local-Storage-Plugin [13] ein, das die Methoden zur Datensynchronisation in Backbone überschreibt. Die Daten landen hier im Local-Storage-Modul des Webbrowsers [14]. Darum lässt sich das Programm in modernen Browsern direkt vom lokalen Dateisystem aus starten.
Treibende Kraft
Die Methoden von Model und Collection werden gemäß MVC-Paradigma nicht von sich aus aktiv. Der Controller-Part der Anwendung ruft sie auf, wenn der Anwender Daten eingegeben oder einen Button angeklickt hat. Hier kommt die Backbone-Klasse »View« ins Spiel.
Objekte der Klasse »Backbone.View« übernehmen das Eventhandling, das eigentlich in den Controller-Part gehört. Der Klassenname passt also nicht zur üblichen Aufgabenteilung im MVC-Paradigma. Javascript greift jedoch sowohl für Anpassungen der angezeigten HTML-Seite als auch für die Eventhandler auf die DOM-Schnittstelle zurück. Daher ist es plausibel, beides in eine gemeinsame Klasse zu packen.
Eine »View« steht in Backbone deshalb für einen zusammenhängenden interaktiven Seitenbereich. View-Klassen verwalten sowohl die Benutzereingaben als auch Veränderungen am Aussehen in diesem Anzeige-Ausschnitt.
Die Haupt-View in der Sportdatenbank (»MainView« , Klassendefinition »MainViewPrototype« in »app.js« , Zeile 81) deckt sich mit dem inneren grauen Rahmen im Anwendungsfenster (Abbildung 4). Wie die Model- und Collections-Klassen leitet sie sich über die »extend()« -Methode von der Backbone-Basisklasse ab (Listing 3).
Listing 3
MainView
01 var mainViewPrototpye = Backbone.View.extend({
02 el : '#maintable',
03 events:{"change select":"SportChanged",
04 "change input" :"DataChanged"},
05 });

Abbildung 4: Die Klasse »View« übernimmt in Backbone mit dem Eventhandling die Aufgaben der Controller-Sparte der Javascript-Anwendung. Eine eigene Controller-Klasse gibt es nicht.
Ereignisgesteuert
Das »el« -Attribut bindet die View an ein DOM-Element der HTML-Seite. Dabei kommt die Jquery-Selektor-Syntax [15] zum Einsatz, »#maintable« wählt das DOM-Elemente mit der ID »maintable« . Das als »events« übergebene Objekt setzt zwei Eventhandler.
Deren Attributnamen bestehen aus zwei Teilen. Der erste nennt den Eventnamen: »change« sendet der Browser, wenn der Anwender den Wert eines Eingabefelds verändert. Der zweite ist ein Jquery-Selektor, der die HTML-Elemente definiert, die der Handler überwacht. Im Beispiel sind dies alle Texteingabefelder (»<input>« ). Bei jeder Veränderung hält der Eventhandler das Model aktuell. Dafür ist die Methode »DataChanged()« der View zuständig. Die Handler-Methoden sind als Wert für die eben erläuterten zweiteiligen Attributnamen anzugeben.
Wenn der Anwender den Wert für »Strecke« , »Dauer« oder »Energie« verändert hat, ruft »DataChanged()« außerdem »compute(Feldname)« auf, um die anderen beiden Felder den definierten Relationen gemäß anzupassen. In der Sportdatenbank gibt es nur ein einziges »<select>« -Element: das Dropdown-Feld für die Sportart, auf das sich die zweite Zeile im Events-Attribut bezieht. Veränderungen dieses Feldes brauchen eine Sonderbehandlung im Eventhandler »SportChanged()« , denn beim Wechsel der Disziplin sind neue Werte für Geschwindigkeit und Energieverbrauch zu laden. Der Kasten “Werte berechnen” erläutert, wie dies funktioniert.
Werte berechnen
Über das reine Verwalten der Daten hinausgehend, das die Backbone-Standardklassen abdecken, braucht die Model-Komponente der Sportdatenbank noch Methoden zur Datenberechnung.
Ändert der Benutzer eines der Felder »Strecke« , »Dauer« oder »Energie« , aktualisiert »compute(Feldname)« (»app.js« , Zeilen 14 bis 22) die Werte nach pro Sportart festgelegten Relationen. Der Hash »computeMethods« enthält Lambda-Funktionen, von denen jede für Veränderungen in einem der Felder »Strecke« , »Dauer« und »Energie« zuständig ist.
Die Werte für Geschwindigkeit und Energieverbrauch pro Stunde sind in der Beispielanwendung für jede Sportart in der Collection »SportsParameters« abgelegt. Der Eventhandler »SportChanged()« (»app.js« , Zeilen 126 bis 137) am Eingabefeld »Sportart« holt ein neues Model aus der Collection »SportsParameters« , wenn sich die Sportart ändert. Dazu nutzt er die »where()« -Methode der Backbone-Collections: »SportsParameters.where(sportart:Neue Sportart)« (»app.js« , Zeile 131) liefert ein Array mit einem Model aus der Collection als einzigem Treffer.
Die Gesamtsummen der Felder »Strecke« , »Dauer« und »Energie« errechnet »computeSums()« (»app.js« , Zeilen 63 bis 75) in der »Exercise-Collection« . Die Methode iteriert über das Array »Fields = [“strecke”, “dauer”, “energie”]« . Zur Summenberechnung für jedes dieser Felder nutzt sie eine Methode aus dem Standardinventar der Backbone-Collections.
Die Views in Backbone stellen wenig Funktionalität bereit, die über die zugrunde liegenden Jquery-Methoden hinausgeht. Die Backbone-Entwickler verstehen sie in erster Linie als Vorschlag für ein einheitliches Bauprinzip der Controller-Klassen. Im Vergleich zum Setzen der Eventhandler mit Jquery, das auch nicht mehr Code benötigt, ergeben sich dennoch zwei Vorteile: Die Selektoren suchen nur innerhalb des mit »View.el« festgelegten Seitenausschnitts, was das Wiederverwenden einer View an einer anderen Stelle der HTML-Seite erleichtert. Außerdem deutet »this« innerhalb der Eventhandler-Methoden auf das View-Objekt, nicht auf das auslösende DOM-Objekt, wie es beim direkten Einsatz von Jquery der Fall wäre. Wenn der View wie üblich ein Model zugeordnet ist, greifen Eventhandler daher einfach mit »this.model« darauf zu.
In der Beispielanwendung bindet der Konstruktor das Model »currentExercise« an die View (»app.js« , Zeilen 210 und 236):
var mainView = new mainViewPrototpye({model:currentExercise});
Eine wichtige Rolle im »MainView« -Objekt spielt noch die »initialize()« -Methode, die Backbone bei der Instanzierung aufruft (Listing 4). Die »on()« -Methode des zur View gehörigen Model setzt einen Eventhandler, der bei jeder Änderung der Daten die Ansicht neu rendert. Die Anzeige bleibt daher immer mit dem Model synchron. Die Zeile »this.render()« zeigt die View beim Instanzieren der Model-Klasse an.
Listing 4
initialize
01 initialize : function(){
02 this.model.on('change', this.render, this);
03 this.render();
04 },
Für die Navigationsbuttons und das Eingabefeld »Übungseinheit Nr.« ist eine zweite View namens »NavigatorView« zuständig. Diese knüpft Eventhandler an die Buttons und das Eingabefeld »Übungseinheit Nr.« .
Aushilfe gesucht
Für die Präsentationsschicht der Javascript-Anwendung gibt Backbone keine Hilfestellung. Underscore.js, auf dem Backbone aufsetzt, enthält aber ein einfaches Template-System, das Attribute eines Objekts in den Template-Code einbettet: »<%=Attribut%>« oder »<%-Attribut%>« . Die zweite Variante HTML-encodiert die übergebenen Werte zum Schutz vor XSS-Angriffen.
Der Template-Code steht in der Beispielanwendung so in der Seite »index.html« :
<script type="text/template" id="<i class="replaceable">Template-Name"></script>
Da der Browser den Inhaltstyp »text/template« nicht kennt, führt er ihn auch nicht aus. Dennoch lässt sich der Inhalt des »script« -Tag über das DOM auslesen (»app.js« , Zeile 110). Die Underscore-Methode »_.template(Template-Code)« gibt selbst wieder eine Funktion zurück, die in der Beispielanwendung an das Attribut »template« gebunden ist. Sie verhält sich in Javascript also wie eine Methode des »MainView« -Objekts.
Diese Methode erzeugt das HTML für die Anzeige der Übungseinheit dann in einer einzigen Zeile (»app.js« , Zeile 106):
this.$el.html(this.template(RenderData));
»RenderData« enthält einen Hash aus Feldname-Wert-Paaren des Model »currentExercise« , der sich mit der Backbone-Methode »currentExercise.toJSON()« erzeugen lässt. Im Template-Code finden sich die Texteingabefelder (»index.html« , Zeile 47) in der Form:
<input type="text" value="<%-<i class="replaceable">Feldname%>" name="Feldname"id="Feldname">
Das »$el« -Attribut eines View-Objekts ist ein von Backbone automatisch erzeugter Jquery-Selektor, der auf das DOM-Element zeigt, an das die View gebunden ist. In der Zeile
this.$el.html(this.template(Datenattribute des Models));
überschreibt also die Jquery-Methode »html()« die bisherige »MainView« mit dem HTML-Code, der beim Rendern des Template entstanden ist. Die Sportdatenbank zeigt nun die aktuellen Werte aus »currentExercise« an.
Vor dem Rendern hat die Underscore-Methode »_.extend()« die Variable »RenderData« noch um die Attribute des Ergebnis-Objekts aus »ExerciseCollection.computeSums()« erweitert (»app.js« , Zeile 104). Auch für diese Gesamtsummenwerte gibt es im Template »<%-Attributname%>« -Tags (»index.html« , Zeile 79).
Freiheit für die Daten
Backbone verwaltet Daten, hält sie mit dem Server synchron und feuert bei Veränderungen im Datenbestand Events ab. Dabei lässt es dem Entwickler alle Freiheit: Wie der Anwendungscode auf »change« -Events reagiert, gibt das Framework nicht vor.
Ob er die Views mit der einfachen Template-Engine der Backbone-Dependency Underscore rendert oder leistungsfähigere Alternativen wie Mustache.js [16] ins Boot holt, bleibt ihm ebenfalls überlassen. Zusätzlich zu den in der Beispielanwendung genutzten Klassen gibt es noch »Backbone.Router« , das den Client-seitigen Anwendungsstatus an den URL-Hash (»http://pfad#Hash« ) koppelt.
K. o.
Wem diese Gewichtung nicht zusagt, sollte sich Knockout.js [17] ansehen (Kasten “Knockout.js”): Dieses Framework legt das Hauptgewicht nicht auf das Model, sondern auf Template-basierte Views und die Datenbindung zwischen Model und View. Diese kommen rein deklarativ durch Pseudo-Attribute im Template-Code zustande. In Knockout sind Kürze und Eleganz des Codes wichtiger als die Freiheiten des Entwicklers. (mhu)
Knockout.js
Backbones Kernfunktionalität besteht aus Collections und Models, die bei einer Datenänderung Events aussenden. Wie die Anwendung darauf reagiert, bleibt dem Entwickler selbst überlassen. Das wie Backbone MIT-lizenzierte Knockout.js [17] legt den Schwerpunkt dagegen auf die View-Komponente: Im Zentrum stehen Data-Bindings mit Hilfe von Pseudo-Attributen wie »data-bind=”text: Datenfeld”>« , die HTML-Elementen wie Div-Tags oder Input-Feldern einen dynamischen Wert aus einem Datenmodell zuweisen.
Knockout-Datenmodelle bestehen aus Javascript-Objekten, deren Attribute mit »ko.observable()« zu initialisieren sind:
var ViewModel = {
Name = ko.observable("Hans"),
Alter = ko.observable(34)
}
Beim Ändern des Namens mit »ViewModel.Name(“Jim”)« aktualisiert Knockout automatisch alle DOM-Elemente mit Datenbindung an »Name« wie zum Beispiel »<div data-bind=”text: Name> </div>« . Inputs mit Datenbindung (»<input data-bind=”value: Name” />« ) schreiben eingegebene Werte automatisch zurück in das Model.
Knockout baut die Datenbindungen zu einer vollwertigen Template-Sprache aus: Es gibt »foreach« – und »if« -Bindungen. Auch HTML-Code oder HTML-Attribute wie »style« lassen sich dynamisch gestalten. Standardmäßig interpretiert der Code »ko.applyBindings(new ViewModel({Attribut:Wert}))« die ganze HTML-Seite als Template, benannte Bereiche mit eigenem Model sind ebenfalls möglich.
Als Highlight in Knockout erweisen sich die »computed Observables« , dynamische Attribute, die Werte aus anderen Feldern ableiten. Hier verfolgt Knockout selbstständig die Abhängigkeit zurück und berechnet die abgeleiteten Werte immer dann neu, wenn sich eines der referenzierten Felder ändert.
Infos
- Programmiersprachen-Shootout: http://shootout.alioth.debian.org/u32/which-programming-languages-are-fastest.php
- Javascript-Framework-Überblick: http://net.tutsplus.com/articles/web-roundups/20-javascript-frameworks-worth-checking-out/
- Backbone.js: http://backbonejs.org
- Quellcode der Beispielanwendung: https://www.linux-magazin.de/static/listings/magazin/2012/11/javascript/
- Document Cloud: http://www.documentcloud.org
- Underscore.js: http://underscorejs.org
- Jquery: http://jquery.com
- Zepto: http://zeptojs.com
- Json2: https://github.com/douglascrockford/JSON-js/blob/master/json2.js
- Javascript-Inheritance: http://javascript.crockford.com/inheritance.html
- Collections: http://backbonejs.org/#Collection
- Backbone.Sync: http://backbonejs.org/#Sync
- Local-Storage-Plugin: http://documentcloud.github.com/backbone/docs/backbone-localstorage.html
- Andreas Möller, “Ohne Netz”: Linux-Magazin 10/12, S. 100
- Jquery-Selektoren: http://api.jquery.com/category/selectors/
- Mustache.js: https://github.com/janl/mustache.js/
- Knockout.js: http://knockoutjs.com






