Teil 2: Sails.js mit Backbone.js am Client kombinieren
Von Desiree Zottl am 23.01.2014
Hat man sich für Sails.js als MVC Framework am Server entschieden, lässt sich das MVC Framework Backbone.js am Client sehr gut damit kombinieren. Ich möchte in diesem Beitrag zeigen, wie man zum Serverteil ein ineinandergreifendes modulares Frontend mit Hilfe von Underscore.js, Backbone.js und Require.js entwickeln kann.
Backbone.js ist ein JavaScript Framework zum Entwickeln von Singe Page Applikationen und unterstützt die Kommunikation zum Server über eine RESTful API. Es besteht aus Models, Collections, Views und Router. Für die Verwendung ist nur die Einbindung von Underscore.js erforderlich, allerdings können viele andere JavaScript Erweiterungen gut damit kombiniert werden.
Backbone Model
Das Model repräsentiert die Daten der Applikation und stellt die kleinste mögliche Einheit an Funktionalitäten dar. Um ein Backbone Model zu generieren, muss lediglich die Backbone Klasse Backbone.Model extendet werden, dann kann es mit eigenen Funktionalitäten erweitert werden. Eine Möglichkeit sind beispielsweise Defaultwerte für bestimmte Attribute des Models.
Möchte man das Model einfach vom Server übernehmen, ohne Funktionalitäten hinzuzufügen, reichen die folgenden Zeilen zum Generieren des Models.
var Event = Backbone.Model.extend({ }); return Event;
Backbone Collection
Eine Collection ist eine Gruppe von Models. Normalerweise enthält eine Collection einen Typ von Models und kann wie diese durch eigene Funktionalitäten erweitert werden. Man verwendet das “model” Property, um der Collection zu sagen, von welcher Modelklasse jedes Item in der Collection generiert werden soll.
var EventList = Backbone.Collection.extend({ model: Event }); return EventList;
Backbone View
Views zeigen die Daten der Applikation in der HTML Seite an. Dazu können Templates verwendet werden, die in der View definiert, oder von extern geladen werden können. Die initialize Funktion wird in Backbone beim Instanzieren vom Objekt immer aufgerufen. Davon ausgehend, können dann weitere Funktionen ausgeführt werden. Hier ein Beispiel für eine View, die den Titel des Models ausgibt.
var EventListItemView = Backbone.View.extend({ tagName:'li', template:_.template('<h1><%= title %></h1>'), initialize:function () { this.model.bind('change', this.render, this); }, render:function () { $(this.el).html(this.template(this.model)); return this; } }); return EventListItemView;
Templates
Am Codebeispiel der View sieht man auch bereits die Verwendung des Underscore.js Template. Diese sind sehr nützlich, um HTML aus JSON Daten zu rendern und ermöglichen das Einfügen von Variablen durch Platzhalter <%= Variablenname %>. Ebenso kann beliebiger JavaScript Code ausgeführt werden <% JavaScript Code %>. Die HTML Snippets können nicht nur direkt in der View geschrieben werden, sondern auch als sogenannte “Mikrotemplates”, in der HTML Seite eingefügt und durch die Id ausgewählt werden.
<script id="event" type="text/template"> <h1><%= title %></h1> <div><%= description %></div> </script>
Die Templates können auch in extra Files ausgelagert werden und vor allem bei Verwendung von Require.js und dem Require Text Modul, bei Bedarf einfach in die View geladen werden. Näheres dazu später im Beitrag unter “Verwendung von Require.js”.
Backbone Routing
Routen werden verwendet, um je nach URL, in der Applikation bestimmte Funktionen auszuführen und eine Browser-History zu erzeugen. Interpretiert wird dabei nur der Teil der URL nach dem Hash Tag. Damit kann auch der Einstiegspunkt zum Rendern der View erfolgen.
Im folgenden Beispiel würde die Url http://www.myDomain.at/#/events die EventListView rendern, für jede andere URL kann eine default Route durch *action definiert werden.
Dynamische Routenparameter können durch Doppelpunkt, zur Angabe eines Parameters, oder *, zur Angabe beliebig vieler Parameter, definiert werden.
var appRouter = Backbone.Router.extend({ routes : { 'events' : 'showEvents', 'events/:id' : 'showEvent', 'download/*path' : 'downloadFile', '*action' : 'defaultRoute' }, initialize: function() { Backbone.history.start({ pushState: true }); }, showEvents: function() { // init EventListView }, showEvent: function(id) { // init EventView }, downloadFile: function(path) { // download File }, defaultRoute: function() { // init default view } }); return appRouter;
Verwendung von Require.js
Je nach Applikationsumfang kann es sehr viele Models, Collections und Views geben, welche nicht alle immer sofort und gleichzeitig benötigt werden und in der richtigen Reihenfolge integriert werden müssen, da sie voneinander abhängen.
Um dieses Problem zu lösen und die Ladezeiten gering zu halten, ermöglicht Require.js das modulare Laden von JavaScript Files. Durch die Angabe der Abhängigkeiten eines JavaScript Moduls werden die Files immer dann geladen, wenn sie benötigt werden.
Als Script Tag wird lediglich Require.js selbst eingebunden und im Attribut data-main der Pfad zum Einstiegspunkt angegeben.
<script type="text/javascript" src="/js/plugins/require.js" data-main="/js/main.js"></script>
In diesem Fall startet die Applikation in der main.js, in der es sich anbietet die Konfiguration für Require anzugeben. Beispielsweise können für die Pfade zu den einzelnen JavaScript Files sprechende Namen vergeben werden, um sie später leichter als Abhängigkeit in einem File angeben zu können.
require.config({ paths: { // libraries jquery: 'js/jquery.min', underscore: 'js/underscore-min', backbone: 'js/backbone-min', // plugins text: 'js/plugins/text', // views eventListView: 'app/views/eventListView', eventListItemView: 'app/views/eventListItemView', // Models event: 'app/models/event', // collections eventList: 'app/collections/eventList', // routing router: 'app/routes/router' } });
Danach wird in diesem Beispiel das Routing instanziert, welches ab diesem Zeitpunkt die Aktionen, in unserem Fall das Rendern der Views je nach Url, übernimmt. Durch die Angabe der Abhängigkeit im define wird das benötigte File automatisch geladen. Als Parameter wird der Funktion ein beliebiger Name übergeben, unter welchem das Router Modul dann zur Verfügung steht.
define(['router'], function(Router) { var myRouter = new Router; });
Im File router.js müssen nun ebenfalls die Abhängigkeiten angegeben werden, damit die entsprechenden Datein geladen werden.
define([ 'jquery', 'underscore', 'backbone', 'eventList', 'eventListView' ], function($, _, Backbone, EventList, EventListView){ var appRouter = Backbone.Router.extend({ routes : { 'events' : 'showEvents', ...
In allen anderen JavaScript Modulen verhält es sich gleich. Über define werden die Pfade oder die dafür vergebenen Namen angegeben, die zum Ausführen des Codes benötigt werden. Der gesamte Code des Moduls wird in eine Funktion gewrappt, die als Parameter die eigene Bezeichnung der abhängigen Module in der gleichen Reihenfolge wie im define nimmt.
Durch das Require.js Text Plugin ist es möglich, in einer View das benötigte Template von extern zu Laden. Über text! + die Url des Templates kann angegeben werden, welches HTML File integriert werden soll.
define([ 'jquery', 'underscore', 'backbone', 'text!templates/event.html' ], function ($, _, Backbone, tmp) { var EventListItemView = Backbone.View.extend({ tagName:'li', template:_.template(tmp) }); return EventListItemView });
Kombination mit Sails.js am Server
Nachdem das Model, die Collection und die View definiert wurden, müssen diese nun auch mit Models gefüllt werden. Die Daten dazu kommen von unserem Sails.js Server, der im Blogbeitrag Teil1: Sails.js Einführung in das Backend MVC Framework vorgestellt wurde.
Backbone bietet eine elegante Integration von RESTful Services. Stellt das Backend eine RESTful API zur Verfügung, welche Daten über GET abruft, über POST erstellt, über PUT updated und über DELETE löscht, ist es einfach, diese mit Backbone zu verwenden. Sails.js legt automatisch, sofern die REST Blueprints aktiviert sind, für jedes Model Routen mit diesem HTTP Methoden an. In ihrer Dokumentation nennen sie dieses System sogar die “Backbone Convention”. http://sailsjs.org/#!documentation/routes
# Backbone Conventions GET : /:controller => findAll() GET : /:controller/read/:id => find(id) POST : /:controller/create => create() POST : /:controller/create/:id => create(id) PUT : /:controller/update/:id => update(id) DELETE: /:controller/destroy/:id => destroy(id)
Das Model sowie die Collections besitzen in Backbone ein Attribut url oder urlRoot, welches angibt, von welcher Server Adresse sie die Daten bekommen. Wir erweitern also unser Model und unsere Collection an Events von oben um dieses Attribut:
var Event = Backbone.Model.extend({ urlRoot: '/event' }); return Event;
var EventList = Backbone.Collection.extend({ model: Event url: '/event' }); return EventList;
Heißt das Model am Sails Server Event, lautet die dazu automatisch generierte Url /event. Diese Information reicht der Collection am Client, um all ihre Abfragen, Updates und Löschbefehle richtig zu adressieren.
Jedes Backbone Model besitzt die Methoden fetch(), save() and destroy(), wobei save einen create oder update Befehl bewirkt, je nachdem ob das Model bereits eine ID hat oder nicht.
Erweitert man also die Collection wieder und definiert eine initialize Funktion, die wie oben bereits erwähnt beim Instanzieren eines Objekts immer aufgerufen wird, kann man beispielsweise an dieser Stellte die fetch() Methode aufrufen, um die Daten vom Server zu holen.
var EventList = Backbone.Collection.extend({ model: Event url: '/event', initialize: function() { this.fetch(); } }); return EventList;
Hier wird laut den HTTP Methoden, die Backbone und Sails verwenden, ein GET Befehl an die Url http://www.myDomain.at/event gesendet. Sails liefert dann ein JSON mit allen Events zurück, welche dann als Backbone Event Models am Client verfügbar sind.
Das Zusammenspiel von Client und Server erfolgt so automatisiert und es müssen keine manuellen Requests geschickt oder am Server abgefangen werden.
The comments are closed.