Webanwendungen strukturieren mit Backbone und Chaplin

JavaScript Days, 5. März 2014

Mathias Schäfer (molily)

Programm

  1. Vorstellung
  2. Voraussetzungen
  3. Code mit Backbone.js strukturieren
  4. Anwendungen mit Chaplin.js

Ablauf

  • Vortragsteil mit Fragen und Antworten
  • Aufgaben und Beispiele
  • Umsetzungen besprechen, Refactoring

Werkzeuge

Vorstellungsrunde

  • Fortgeschrittene JavaScript-Programmierung? ECMAScript-Interna?
  • Erfahrungen mit jQuery, Backbone, Chaplin? Node.js?
  • Entwicklung von JavaScript-Webanwendungen? Frameworks?

JavaScript-Anwendungsfälle

  1. Unobtrusive JavaScript Formularvalidierung, Tabs, Menüs, Slideshows, Datepicker, Autocompletion Bibliotheken: jQuery, Plugins
  2. JavaScript-lastige Interfaces Ajax, UI-Controls, Dialoge, Formular-Widgets, Drag and Drop
    Bibliotheken: jQuery, jQuery UI
  3. Single Page Applications Bibliotheken: Backbone/Spine/CanJS, Ember, Angular

Frontend-Architekturen

  1. JavaScript operiert auf dem HTML, das vom Server erzeugt wird
  2. JavaScript lädt HTML vom Server nach (Ajax)
  3. JavaScript lädt JSON oder XML und erzeugt das HTML daraus
  4. 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

Beispiel: Bildergalerie

  1. GET-Request auf die JSON-API von Flickr
    • ajax() oder getJSON()
  2. HTML für Ergebnisse zusammenbauen und ins DOM einfügen
    • html() oder append()
  3. Vollansicht bei Klick
    • html(), click()

$.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 (backbonejs.org)

  • »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…
    • für HTML-Templates

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

Chaplin.js (chaplinjs.org)

  • 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

Chaplin-Boilerplate

Chaplin Car Manager