Model View Controller mit Backbone.js
Backbone.js ist das mit Abstand am weitesten verbreitete MVC-Framework fĂŒr die Entwicklung von Single-Page-Anwendungen â doch ist es auch das Beste? Um diese Frage beantworten zu können, muss man zunĂ€chst die Konzepte von Backbone.js verstehen und einordnen.
Backbone.js ist das mit Abstand am weitesten verbreitete MVC-Framework fĂŒr die Entwicklung von Single-Page-Anwendungen â doch ist es auch das Beste? Um diese Frage beantworten zu können, muss man zunĂ€chst die Konzepte von Backbone.js verstehen und einordnen.
Anders als Knockout.js [1] verwendet Backbone.js nicht das moderne Entwurfsmuster Model View ViewModel (MVVM), sondern das deutlich Àltere und daher weiter verbreitete Model View Controller (MVC). Allerdings passt es einige Aspekte von MVC an seine individuellen Anforderungen an, setzt MVC also nicht in Reinform um.
Die ursprĂŒngliche Idee des MVC-Entwurfsmusters war, einer Anwendung eine Komponente hinzuzufĂŒgen, die zwischen der Darstellung der Daten und der fachlichen DomĂ€ne vermittelt. Auf diesem Weg kann man Eingaben gebĂŒndelt verarbeiten und im Anschluss alle relevanten Ansichten aktualisieren, ohne die hierfĂŒr erforderliche Logik replizieren oder verteilen zu mĂŒssen.
Den Kern von Backbone.js bildet die Klasse Backbone.Events, von der sich wiederum Klassen wie Backbone.Model und Backbone.Collection ableiten. Da sie das Beobachtermuster implementiert, ermöglicht Backbone.Events eine entkoppelte und zugleich standardisierte Kommunikation innerhalb einer Backbone.js-Anwendung.
FĂŒr die Vererbung greift Backbone.js auf die Bibliothek Underscore.js zurĂŒck, die fĂŒr zahlreiche Webentwickler ohnehin zur Standardausstattung gehört. Dass die Wahl ausgerechnet auf sie gefallen ist, ist kein Zufall: Underscore.js stammt vom gleichen Autor wie Backbone.js und ist aus diesem hervorgegangen.
Um eine DomÀne zu modellieren, kennt Backbone.js die beiden bereits angesprochenen Klassen Backbone.Model und Backbone.Collection: WÀhrend erstere dazu dient, einzelne EntitÀten zu beschreiben, stellt letztere eine Liste dieser dar. Die beiden Klassen bilden also eine 1:n-Beziehung.
Eine EntitĂ€t kann in Backbone.js nicht nur Daten, sondern auch GeschĂ€ftsfunktionen enthalten; gleiches gilt fĂŒr eine EntitĂ€tsliste. Referenzen zwischen EntitĂ€ten unterstĂŒtzt Backbone.js dabei allerdings nicht: Es bleibt dem Entwickler ĂŒberlassen, Beziehungen entweder ĂŒber das manuelle Verwalten von IDs oder das Verwenden von Aggregation zu modellieren.
Backbone.js enthĂ€lt auĂerdem die Klasse Backbone.View, die der Anzeige von EntitĂ€ten und Listen dient. Allerdings kapselt eine View lediglich den Zugriff auf den Webbrowser, die eigentliche Aktualisierung der grafischen OberflĂ€che fĂŒhrt Backbone.js nicht durch. HierfĂŒr muss der Entwickler auf eine Bibliothek wie jQuery zurĂŒckgreifen.
Implementierung
Implementieren der Konzepte
Um Backbone.js zu verwenden, ist zunĂ€chst die passende Skriptdatei von der Webseite der Bibliothek herunterzuladen [2] und einzubinden. Da Backbone.js unter anderem fĂŒr die Vererbung auf Underscore.js [3] zurĂŒckgreift, gilt fĂŒr sie das Gleiche. DarĂŒber hinaus empfiehlt die Dokumentation von Backbone.js den Einsatz von JSON3 [4] und jQuery [5]:
<!doctype html>
<html>
<head>
<title>Backbone.js-Demo</title>
</head>
<body>
<script type='text/javascript' src='json3.min.js'></script>
<script type='text/javascript' src='jquery-2.0.3.min.js'></script>
<script type='text/javascript' src='underscore-min.js'></script>
<script type='text/javascript' src='backbone.js'></script>
</body>
</html>
AnschlieĂend kann man die beiden Klassen Backbone.Model und Backbone.Collection verwenden, um die DomĂ€ne mithilfe von EntitĂ€ten und Listen zu modellieren. Ausgangspunkt dafĂŒr ist stets das Ableiten von einer der beiden Klassen, wobei man die Modellierung in der Regel mit einer EntitĂ€t beginnt:
var Customer = Backbone.Model.extend({
defaults: {
firstName: '',
lastName: ''
}
});
Die defaults-Eigenschaft definiert die Standardwerte einer neuen EntitĂ€t. Customer stellt allerdings noch keine solche dar, sondern lediglich deren Blaupause. Um einen konkrete EntitĂ€t zu erzeugen, muss man Customer als Konstruktor aufrufen. Dabei kann man die Werte der EntitĂ€t als Parameterobjekt ĂŒbergeben:
var customer = new Customer({
firstName: 'Golo',
lastName: 'Roden',
age: 34
});
Um auf die Werte zu einem spĂ€teren Zeitpunkt wieder zugreifen zu können, stellt Backbone.js die beiden Funktionen get und set zur VerfĂŒgung:
console.log(customer.get('firstName')); // => 'Golo'
customer.set('age', customer.get('age') + 1);
Möchte man mehrere Werte gleichzeitig setzen, lĂ€sst sich der set-Funktion ein Parameterobjekt ĂŒbergeben, Ă€hnlich wie beim Aufruf des Konstruktors.
UnabhĂ€ngig davon, wie die neuen Werte gesetzt werden, benachrichtigt die EntitĂ€t die AuĂenwelt ĂŒber ihre VerĂ€nderung: Zum einen löst sie ein allgemeines change-Ereignis aus, zum anderen ein spezielles change:*-Ereignis fĂŒr jeden einzelnen geĂ€nderten Wert. Da Backbone.js jeder EntitĂ€t ĂŒber die Basisklasse Backbone.Events eine on-Funktion zur VerfĂŒgung stellt, ist es ein Leichtes, auf diese Ereignisse zu reagieren:
customer.on('change:lastName', function (model, value, options) {
// ...
});
Weist man einer EntitĂ€t einen Wert mit der Bezeichnung id zu, behandelt Backbone.js sie auf besondere Art: Jede EntitĂ€t verfĂŒgt ĂŒber eine direkte Eigenschaft mit diesem Namen, auf die sich ohne den Umweg ĂŒber die get-Funktion zugreifen lĂ€sst. AuĂerdem verwendet Backbone.js diese Eigenschaft intern, um eine EntitĂ€t eindeutig zu identifizieren, beispielsweise innerhalb einer Liste.
Gelegentlich benötigt man statt einer EntitÀt ein reines Datenobjekt. Da Backbone.js mit Ausnahme der Eigenschaft id alle Werte intern kapselt, lÀsst sich mit der Funktion toJSON eine EntitÀt in ein solches Objekt umwandeln:
var pureData = customer.toJSON();
// => {
// firstName: 'Golo',
// lastName: 'Roden',
// age: 35
// }
Listen von EntitĂ€ten funktionieren prinzipiell auf die gleiche Art, man verwendet lediglich die Basisklasse Backbone.Collection statt Backbone.Model. Als Konvention fĂŒr die Bezeichner von Listen hat sich eingebĂŒrgert, ihnen das Suffix List anzuhĂ€ngen. Als Parameter ist einer Liste die zu verwendende EntitĂ€t zu ĂŒbergeben:
var CustomerList = Backbone.Collection.extend({
model: Customer
});
Auch in diesem Fall erhĂ€lt man als Ergebnis keine konkrete Liste, sondern lediglich eine Blaupause, weshalb auch hier CustomerList als Konstruktor aufzurufen ist. Als Parameter kann man dem Konstruktor eine Liste von EntitĂ€ten ĂŒbergeben, mit denen die Liste initialisiert wird:
var golo = new Customer({ firstName: 'Golo', lastName: 'Roden', age: 34 }),
bill = new Customer({ firstName: 'Bill', lastName: 'Gates', age: 57 });
var customers = new CustomerList([ golo, bill ]);
Analog zu der set-Funktion einer EntitĂ€t stellt Backbone.js eine gleichnamige Funktion fĂŒr Listen bereit: Bisher nicht enthaltene EntitĂ€ten werden der Liste hinzugefĂŒgt, bereits enthaltene werden angepasst â oder entfernt, wenn sie der set-Funktion nicht mehr ĂŒbergeben werden.
FĂŒr einen gezielten Zugriff auf einzelne ListeneintrĂ€ge bietet Backbone.js darĂŒber hinaus die Funktionen add und remove an. FĂŒr das Aktualisieren einer bestehenden EntitĂ€t muss man keine Funktion aufrufen, es genĂŒgt, ihre Werte anzupassen.
Auch in diesem Fall löst Backbone.js verschiedene Ereignisse aus, insbesondere add und remove. Auf sie lÀsst sich auf dem gleichen Weg zugreifen wie auf die Ereignisse von EntitÀten. Die Dokumentation von Backbone.js enthÀlt eine vollstÀndige Liste aller Ereignisse [6], die EntitÀten und Listen auslösen können.
Backbone.js bietet fĂŒr Listen zudem zahlreiche Funktionen an, um ihren Inhalt zu filtern oder zu sortieren. Dazu zĂ€hlen insbesondere diverse Funktionen [7], die aus Underscore.js stammen und von Backbone.js nach auĂen durchgereicht werden, wie die fĂŒr zahlreiche Aufgaben ausgesprochen hilfreichen Funktionen map und reduce. Zu guter Letzt verfĂŒgen auch Listen ĂŒber eine toJSON-Funktion, die ihren Inhalt als reines Datenobjekt zurĂŒckgibt.
Backbone.js unterstĂŒtzt es von Haus aus, eigene Logik in einer EntitĂ€t oder Liste unterzubringen. HierfĂŒr sind keine besonderen Vorkehrungen notwendig. Stattdessen fĂŒgt man die gewĂŒnschten Eigenschaften und Funktionen der EntitĂ€t oder der Liste im Aufruf der jeweiligen extend-Funktion hinzu:
var Customer = Backbone.Model.extend({
defaults: {
firstName: '',
lastName:''
},
fullName: function () {
return this.get('firstName') + ' ' + this.get('lastName');
}
});
Besonderes Augenmerk legt Backbone.js darĂŒber hinaus auf das Validieren und Sortieren von EntitĂ€ten und Listen. Den wichtigsten Einstiegspunkt fĂŒr das Validieren von EntitĂ€ten stellt die Funktion validate dar, die im Falle einer gĂŒltigen EntitĂ€t undefined, im Fehlerfall jedoch eine entsprechende Fehlermeldung zurĂŒckgibt. Auch diese Funktion ĂŒbergibt man im Rahmen des Aufrufs von extend:
var Customer = Backbone.Model.extend({
defaults: {
firstName: '',
lastName: ''
},
validate: function (attributes, options) {
if (attributes.age < 0) { return 'age must not be negative.'; }
}
});
Die Funktion isValid fĂŒhrt die eigentliche Validierung aus und gibt als logischen Wert zurĂŒck, ob die EntitĂ€t gemÀà den in der validate-Funktion hinterlegten Regeln gĂŒltig ist oder nicht. Im Fehlerfall kann man dann entsprechend reagieren und den aufgetretenen Fehler mithilfe der Eigenschaft validationError ermitteln:
if (!customer.isValid()) {
console.log(customer.validationError);
}
Zu beachten ist hierbei, dass die set-Funktion keine Validierung auslöst, es sei denn, man ĂŒbergibt zusĂ€tzlich ein passendes Parameterobjekt:
customer.set('age', customer.get('age') + 1, { validate: true });
FĂŒr das Sortieren von EntitĂ€ten dient in Backbone.js die comparator-Eigenschaft von Listen. Wenn man sie setzt, kĂŒmmert sich Backbone.js beim EinfĂŒgen von neuen EntitĂ€ten in eine Liste automatisch darum, sie sortiert einzufĂŒgen, sodass man stets ĂŒber eine korrekt sortierte Auflistung verfĂŒgt.
Um den Wert der comparator-Eigenschaft zu setzen, gibt es drei verschiedene Vorgehensweisen:
- Im einfachsten Fall weist man ihr den Namen jenes Attributs zu, das man als Sortierkriterium verwenden will. Dieses Vorgehen ist einfach umzusetzen, birgt aber den Nachteil, dass man die Liste ausschlieĂlich nach einem einzigen statischen Kriterium in alphabetischer Reihenfolge sortieren kann.
- Alternativ kann man der comparator-Eigenschaft eine Funktion zuweisen, die die EntitĂ€t entgegennimmt und den Namen des Attributs zurĂŒckgibt, nach dem man sortieren will. Dabei gelten allerdings die gleichen EinschrĂ€nkungen wie zuvor. Die einzige Ausnahme stellt die hinzugekommene Möglichkeit dar, das sortierrelevante Attribut dynamisch festzulegen.
- Als dritte Möglichkeit kann man der comparator-Eigenschaft eine Funktion zuweisen, die zwei Parameter erwartet. Beide Parameter reprĂ€sentieren jeweils eine EntitĂ€t. Auf diesem Weg kann man eine beliebig komplexe Sortierlogik implementieren, wichtig ist einzig der RĂŒckgabewert der Funktion: Der Wert -1 bedeutet, dass die erste EntitĂ€t vor der zweiten einzusortieren ist; der Wert 1 beschreibt das Gegenteil. Gibt die Funktion hingegen den Wert 0 zurĂŒck, bedeutet das, dass beide EntitĂ€ten als gleichwertig anzusehen sind.
Nimmt man all dies zusammen, wird klar, dass Backbone.js das Hauptaugenmerk auf die Modellierung der DomÀne der Anwendung legt. Bis zu diesem Punkt kann man Backbone.js sogar unabhÀngig von der Tatsache verwenden, dass es eigentlich zur Implementierung von Single-Page-Anwendungen gedacht ist.
Infrastrukturanbindung
Anbinden von Infrastruktur
Ein DomĂ€nenmodell modellieren zu können, ist hilfreich, genĂŒgt fĂŒr eine vollstĂ€ndige Anwendung jedoch nicht. In der Regel will man es zusĂ€tzlich persistieren, meist mit einem REST-Webdienst. Auch hierbei bietet Backbone.js seine UnterstĂŒtzung an, denn EntitĂ€ten verfĂŒgen von Haus aus ĂŒber eine save- und eine destroy-Funktion.
Die save-Funktion serialisiert die zu speichernde EntitĂ€t und ĂŒbertrĂ€gt sie mithilfe einer AJAX-Anfrage im Hintergrund an den Webserver. AbhĂ€ngig vom RĂŒckgabewert der Funktion isNew der EntitĂ€t entscheidet Backbone.js, ob es eine POST- oder eine PUT-Anfrage erzeugt â ob es also versucht, die EntitĂ€t erstmalig zu speichern oder eine bestehende Version zu aktualisieren.
Analog erzeugt die destroy-Funktion eine DELETE-Anfrage, um die EntitÀt vom Webserver zu löschen. Backbone.js benötigt dazu eine URL, an die es die Anfragen schicken kann. Diese URL setzt man mit Hilfe der url-Eigenschaft an der zugehörigen Liste:
var CustomerList = Backbone.Collection.extend({
model: Customer,
url: '/customers'
});
Backbone.js kombiniert die URL und die id-Eigenschaft der betroffenen EntitĂ€t, um die eigentliche Adresse fĂŒr eine Anfrage zu ermitteln. Auf dem prinzipiell gleichen Weg befĂŒllt man eine Liste anfĂ€nglich mit Daten vom Webserver. Hierzu dient die Funktion fetch, die man allerdings nicht an einer EntitĂ€t, sondern an der Liste aufruft.
StandardmĂ€Ăig fĂŒhrt Backbone.js den Inhalt der bestehenden Liste mit den vom Webserver empfangenen Daten zusammen. Falls man die Liste von Grund auf neu aufbauen möchte, ist daher zusĂ€tzlich ein entsprechendes Parameterobjekt zu ĂŒbergeben:
customers.fetch({ reset: true });
Im Hintergrund verwendet Backbone.js fĂŒr den Datenabgleich mit dem Webserver eine interne Funktion namens Backbone.sync. Sie adressiert standardmĂ€Ăig einen REST-basierten Webdienst. Bei Bedarf lĂ€sst sie sich allerdings durch eine eigene Implementierung ersetzen, um eine beliebige Synchronisationslogik bereitzustellen.
Dazu genĂŒgt es, Backbone.sync eine neue Funktion zuzuweisen, die der folgenden Signatur entspricht:
Backbone.sync = function (method, model, options) {
// ...
};
Der Parameter method enthĂ€lt einen der Werte create, read, update oder delete, der Parameter model hingegen die zu speichernde EntitĂ€t oder die zu ladende Liste. options schlieĂlich umfasst zusĂ€tzliche Optionen, die beim Speichern oder Laden von Daten zu beachten sind.
Alternativ besteht die Möglichkeit, eine derartige Funktion von einem Drittanbieter zu verwenden. So stellt beispielsweise das Projekt Backbone.localstorage [8] eine Funktion zur VerfĂŒgung, die auf den lokalen Speicher des Webbrowsers zugreift. Die Integration ist denkbar einfach: Man muss nach dem Laden der Datei backbone.js lediglich die Datei backbone.localstorage.js laden, die die Funktion Backbone.sync wie gewĂŒnscht ersetzt:
[...]
<script type="text/javascript" src="backbone.js"></script>
<script type="text/javascript" src="backbone.localstorage.js"></script>
</body>
[...]
Interessant ist darĂŒberhinaus auch die Möglichkeit, ĂŒber die Funktion Backbone.ajax eine eigene AJAX-Anbindung anzulegen, beziehungsweise mithilfe der Funktionen [9] Backbone.emulateHTTP und
Backbone.emulateJSON die KompatibilitÀt zu einigen Webservern verbessern zu können.
Routen, Ansichten
Verwalten von Routen und Ansichten
Wie bereits erwĂ€hnt, kĂŒmmert sich Backbone.js an sich nicht um das Aktualisieren der Darstellung im Webbrowser, sondern ĂŒberlĂ€sst diese Aufgabe dem Entwickler und einer von ihm gewĂ€hlten Bibliothek. Dennoch stellt Backbone.js die Basisklasse Backbone.View zur VerfĂŒgung, die zur Definition von Ansichten dient. Eine Ansicht entspricht allerdings eher dem aus MVC bekannten Controller, ĂŒbernimmt also die Kommunikation zwischen der Webseite und dem DomĂ€nenmodell. Zum einen reagiert eine Ansicht auf Eingaben im Webbrowser, indem sie Funktionen am DomĂ€nenmodell aufruft. Zum anderen veranlasst sie die Aktualisierung der Darstellung im Webbrowser, wenn Ănderungen am DomĂ€nenmodell auftreten.
Um eine Ansicht zu definieren, muss man sie analog zu EntitÀten und Listen von der Klasse Backbone.View ableiten und einige Eigenschaften definieren: Mindestens tagName ist einzubauen, da die Eigenschaft definiert, als welches HTML-Element die Ansicht gerendert werden soll. ZusÀtzlich kann man die Eigenschaften className und id angeben, um eine CSS-Klasse und -ID festzulegen:
var CustomerView = Backbone.View.extend({
tagName: 'div',
className: 'customerDetail'
});
AuĂerdem ist es sinnvoll, die initialize-Funktion zu definieren und in ihr als Reaktion auf eine Ănderung des DomĂ€nenmodells die render-Funktion aufzurufen. Die Aufgabe dieser Funktion besteht darin, die Darstellung im Webbrowser zu aktualisieren. Wie sie implementiert wird, ĂŒberlĂ€sst Backbone.js jedoch vollstĂ€ndig der Fantasie des Entwicklers:
var CustomerView = Backbone.View.extend({
tagName: 'div',
className: 'customerDetail',
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () {
// ...
}
});
In der Regel ĂŒbernimmt die render-Funktion selbst nicht das eigentliche Aktualisieren, sondern erzeugt lediglich den erforderlichen HTML-Code, um die Ansicht anzuzeigen. Das weist sie der Eigenschaft el beziehungsweise deren jQuery-gekapselter Variante $el zu, die das konkrete HTML-Element reprĂ€sentiert, das mit der Ansicht verknĂŒpft wurde. An dieser Stelle bindet man ĂŒblicherweise auch die Verwendung von HTML-Vorlagen ein, wobei sich im einfachsten Fall wiederum auf die template-Funktion von Underscore.js zurĂŒckgreifen lĂ€sst.
Damit man die render-Funktion an ĂŒbergeordneter Stelle verkettet aufrufen kann, empfiehlt die Dokumentation von Backbone.js, dass die Funktion this an den Aufrufer zurĂŒckgibt:
var CustomerView = Backbone.View.extend({
template: _.template(...),
tagName: 'div',
className: 'customerDetail',
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
AuĂerdem lĂ€sst sich durch die events-Eigenschaft auf Ereignisse innerhalb des Webbrowsers reagieren, um beispielsweise entsprechende Funktionen an der Ansicht aufzurufen:
var CustomerView = Backbone.View.extend({
template: _.template(...),
tagName: 'div',
className: 'customerDetail',
events: {
'click .save': 'saveCustomer'
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
},
saveCustomer: function () {
// ...
}
});
Beim Erzeugen der konkreten Ansicht kann man dem Konstruktor als Parameter ein bestehendes HTML-Element ĂŒbergeben, an das sie gebunden wird. AuĂerdem kann man dem Konstruktor dynamisch die anzuzeigende EntitĂ€t beziehungsweise die anzuzeigende Liste ĂŒbergeben, indem man dem Parameterobjekt die Eigenschaft model beziehungsweise collection hinzufĂŒgt:
var customerView = new CustomerView({
el: document.getElementById(...),
model: customer
});
Auch fĂŒr das Verwalten von Routen enthĂ€lt Backbone.js UnterstĂŒtzung. Basis hierfĂŒr ist die Klasse Backbone.Router, die sich auf dem ĂŒblichen Weg ableiten lassen. Als Parameter sind der extend-Funktion ein Objekt mit Routen und die von ihnen aufzurufenden Funktionen zu ĂŒbergeben:
var AppRouter = Backbone.Router.extend({
routes: {
'customers/:id': 'loadCustomer',
'customers': 'loadCustomers'
},
loadCustomer: function (id) {
// ...
},
loadCustomers: function () {
// ...
}
});
Um einen konkreten Router zu erzeugen, muss man AppRouter als Konstruktor aufrufen:
var appRouter = new AppRouter();
Die Existenz einer Router-Instanz an sich genĂŒgt jedoch noch nicht, damit Backbone.js die Navigation des Webbrowsers verwaltet. DarĂŒber hinaus muss man noch die start-Funktion des Objekts Backbone.history aufrufen.
Dieser Funktion kann man dabei als Parameter ĂŒbergeben, ob man Hashbang nutzende Routen oder die in HTML5 neu eingefĂŒhrte, ausschlieĂlich mit modernen Webbrowsern kompatible History-API [10] verwenden möchte:
Backbone.history.start({ pushState: true });
Da die Navigation mit Hashbang im Internet Explorer die Verwendung eines iframe-Elements voraussetzt, ist es wichtig, die start-Funktion erst nach dem vollstÀndigen Laden der Webseite aufzurufen. Aus diesem Grund ist man gut beraten, das Instanziieren der Router und den Aufruf der Funktion in einen Aufruf der $-Funktion von jQuery zu kapseln:
$(function () {
var appRouter = new AppRouter();
Backbone.history.start({ pushState: true });
});
Fazit
StÀrken und SchwÀchen
Backbone.js hat unbestritten zahlreiche StĂ€rken: Gerade im Vergleich zu Knockout fĂ€llt auf, dass Backbone.js nicht nur einen Teilaspekt bei der Entwicklung von Single-Page-Anwendungen adressiert, sondern einen umfassenden Ansatz bietet. Kombiniert mit der klar gegliederten Struktur, die Backbone.js-Anwendungen ĂŒblicherweise aufweisen, trĂ€gt das viel zur Wartbarkeit von groĂen Webanwendungen bei.
Der Fokus auf die Modellierung der DomĂ€ne und die automatische Synchronisierung mit dem Webserver zwingen den Entwickler, von vornherein eine gewisse Struktur fĂŒr die Anwendung zu planen und sich Gedanken ĂŒber den fachlichen Kern zu machen. Dies erklĂ€rt, warum mit Backbone.js entwickelte Webanwendungen in der Regel sehr sauber strukturiert sind.
Diese StĂ€rke ist zugleich aber auch eine der relevanten SchwĂ€chen von Backbone.js: Da fĂŒr jede Anwendung zunĂ€chst die entsprechende Struktur aufzubauen ist, entsteht insbesondere fĂŒr kleine Projekte rasch viel Code, der zunĂ€chst nichts zu der eigentlichen Anwendung beitrĂ€gt, sondern lediglich Infrastrukturcode darstellt. Die Entwicklung ist gerade zu Beginn entsprechend aufwendig und zĂ€h.
Die gröĂte SchwĂ€che von Backbone.js liegt jedoch in dem Verzicht auf ein Konzept zur Aktualisierung der Darstellung. Dadurch entstehen Backbone.js gleich zwei Nachteile. Zum einen ist man wiederum auf die Integration einer Bibliothek eines Drittanbieters angewiesen, was langfristig stets auch zu KompatibilitĂ€ts- und Wartungsproblemen fĂŒhren kann. AuĂerdem gibt man die saubere Struktur einer Backbone.js-Anwendung zumindest teilweise zu Gunsten von Code auf, der mit einer anderen Bibliothek entwickelt wurde. Das kann sich insbesondere bei der Verwendung von jQuery schnell rĂ€chen.
Zum anderen ĂŒbernimmt Backbone.js von sich aus keinerlei Verwaltung der dargestellten Ansichten. Das bedeutet, dass man als Entwickler selbst Sorge fĂŒr deren korrekte Entsorgung tragen muss. All zu rasch entstehen hierbei jedoch nur schwer auffindbare Speicherlecks, die der StabilitĂ€t und Leistung der Anwendung langfristig schaden.
Backbone.js begegnet diesen Problemen nicht. Abhilfe schaffen Erweiterungen wie Backbone.Marionette [11] oder Thorax [12], die allerdings jeweils wiederum alles andere als kompakt sind und die ohnehin schon vorhandene KomplexitÀt der Anwendung weiter erhöhen.
Das Ergebnis sind dann hĂ€ufig Anwendungen, die zwar sauber strukturiert sind, fĂŒr wenig Effekt allerdings eine komplexe Infrastruktur und verhĂ€ltnismĂ€Ăig viel Code aufweisen.
Fazit
Alles in allem kann man festhalten, dass Backbone.js ein prinzipiell zwar durchdachtes Framework ist, das allerdings rasch zu unnötig hoher KomplexitĂ€t fĂŒhrt. Insbesondere der Verzicht auf ein eigenes Konzept zur Verwaltung der Darstellung trĂ€gt hierzu bei, unter anderem aufgrund der Speicherverwaltung fĂŒr einzelne Ansichten, die man von Hand implementieren muss.
MVC- und MVVM-Framework fĂŒr JavaScript im Vergleich
Der vorliegende Artikel ist der zweite Teil einer Serie, in der verschiedene mit JavaScript arbeitende MV*-Frameworks vorgestellt werden. In der nÀchsten Folge wird das aus dem vergangenen Teil bereits bekannte MVVM-Entwurfsmuster nochmals aufgegriffen, dann allerdings nicht in Verbindung mit Knockout.js, sondern einem zwar noch sehr jungen, aber ausgesprochen vielversprechenden Framework: AngularJS.
Bewertet man Backbone.js im Hinblick auf die Tatsache, dass es bereits im Jahr 2010 die Möglichkeit eröffnet hat, Single-Page-Anwendungen wohlstrukturiert und professionell zu entwickeln, handelt es sich trotz allem um ein durchaus beachtungswĂŒrdiges Framework. Betrachtet man es jedoch aus heutiger Sicht, fallen die KomplexitĂ€t und der erforderliche Aufwand ausgesprochen negativ auf. Vergleicht man Backbone.js zudem mit Knockout.js, so macht sich das fehlende Konzept zur Verwaltung der Darstellung schmerzlich bemerkbar. Backbone.js wirkt dadurch nicht mehr zeitgemĂ€Ă.
Prinzipiell wĂ€re Knockout daher die perfekte ErgĂ€nzung zu Backbone.js, was unter anderem zu dem Projekt Knockback [13] gefĂŒhrt hat. Auch dieser Ansatz birgt allerdings Gefahren, hĂ€ngt man dann doch gleich von zwei groĂen Frameworks, deren KompatibilitĂ€t zueinander und einer weiteren, dritten Komponente ab, die beide verbindet.
Zweifelsohne eignet sich Backbone.js fĂŒr komplexe Webanwendungen eher als Knockout, dennoch kann man es auf Grund der genannten SchwĂ€chen nur bedingt empfehlen.
Golo Roden
ist GrĂŒnder und GeschĂ€ftsfĂŒhrer der "the native web UG", eines auf native Webtechniken spezialisierten Unternehmens. FĂŒr die Entwicklung moderner Webanwendungen bevorzugt er JavaScript und Node.js und hat mit "Node.js & Co." das erste deutschsprachige Buch zum Thema geschrieben.
(jul [14])
URL dieses Artikels:
https://www.heise.de/-1938069
Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Model-View-ViewModel-mit-Knockout-js-1928690.html
[2] http://backbonejs.org/
[3] http://underscorejs.org/
[4] https://github.com/bestiejs/json3
[5] http://jquery.com/
[6] http://backbonejs.org/#Events-catalog
[7] http://backbonejs.org/#Collection-Underscore-Methods
[8] http://documentup.com/jeromegn/backbone.localStorage
[9] http://backbonejs.org/#Sync
[10] http://diveintohtml5.info/history.html
[11] http://marionettejs.com/
[12] http://thoraxjs.org/
[13] http://kmalakoff.github.io/knockback/
[14] mailto:jul@heise.de
Copyright © 2013 Heise Medien