Webkrauts Logo

Webkrauts Webkrauts Schriftzug

- für mehr Qualität im Web

Web-Applikationen mit Backbone.js

Komplexen JavaScript-Code strukturieren

Web-Applikationen mit Backbone.js

Bei der Strukturierung von JavaScript-basierten Web-Applikationen hilft Backbone.js: nach dem Baukasten-Prinzip können Webworker verschiedene Komponenten erstellen – vom einfachen Tab-View bis zur komplexen Single-Page-Application.

In den Anfangsjahren des Webs war das Modell sehr einfach: Ein Klick auf einen Link lädt eine neue HTML-Seite. Insbesondere Web-Applikationen setzen aber auf ein anderes Aktionsmodell, das denen von Desktop-Apps näher kommt: Während traditionell die komplette HTML-Seite mit leicht verändertem Inhalt neu geladen wird, tauschen Web-Applikationen gezielt einzelne Bereiche aus, zeigen Popups an, oder führen flüssig durch komplexe Navigationsstrukturen.

Backbone.js ist ein Baukasten für solche Web-Applikationen oder Single Page Applications und wurde vor gut zwei Jahren von Jeremy Ashkenas für DocumentCloud, einer Dokumentverwaltungs-Software für Journalisten geschrieben. Seitdem findet es auch auf vielen anderen Websites Anwendung, etwa bei der mobilen Website von LinkedIn und SoundCloud, auf AirBnB oder beim Projektmanagement-System Trello.

Backbone.js kommt modular daher und erlaubt dem Programmierer, die gerade benötigten Teile der Bibliothek zu verwenden – von einfacher Strukturierung des JavaScript-Codes bis zum Bau komplexer Single Page Applications im Stile von Google Mail bietet Backbone.js Hilfestellungen, die den Prozess deutlich beschleunigen. Im Vergleich zu ähnlichen Frameworks wie ember, Spine oder Googles AngularJS bietet Backbone zwar weniger Hilfestellungen und klare »Best Practices«, dafür lässt einem Backbone viel Spielraum, da es fast immer mehrere Wege zum Ziel gibt.

Models, Views und Router

Backbone.js funktioniert nach dem bekannten »Model-View-Router«-Prinzip: Die Applikation wird in diese drei Teile strukturiert:

In der Abkürzung MV* steht M für Models, V für Views und * für den Rest. Traditionell gibt es bereits seit den siebziger Jahren die MVC-Architektur, wobei C für Controller steht. Diese reagieren auf Benutzereingaben und veranlassen das Darstellen von Models via Views. Backbone.js implementiert dieses Schema allerdings nur lose und stellt statt Controllern sogenannte »Router« bereit, die eine ähnliche Funktion übernehmen.

Models dienen dazu, Daten zu repräsentieren, etwa einen Eintrag in einer Datenbank, der neu angelegt, verändert oder gelöscht werden kann. Views sind verantwortlich für die Anzeige des Inhalts: Sie vermitteln zwischen dem Benutzer und dem Model. Die Idee ist, dass ein Model viele verschiedene Views haben kann, um etwa auf unterschiedliche Weise angezeigt zu werden. Der Router übernimmt die Navigation innerhalb einer Seite und veranlasst etwa, dass ein neuer View angezeigt wird, wenn der Benutzer auf einen Link klickt, oder dass beim Neuladen der Seite an die richtige Stelle im Interaktionsfluss gesprungen wird.
Zusätzlich gibt es noch Collections, die einen Spezialfall eines Models darstellen und mehrere Models des gleichen Typs gruppieren.

Dabei sind alle drei Komponenten nur lose miteinander verbunden; eine Backbone-Applikation kann auch nur eine oder zwei der Komponenten verwenden, etwa wenn keine Models existieren, oder kein Navigationsfluss existiert, der abgebildet werden muss.

Wir werden uns nun die verschiedenen Komponenten von Backbone.js anhand einer Kochbuch-Applikation ansehen. Hier ist vorab schon einmal das spätere Kochbuch zu sehen.

Models

Ein einfaches Model für Rezepte in unserer App sieht folgendermaßen aus:

  1. var Recipe = Backbone.Model.extend({
  2.   defaults: {
  3.     title: "New Recipe",
  4.     ingredients: "",
  5.     instructions: ""
  6.   }
  7. });

