Webanwendungen strukturieren mit Backbone und Chaplin
JavaScript Days, 5. März 2014
Programm
Vorstellung
Voraussetzungen
Code mit Backbone.js strukturieren
Anwendungen mit Chaplin.js
Ablauf
Vortragsteil mit Fragen und Antworten
Aufgaben und Beispiele
Umsetzungen besprechen, Refactoring
Vorstellungsrunde
Fortgeschrittene JavaScript-Programmierung? ECMAScript-Interna?
Erfahrungen mit jQuery, Backbone, Chaplin? Node.js?
Entwicklung von JavaScript-Webanwendungen? Frameworks?
JavaScript-Anwendungsfälle
Unobtrusive JavaScript
Formularvalidierung, Tabs, Menüs, Slideshows, Datepicker, Autocompletion
Bibliotheken: jQuery, Plugins
JavaScript-lastige Interfaces
Ajax, UI-Controls, Dialoge, Formular-Widgets, Drag and Drop
Bibliotheken: jQuery, jQuery UI
Single Page Applications
Bibliotheken: Backbone/Spine/CanJS, Ember, Angular
Frontend-Architekturen
JavaScript operiert auf dem HTML, das vom Server erzeugt wird
JavaScript lädt HTML vom Server nach (Ajax)
JavaScript lädt JSON oder XML und erzeugt das HTML daraus
Hybrid: Initiales HTML wird vom Server gerendert, JavaScript übernimmt
Vgl. Pamela Fox
Ziele für heute
JavaScript-Code der Typen 1 und 2 strukturieren
Das Entwurfsmuster Model View Controller anwenden
Refactoring von jQuery-Code
Übergang zu komplexen Anwendungen (3, 4)
Erkennen, wann welche Architektur sinnvoll ist
jQuery
Ausgereiftes Standardtool für DOM- und Ajax-Operationen
Das DOM ist umständlich und Low-Level
jQuery bietet eine kompakte Syntax und ist browserübergreifend
jQuery-Features
Elemente mittels CSS-Selektoren finden
DOM-Traversal (Bewegen im DOM-Baum)
DOM-Manipulation (Elemente einfügen, Inhalte wechseln, Attribute setzen)
CSS-Eigenschaften ändern, Animationen
Event-Handling (z.B. Maus- und Tastaturevents), Event-Delegation
HTTP-Anfragen an Server senden (Ajax)
Vorteile von jQuery
Schneller Einstieg
Knapper, konkreter, verständlicher Code
Häufige Aufgaben einfach lösen
Etablierte einige gute Konventionen
Allgegenwärtig, Ökosystem an Plugins
jQuery-Grundlagen
<p id="content">Hello World</p>
var jqueryObj = $('#content');
console.log( jqueryObj.length ); // 1
console.log( jqueryObj[0] ); // [object HTMLParagraphElement]
jQuery wrappt Elementobjekte in eine Listenobjekt mit vielen nützlichen Methoden
jQuery-Patterns
Grundlegender Datentyp: Listenobjekt mit Elementknoten
$()
und jQuery()
rufen new jQuery()
auf
jQuery.prototype
enthält Listenoperationen (each, map, filter, reduce usw.).
Objekt-orientiert (Konstruktor, Prototyp, Instanzen) und funktional (Listen, Chaining)
Elemente in ein mächtiges Listenobjekt wrappen: Eine geniale Abstraktion
Grenzen von jQuery
Deckt nur einen kleinen Bereich ab (DOM, Ajax)
jQuery bietet fast nichts zur Strukturierung
jQuery-Code skaliert nicht, wird schlecht wartbar
Letztlich sind fundierte JavaScript-Kenntnisse nötig
Nur intern modular, nach außen hin monolithisch
Beispiel: Bildergalerie
GET-Request auf die JSON-API von Flickr
HTML für Ergebnisse zusammenbauen und ins DOM einfügen
Vollansicht bei Klick
$.ajax({
url: 'http://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?',
dataType: 'json',
data: {
tags: term,
tagmode: 'all',
format: 'json'
},
success: successCallback
);
Objektorientierte Programmierung
OOP-Überblick
Objekte zur Programmstrukturierung
Funktionen und Closures für private Daten
Funktionale Programmierung, Funktionen als vollwertige Objekte
Prototypen für Code-Wiederverwendung und Pseudoklassen
Model View Controller
Bewährtes Entwurfsmuster für GUIs
Model: Daten und deren Logik
View: Darstellung der Daten, User Interface
Controller: Benutzeraktionen auswerten, Daten manipulieren
Controller erzeugt Model und View, View überwacht das Model
»Backbone.js gives structure to web applications«
Objektorientiert mit Pseudoklassen
Einfache und kleine Bibliothek (1.650 Zeilen)
Breiter Einsatz, aktive Entwicklung
Lesbarer, stabiler Code
Kostenlos und Open Source
Backbones Abhängigkeiten
Underscore, Lodash…
Werkzeugkasten für funktionale und OOP
jQuery, Zepto, Ender…
für DOM Scripting und Ajax
_.template, Mustache, Handlebars…
Backbones Grundideen
Die Trennung von Models und Views (Separation of Concerns)
Models laden, verarbeiten und speichern die Daten
Views stellen Daten im DOM dar und erzeugen das User Interface
Router/History synchronisiert den Anwendungsstatus mit der URL
Backbone.js richtig einordnen
Kein Framework, sondern eine kleine Bibliothek mit begrenzten Aufgabenbereich
Mit Absicht minimalistisch, überlässt einem viele Entscheidungen
Das ist der größte Vor- und Nachteil zugleich
Keine Allround-Lösung, sondern ein Teil des Software-Stacks
Nicht nur für Single-Page-Apps gedacht
Backbone-Übersicht
Backbone.Events
Basis für eine Event-basierte Architektur
Event-Handler registrieren und Events feuern
Backbones Kernfeature, benutzt von allen anderen Klassen
Komponenten reden über Events miteinander
Methoden: on
, off
, trigger
, listenTo
Models
Abrufen, Verarbeiten und Speichern von Daten
Models sind die »einzige Quelle der Wahrheit«
Daten werden nicht im DOM gespeichert
Kernfeature: Das attributes
-Objekt
Attribute lesen und schreiben mit get()
und set()
Änderungen erzeugen change
-Events
Models
// Unterklassen mit extend() erzeugen
var Car = Backbone.Model.extend();
// Instanz mit vordefinierten Attributen
var car = new Car({
name: 'DeLorean DMC-12',
manufactured: 1981
});
// Attribut lesen
console.log( car.get('name') );
// Attribute schreiben
car.set('manufactured', 1982); // Ein Attribut
car.set({ manufactured: 1982 }); // Mehrere Attribute
console.log( car.get('manufactured') );
Demonstration
Model-Events überwachen
car.on('change', function (car, options) {
console.log('Some attribute changed');
});
car.on('change:manufactured', function (car, newValue, options) {
console.log('manufactured changed:', newValue);
});
car.set({ manufactured: 1982 });
Demonstration
Models laden und speichern
Synchronisierung über RESTful HTTP mit JSON
urlRoot
angeben, z.B. cars
model.fetch()
erzeugt ein GET /cars/:id
model.save()
erzeugt ein POST/PUT /cars/:id
model.destroy()
erzeugt ein DELETE /cars/:id
Models laden und speichern
var Car = Backbone.Model.extend({
urlRoot: '/cars'
});
model.fetch().then(successCallback, failCallback);
model.save().then(successCallback, failCallback);
model.destroy().then(successCallback, failCallback);
Promises / jQuery Deferreds
Collections
Eine Liste von Models
Feuert die Events add
, remove
and reset
Kurz gesagt, ein überwachbarer Array
Listen-Helfer (each
, map
, reduce
, sort
, filter
…)
Collections
var Car = Backbone.Model.extend();
var Cars = Backbone.Collection.extend({ model: Car });
var cars = new Cars([
{ name: 'DeLorean DMC-12', manufactured: 1981 },
{ name: 'Chevrolet Corvette', manufactured: 1953 }
]);
alert( cars.at(0).get('name') ); // DeLorean DMC-12
cars.push({ name: 'VW Scirocco', manufactured: 1974 });
alert( cars.length ); // 3
Views
Eine View verwaltet ein Element (this.el
, this.$el
)
Darstellung der Modeldaten (Render-Pattern)
Referenz auf ein Model oder eine Collection
Verarbeitet DOM-Events (Nutzereingaben)
Überwacht Model-Events (Model-View-Binding)
Ruft Model-Methoden auf oder emittiert Events
View ohne Template
var CarView = Backbone.View.extend({
initialize: function () {
// Überwache Model-Änderungen: Neu rendern
this.listenTo(this.model, 'change', this.render);
},
render: function () {
this.$el.html('Name: ' + this.model.get('name'));
}
});
var carView = new CarView({
model: car,
el: $('#car')
});
carView.render();
Demonstration
Templates
Views übersetzen Modeldaten in HTML mithilfe eines Templates
Modeldaten:
{ message: 'Hello World' }
Template:
<p>{{message}}</p>
Ausgabe:
<p>Hello World</p>
Der erzeugte HTML-Code wird ins DOM eingefügt
View mit Template
var CarView = Backbone.View.extend({
template: _.template('Name: {{name}}'),
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () {
this.$el.html(this.template(this.model.attributes));
}
});
var carView = new CarView({
model: car,
el: $('#car')
});
carView.render();
Demonstration
Model-View-Binding
Muss manuell eingerichtet werden
Eine View hört auf Model-Änderungen-
Die View rendert sich neu oder aktualisiert das DOM
this.listenTo(this.model, 'change', this.changeHandler)
Eine View hört auf Nutzereingaben und ruft Model-Methoden auf oder feuert Events beim Model
Demonstration
Einsatzbereiche
Die Trennung von Model- und View-Logik und das Zusammenfügen durch Events ist nützlich und vielseitig anwendbar. Auch wenn Backbone nicht verwendet wird. Viele andere Bibliotheken haben diese Struktur übernommen.
Refactoring der Bildergalerie
Photos
-Collection lädt die Daten von Flickr
SearchView
steuert das Suchformular
PhotosView
überwacht und rendert die Collection
PhotoItemView
für jedes Item in der Liste
FullPhotoView
für die Vollansicht
Beispielimplementierung
Backbone-basierte Frameworks
Frameworks geben eine sinnvolle Anwendungsstruktur
Treffen Entscheidungen, die Backbone einem überlässt
Ermöglichen Single-Page-Apps
Marionette , Chaplin , Thorax
Anwendungsarchitektur auf Basis von Backbone
Best Practices und Konventionen
In CoffeeScript geschrieben
Modularisiert mit CommonJS/AMD
Open Source, automatisiert getestet
Chaplin.js
Chaplin-Komponenten (1)
Application startet die Kernkomponenten
Router überwacht URL-Änderungen und prüft, ob Routen auf die URL passen
Dispatcher startet und verwaltet Controller wenn Routen passen
Controller erzeugen Models und Views
Chaplin-Komponenten (2)
Composer zum Wiederverwenden von Models/Views zwischen Controllern
Layout ist die oberste View, fängt Klicks auf Links ab
Regions sind benannte Bereiche im UI, die gefüllt werden können (z.B. main-content
, sidebar
)
mediator zur Kommunikation via Publish/Subscribe
Routing-Ablauf
Router ⇢ Dispatcher → Controller → Model/View
⇢
benachrichtigt
→
erzeugt
Routen
Konfigurationsdatei routes.js
match('', 'homepage#show');
match('cars', 'cars#index');
match('cars/:id', 'cars#show');
Pfad /
– Controller homepage
– Action show
Pfad /cars
– Controller cars
– Action index
Pfad /cars/:id
– Controller cars
– Action show
Controller
var CarsController = Controller.extend({
index: function () {
this.cars = new Cars();
this.view = new CarsView({ collection: this.cars });
},
// cars/:id
// cars/1
show: function (params) {
this.car = new Car({ id: params.id });
this.view = new CarView({ model: this.car });
}
});
Controller
Erzeugen Models/Collections (this.model
/this.collection
)
Erzeugen die Haupt-View (this.view
)
Methoden sind Actions
Nehmen URL-Parameter entgegen (params
)
Controller-Lebenszyklus
Ein aktiven Controller, der die Haupt-View erzeugt
Beim Starten eines Controllers wird der vorherige Controller mitsamt Models und Views abgebaut
Models und Views können kontrolliert wiederverwendet werden (Composer)
Persistente Controller sind möglich
Object Disposal
Definierter Lebenszyklus von Controllern, Models und Views
Alle Komponenten haben eine dispose
-Methode als Destructor
Garbage Collection ermöglichen zur Vermeidung von Memory-Leaks
Wichtig bei Event-basierten Architekturen
Chaplin wirft per default alles weg
Mediator
MVC-Komponenten haben keine Referenzen aufeinander
Ein drittes Objekt zum Datenaustausch
Publish/Subscribe-Pattern
mediator.publish()
, mediator.subscribe()
Modularisierung
Chaplin nutzt die Modulstandards CommonJS bzw. AMD
Abhängigkeiten zwischen Klassen deklarieren
Abhängigkeitsbaum maschinell auslesen
Richtige Ladereihenfolge der Scripte
Lazy-Loading von Abhängigkeiten
Packaging mehrerer Module in einer Datei
Beispiel AMD
define(
[
'controllers/base/controller',
'models/cars',
'models/car',
'views/cars-view'
'views/car-view'
],
function (Controller, Cars, Car, CarsView, CarView) {
'use strict';
var CarsController = Controller.extend({ … });
return CarsController;
}
);
Demonstration
Modularisierung
Chaplin besteht aus Modulen
Jede Klasse, jedes Singleton-Objekt ist ein Modul
Ein Modul pro Datei
Der Name entspricht dem Dateipfad ohne Endung, z.B. controllers/hello_world_controller
Modularisierungstools
Require.js – browserseitiger AMD-Loader
r.js – Pakete aus AMD-Modulen schnüren
Almond.js – browserseitige Minimalimplementierung ohne Loader
Browserify – Pakete aus CommonJS-Modulen schnüren
Chaplin-Views
Vordefinierte render
-Methode
Deklaratives Event-Handling:events
und listen
Optionen: autoRender
und container
Subviews mit der subview
-Methode