Objekte vom Typ Recipe erben nun alle Methoden des Basis-Models und werden mit den entsprechenden Standard-Werten initialisiert. Ein neues Rezept wird mit new Recipe() erstellt. Optional können wir schon bei der Erstellung einige Werte setzen:

  1. var recipe = new Recipe({
  2.   title: "Spiegeleier",
  3.   ingredients: "2 Eier",
  4.   instructions: "Eier in die heiße Pfanne geben. Warten."
  5. });

Alle Models haben einige Methoden, mit denen wir die Daten verändern und auslesen können (.set()/.get()/.has()).

Das Model-Objekt kümmert sich wirklich nur um die Speicherung der Daten und hat keinerlei Funktionen, um etwa HTML-Elemente zu generieren. Allerdings löst eine Veränderung der Werte an einem Objekt eine Reihe von change-Events aus. Wenn ein View gerade ein Model anzeigt, kann er von diesem benachrichtigt werden, um die Werte im DOM zu verändern.

Views

Views dienen dazu, den Inhalt eines Models anzuzeigen. Dabei können mehrere Views dasselbe Model anzeigen, und ein View kann eine Kombination aus mehreren Models anzeigen. In Backbone.js hat jeder View ein Haupt-DOM-Element, das den Inhalt repräsentiert. Der View kann dann auf Events reagieren, die innerhalb dieses Views auftreten.

In unserem Kochbuch soll man natürlich Rezepte bearbeiten können. Der View dafür sieht folgendermaßen aus:

  1. var EditView = Backbone.View.extend({
  2.   template: _.template($('#template-edit').html()),
  3.  
  4.   events: {
  5.     'click input[type="submit"]': 'save'
  6.   },
  7.  
  8.   save: function() {
  9.     this.model.set({
  10.       title: this.$el.find('#title').val(),
  11.       ingredients: this.$el.find('#ingredients').val(),
  12.       instructions: this.$el.find('#instructions').val()
  13.     });
  14.     this.trigger('finished');
  15.       return false;
  16.     },
  17.  
  18.   render: function() {
  19.     this.$el.html(this.template(this.model.toJSON()));
  20.     return this;
  21.   }
  22. });

Backbone verwendet seinerseits das Underscore-Framework für interne Funktionen, das unter anderem eine Template-Engine mitbringt. Backbone.js ist aber nicht explizit auf ein Template-System festgelegt und kann mit jedem System (oder ganz ohne) verwendet werden.

Weiterhin definiert unser View ein Event: Der Schlüssel ist ein String, der aus dem Event-Namen und einem Selektor besteht, der Wert ist ein Methodenname innerhalb des Views. Alleine durch diese Definition werden alle click-Events auf Submit-Buttons innerhalb des Views die save()-Methode aufrufen. Diese speichert die Werte aus dem Formular zurück in das Model und erzeugt – in unserem Fall – ein finished-Event. Das dient dazu, dem übergeordneten View mitzuteilen, dass die Bearbeitung des Rezepts nun beendet ist. Um ein tatsächliches Abschicken des Formulars zu verhindern, geben wir außerdem false zurück.

Schließlich gibt es noch eine render()-Methode, die den HTML-Code für den View erzeugt (hier per Underscore-Template). Der Name render ist frei gewählt und erfährt kein spezielle Behandlung von Seiten Backbones.

Collections

Meistens gibt es von einem Model nicht nur eine Instanz. Deswegen bietet Backbone Collections an, um mehrere Instanzen eines Typs zusammenzufassen, etwa um alle Rezepte gleichzeitig vom Server zu laden. Collections enthalten normalerweise nur Objekte eines Typs. In unserem Kochbuch-Beispiel gibt es folgende Collection:

  1. var Cookbook = Backbone.Collection.extend({
  2.   model: Recipe
  3. });

Im einfachsten Fall gibt es dann von einer Collection nur eine Instanz: Bei unserem Kochbuch-Beispiel gibt es genau eine Collection: this.cookbook = new Cookbook();. In dieser befinden sich dann alle Rezepte aus unserem Kochbuch.

Router

Backbone.js kann auch das Routing in einer Applikation übernehmen. Dazu wird der Klick auf einen Link abgefangen und stattdessen der Pfad dem Router übergeben. Dieser führt dann die entsprechende Methode aus, um etwa den aktuellen View zu ändern. Backbone.js unterstützt auch Platzhalter in den URLs, was nützlich für Detailansichten ist. Eine stark vereinfachte Version des Kochbuch-Routers sieht so aus:

  1. var CookbookApp = Backbone.Router.extend({
  2.   routes: {
  3.     '': 'home',
  4.     'recipe/add': 'add',
  5.     'recipe/:id': 'display'
  6.   },
  7.  
  8.   home: function() { /* ... */ },
  9.   add: function() { /* ... */ },
  10.   display: function() { /* ... */ }
  11. });

Wenn der Benutzer die Seite lädt, sieht sich der Router zunächst den Teil nach dem #-Symbol an und führt dann die entsprechende Aktion aus. So wird sichergestellt, dass der Benutzer einfach URLs aus der Adresszeile kopieren kann, und sich dennoch an der richtigen Stelle in der Applikation befindet.

Wie alle anderen Komponenten in Backbone ist auch der Router optional. Natürlich müssen wir nicht alle Klicks in der Web-Applikation abfangen, so dass kein einziger Reload passiert, sondern etwa nur jene, bei denen sich nur ein kleiner Teil der Seite ändert.

Navigation

Web-Applikationen verhalten sich anders als das bisherige Interaktionsmodell. Insbesondere funktioniert der »Zurück«-Button nicht mehr wie gewohnt: Der Benutzer bleibt immer auf derselben HTML-Seite. Um das zu verhindern, bietet Backbone zwei Möglichkeiten:

Verändern des URL-Hashes
Per document.location.hash kann nur der Teil der URL nach dem # verändert werden. Normalerweise wird der verwendet, um zu Ankern in einer HTML-Seite zu springen, aber alle Browser können diesen auch auslesen und per JavaScript verändern. Jede Änderung erzeugt einen neuen Eintrag in der Browser-History. Wenn der Benutzer auf den Zurück-Button klickt, wird das onhashchange-Event ausgelöst und Backbone ändert die entsprechende Ansicht.
HTML5 pushState
Der Teil nach dem # wird bei einem Seitenaufruf nicht an den Server gesendet. Der Server kann also bei einem neuen Aufruf nur die Startseite zurückliefern – ein großes Problem etwa für SEO. Deshalb gibt es in HTML5 die pushState-API. Mit ihr können wir direkt die URL verändern, und zwar auch die Teile vor dem #. Eingesetzt wird sie beispielsweise schon bei GitHub zur Repository-Navigation. Backbone kann das Routing auch mit dieser API durchführen, sofern sie denn verfügbar ist (Der Internet Explorer unterstützt sie erst ab Version 10).

Persistenz

Die meisten Web-Applikationen interagieren mit dem Server. Dazu stellt Backbone standardmäßig eine AJAX-Schnittstelle bereit: Per model.save() wird ein Request mit den Model-Daten an den Server geschickt; mit model.fetch(id) können wir neue Models nachladen.

Allerdings kann Backbone auch mit anderen Persistenz-Modellen umgehen: Backbone.localStorage verwendet die localStorage-API um Daten direkt im Browser zu speichern. So können wir Web-Applikationen bauen, die komplette ohne Server arbeiten – alles ist lokal gespeichert.

Weitere Informationen

Wer jetzt Lust hat, selbst eine Backbone-App zu schreiben, findet im Internet viele gute Tutorials und Referenzen:

  • Kochbuch: Ein Zip-File mit dem Beispiel aus diesem Artikel.
  • Backbone-Dokumentation: Gut strukturierte Übersicht, allerdings fehlen Best-Practices.
  • Backbone Fundamentals: Sehr gutes Online-Buch über Backbone. Im März 2013 soll das Buch in gedruckter Form bei O’Reilly erscheinen.
  • JavaScript Web Applications: O’Reilly-Buch über Single-Page-Apps. Der Autor hat das Spine-Framework geschrieben und der Teil über Backbone fällt daher etwas dürftig aus. Allerdings gibt es hier sehr viel Basiswissen zu Web-Applikationen im Allgemeinen.
  • todomvc.com vergleicht viele Backbone-Alternativen und zeigt die Implementierung einer Todo-Applikation in jedem Framework.
  • Marionette hilft bei der Strukturierung von Backbone-Apps durch Vorgabe klarer Workflows

Kommentare

Henri
am 22.05.2013 - 17:12

Irgendwie könnte ich mich für keine der Webframeworks die ich bis jetzt probiert habe. Ich glaube das Umdenken war mir einfach zuviel. Aber Backbone.js sieht tatsächlich interessant aus. Ich werde es gleich probieren ! Danke für den Tipp !

Permanenter Link

Die Kommentare sind geschlossen.