Aus Linux-Magazin 11/2013

Ruby on Rails plus Angular.js

© Graham Oliver, 123RF.com

Das Webframework Ruby on Rails nimmt dem Entwickler viel Arbeit ab und lässt sich doch weitgehend anpassen. In der Beispielanwendung liefert es lediglich die Daten im Json-Format, das Anzeigen im Browser übernimmt die Javascript-Bibliothek Angular.js.

Ruby on Rails ist ein Webframework auf Basis der Programmiersprache Ruby. Im Jahr 2004 hat es der dänische Entwickler David Heinemeier Hansson erstmals als Open Source veröffentlicht, inzwischen liegt die kurz auch Rails [1] genannte Software in der frisch gebackenen Version 4 vor.

Rails verspricht die Entwicklung von Datenbank-gestützten Webanwendungen stark zu beschleunigen. Zusammen mit dem Python-Framework Django begründete es eine neue Klasse von sehr kompletten Frameworks, zu denen zum Beispiel auch das PHP-Framework Symfony zu zählen ist, die dem Entwickler an vielen Stellen Arbeit abnehmen, typische Aufgaben mit Generatoren und anderen Konstrukten automatisieren und es einfach machen, Best Practices der Webentwicklung zu folgen.

Der Programmierer

Der Code stammt von Daniel Harrington https://github.com/rubiii. Er ist freiberuflicher Entwickler in Hamburg und arbeitet aktuell vorwiegend mit Ruby und Javascript.

Über seine nun bald zehnjährige Geschichte hat Rails zahlreiche Trends in der Webentwicklung mitgeprägt oder zumindest früh aufgegriffen. So sind etwa Prototype.js und Script.aculo.us, die als Vorgänger von Bibliotheken wie Jquery gelten können, im Rails-Umfeld entstanden. Rails war zudem eines der ersten Frameworks, die es dem Entwickler leicht gemacht haben, Webseiten mit Ajax-Funktionen anzureichern.

Rails nutzt dabei die Sprachmittel von Ruby sehr geschickt, um dynamisch zur Laufzeit Informationen zur Verfügung zu stellen, die in anderen Umgebungen bereits zur Entwurfszeit in Code gegossen werden.

Die Anwendung

Eine laufende Version der gelösten Aufgabe (Abbildung 1) findet sich unter [2]. Der Quellcode liegt unter [3] und lässt sich, sofern Ruby 2.0.0 installiert ist, recht einfach auch lokal betreiben. Eine genaue Beschreibung der weiteren Abhängigkeiten ist in der Readme-Datei im Quelltext nachzulesen.

Abbildung 1: Die Beispielanwendung verwendet einen Schieberegler (Slider) zur Datumseingrenzung und läuft auf der Plattform Heroku.

Abbildung 1: Die Beispielanwendung verwendet einen Schieberegler (Slider) zur Datumseingrenzung und läuft auf der Plattform Heroku.

Eine typische Rails-Anwendung folgt im Wesentlichen einer eigenen Interpretation des allgegenwärtigen Model-View-Controller-Pattern (MVC). Die Modelle sind in der Regel so genannte »ActiveRecord« -Objekte, die mit Hilfe von Introspektion auf die Datenbank ohne zusätzlichen Code aus den Datenbankspalten Attribute und Methoden generieren.

Da es bei den vorgegebenen Daten um Festivitäten – neudeutsch Events – geht, liegt es nahe, ein Event-Model zu bauen. Es lässt sich, zusammen mit einer Migration, die das Datenbankschema anlegt, über einen Kommandozeilenaufruf generieren (hier nur ausschnittweise):

$ rails generate model Event source_id:string area:text description:textstreet:string[...]

Der Generator produziert drei Dateien: das etwas anämische Model (die Klasse »Event« ), einen vorerst ebenso spartanischen Test namens »EventTest« und die eben erwähnte Migration (Listing 1).

Listing 1

Datenbank-Migration

01 # 20130903183431_create_events.rb
02 class CreateEvents < ActiveRecord::Migration
03   def change
04     create_table :events do |t|
05       t.string :source_id
06       t.text   :area
07       t.text   :description
08       t.string :street
09       t.string :zip_code
10       t.date   :starts_at
11       t.date   :ends_at
12       t.text   :period
13       t.text   :host
14       t.string :email
15       t.string :website
16       t.text   :note
17       t.timestamps
18     end
19
20     add_index :events, :source_id
21   end
22 end

Datenbank-Migrationen

Migrationen in Rails sind möglichst Datenbank-agnostische Dateien, die in Ruby-Code beschreiben, wie das Schema der Datenbank zu ändern ist. Da diese Migrationen versioniert sind, kann man mit ihnen eine Datenbank automatisiert auf den aktuellen Stand bringen. Dafür ist eine Rake-Task zuständig. Rake [4] ist das leistungsfähige Ruby-Äquivalent zu Make und dient für allerlei Automatisierungen. Das Kommando »rake db:migrate« migriert das Datenbankschema auf den neuesten Stand.

Um die Daten in die Datenbank zu importieren, existiert eine weitere Rake-Task, die allerdings selbst gebaut ist: »rake workers:import« ruft die Daten alle auf einmal vom Berlin.de-Server ab, übersetzt die Attribute in die Datenstruktur für die Event-Klasse und legt dann je nach Stand der Datenbank neue Events an oder aktualisiert alte. Eine solche Task lässt sich per Cron regelmäßig ausführen. Die Task selbst ruft eigentlich nur eine Methode einer Klasseninstanz auf (Listing 2)

Listing 2

Import-Task

01 namespace :workers do
02   desc 'Import events from source'
03   task :import => :environment do
04     ImportWorker.new.run
05   end
06 end

Der »ImportWorker« liegt im Verzeichnis »app/workers/« . Dies ist kein Standardverzeichnis, Rails lädt jedoch automatisch alle Dateien, die in Unterverzeichnissen von »app/« liegen. Der Worker holt in einem Rutsch mit Hilfe der Bibliothek HTTPClient [5] die Inhalte vom Berlin.de-Server, benutzt Rubys Json-Bibliothek, um das ganze zu parsen, und schreibt dann, Event für Event, die Inhalte in die Datenbank. Hierzu gibt es eine Mapping-Methode, die die Feldnamen in den Json-Daten in Datenbankattribute übersetzt, sowie eine »upsert_event()« -Methode, die ein Event entweder anlegt oder aktualisiert (Listing 3).

Listing 3

ImportWorker

01 class ImportWorker
02   [...]
03   def map_to_event_hash(data)
04     {
05       source_id:   data['id'],
06       area:        data['bezirk'],
07       description: data['bezeichnung'],
08       street:      data['strasse'],
09       zip_code:    data['plz'],
10       starts_at:   Date.parse(data['von']),
11       ends_at:     Date.parse(data['bis']),
12       period:      data['zeit'],
13       host:        data['veranstalter'],
14       email:       data['mail'],
15       website:     data['www'],
16       note:        data['bemerkung']
17     }
18   end
19
20   def upsert_event(source_id, attributes)
21     Event.where(source_id: source_id)
22       .first_or_initialize
23       .update_attributes(attributes)
24   end
25 end

Die Methode »upsert_event« zeigt sehr schön die Benutzung der Objekt-Abstraktionsschicht »ActiveRecord« : Unter der Haube von »ActiveRecord« arbeitet eine ausgereifte Bibliothek für den Umgang mit relationaler Algebra. Hier ist der Fall eindeutig – die »where()« -Methode erzeugt eine SQL-»WHERE« -Klausel in der Form »WHERE source_id = ?« .

Das »first_or_initialize« liefert entweder den ersten Treffer der Abfrage zurück oder aber ein leeres Objekt, wenn kein Treffer gefunden, das Event also noch nicht importiert wurde. Die Methode »update_attributes« aktualisiert die Attribute und speichert das Ganze in der Datenbank.

Die nächste Schicht im Rails-MVC-Modell ist der Controller, der den HTTP-Request entgegennimmt und beantwortet. Wie Listing 4 zeigt, ist er recht simpel. Den Request nimmt die »index« -Methode entgegen. Das »respond_to« legt fest, dass dieser Controller nur auf Anfragen reagiert, die Json verlangen. In der Index-Methode liest »Event.all« die ganze Events-Tabelle aus. Die Berliner Datenquelle liefert derzeit knapp 300 Events, sodass sich die Rechenarbeit in Grenzen hält. Das »respond_with()« sorgt dafür, dass die Daten im richtigen Datentypus ausgegeben werden. »ActiveRecord« bringt für Json und XML bereits eigene Methoden zur Konvertierung mit, sodass der Entwickler hier der Einfachheit halber die rohen Daten aus der Datenbank ausgeben darf.

Listing 4

Controller

01 class EventsController < ApplicationController
02
03   respond_to :json
04
05   def index
06     respond_with(Event.all)
07   end
08
09 end

Templates

Unter Praxisbedingungen ist der Programmierer oft gezwungen die Daten vor der Ausgabe noch zu modifizieren. Hierfür gibt es derzeit zwei besonders geeignete Ansätze: Die so genannten Jbuilder-Templates, die Teil von Rails 4 sind, erlauben es, Json mit einer Art Templating-Sprache zusammenzubauen – angelehnt an die Builder-Bibliothek, die in Rails zum Generieren von XML dient. Die »ActiveModel« -Serializer gehen einen anderen Weg, indem sie eine eigene Kategorie von Klassen einführen, die Serializer, die mit einer ebenfalls recht simplen Syntax das Umarbeiten von Datenstrukturen ermöglichen.

Damit liegt unter »/events.json« also eine vollständige Liste von Events als Json-Daten (Abbildung 2). Nun geht es darum, sie in der gewünschten Form anzuzeigen.

Abbildung 2: Ruby on Rails liefert in der Beispielanwendung Json-Daten für die Anzeige.

Abbildung 2: Ruby on Rails liefert in der Beispielanwendung Json-Daten für die Anzeige.

Die View-Schicht in Rails besteht im Prinzip nur aus Templates. Diese verwenden standardmäßig eine Template-Engine namens Embedded Ruby (ERB), es existiert allerdings noch eine Handvoll Alternativen. Der Anwendungsautor hat sich hier für eine relativ moderne Variante namens Slim [6] entschieden, die mit signifikantem Whitespace und einer sehr reduzierten Syntax auskommt. Das Template, das die Homepage rendert, ist in Listing 5 zu sehen.

Listing 5

Slim-Template

01 h1 Berliner und Brandenburger Volks- und Straßenfeste 2013
02
03 #events ng-controller='eventsController'
04   #range-slider
05   .range-info
06     span.count
07       | {{groupedEventsCount}} Veranstaltungen
08     span
09       | im gewählten Zeitraum
10
11   div class='ongoing-events' ng-show='groupedEvents.ongoing' ng-cloak=''
12     h2
13       | {{groupedEvents.ongoing.length}} laufende Veranstaltung(en)
14     ol.events
15       li ng-repeat='event in groupedEvents.ongoing'
16         p
17           | {{event.description}}
18         time
19           | {{event.startsAt | date:'dd.MM.yyyy'}} - {{event.endsAt | date:'dd.MM.yyyy'}}
20         span
21           | in {{event.area}}
22
23   div class='upcoming-events' ng-show='groupedEvents.upcoming' ng-cloak=''
24     h2
25       | {{groupedEvents.upcoming.length}} anstehende Veranstaltung(en)
26     ol.events
27       li ng-repeat='event in groupedEvents.upcoming'
28         p
29           | {{event.description}}
30         time
31           | {{event.startsAt | date:'dd.MM.yyyy'}} - {{event.endsAt | date:'dd.MM.yyyy'}}
32         span
33           | in {{event.area}}
34
35   div class='passed-events' ng-show='groupedEvents.passed' ng-cloak=''
36     h2
37       | {{groupedEvents.passed.length}} vergangene Veranstaltung(en)
38     ol.events
39       li ng-repeat='event in groupedEvents.passed'
40         p
41           | {{event.description}}
42         time
43           | {{event.startsAt | date:'dd.MM.yyyy'}} - {{event.endsAt | date:'dd.MM.yyyy'}}
44         span
45           | in {{event.area}}

Layouts

Natürlich entsteht so keine vollständige HTML-Seite. Templates in Rails werden in aller Regel in ein so genanntes Layout gegossen, ein Template, das den Seitenrahmen enthält und eine Stelle definiert, an der der Code der Einzelseiten erscheinen soll. Auch dieses ist in Slim geschrieben (Listing 6)

Listing 6

Slim-Layout

01 doctype html
02 html
03   head
04     title Festive
05     = stylesheet_link_tag 'application'
06     = javascript_include_tag 'application'
07     = csrf_meta_tags
08
09   body ng-app='Festive'
10     #content
11       == yield
12
13       #footer
14         | Source code available at
15         a href='https://github.com/rubiii/festive' github.com/rubiii/festive

Die »ng« -Attribute deuten es schon an: Der Anwendungsentwickler hat es sich nicht nehmen lassen, zusätzlich zu Rails noch eine derzeit angesagte Technologie mit ins Spiel zu bringen: Das Frontend ist mittels Angular.js [7], einem Javascript-Framework von Google, realisiert. Das Template rendert hier gewissermaßen nur ein Template für Angular.js. Bis auf die Überschrift übernimmt Angular alle Elemente und zeigt sie dynamisch zur Laufzeit an.

Angular.js

Damit verlässt der Entwickler die Ebene des Server-seitigen Rails und landet bei Javascript im Client. Da die Beschreibung der Angular-Anwendung den Rahmen dieses Artikels sprengen würde, sei nur so viel gesagt: Die Angular-Anwendung lädt als Erstes die Daten von »/events.js« und filtert diese dann eigenständig nach Start- und Enddatum.

Die Angular.js-Anwendung ist in Coffeescript geschrieben, einer Sprache, die ein wenig an Ruby erinnert und nach Javascript kompiliert. Rails unterstützt Coffeescript mit der so genannten Asset-Pipeline. Hierbei handelt es sich um ein Feature, das seit Rails 3.1 an Bord ist: Es verarbeitet alle Dateien, die unter »app/assets/« liegen. Wesentliche Aufgaben der Asset-Pipeline sind das Zusammenfügen mehrerer Quelldateien, etwa um Stylesheets besser aufteilen zu können, ohne im Produktivbetrieb alle Stylesheets einzeln laden zu müssen, sowie das Einbinden von diversen Compilern und Minifiern wie Uglify.js, Google Closure oder der YUI-Tools.

Im Development-Modus ist standardmäßig das Minifizieren und Zusammenführen ausgeschaltet, was das Debuggen erleichtert. Beim Deployment packt eine Rake-Task die Assets zusammen, führt die Minification durch und legt die Dateien nach »public/assets/« , von wo der Webserver sie ausliefern kann. Wie in dem Layout zu sehen ist, integriert die Anwendung genau eine Javascript-Datei namens »application« – die Endung ».js« versteht sich implizit.

Zunächst bindet Listing 7 die Bibliothek Jquery [8] ein, die »range-slider« benötigt. Danach kommen Underscore.js [9] und Angular.js. Da es sich bei den Bibliotheken um externe Abhängigkeiten handelt, liegen sie nicht im Verzeichnis »app/assets/« , sondern in »vendor/assets/« . Als Letztes bindet Rails alle Javascript- (oder Coffeescript-)Dateien ein, die sich in »app/assets/javascripts/« oder Unterordnern davon befinden.

Listing 7

Javascript

01 #= require jquery/jquery
02 #= require underscore/underscore
03 #= require angular/index
04 #
05 #= require range-slider
06 #= require_tree .

Die externen Abhängigkeiten verwaltet übrigens wenn möglich Bower [10]. Das Tool wurde von Twitter entwickelt und hat sich zu einem De-facto-Standard für das Javascript-Paketmanagement gemausert. Bower speichert die Abhängigkeiten in einem Format, das an das Json-Format der Node.js-Pakete erinnert. Die »bower.json« -Datei des Projekts zeigt Listing 8. Zusätzlich muss in einer Datei namens ».bowerrc« der Zielpfad stehen:

Listing 8

bower.json

01 {
02   "name": "festive",
03   "version": "0.1.0",
04   "dependencies": {
05     "jquery": "1.9.1",
06     "underscore": "1.4.3",
07     "angular": "http://code.angularjs.org/1.2.0-rc.2/angular.js",
08     "angular-mocks": "http://code.angularjs.org/1.2.0-rc.2/angular-mocks.js"
09   }
10 }
{
  "directory": "vendor/components"
}

Danach kann der Anwender die benötigten Pakete mit dem Kommando »bower install« installieren.

Beurteilung

Die gewählte Lösung hat bei aller Attraktivität des dynamischen Frontends das Problem, dass sie ohne Javascript nicht funktioniert. Eine Lösung, die komplett ohne Javascript auskommt, wäre sicher nicht viel aufwändiger zu programmieren. Zum Beispiel könnte der »EventsController« den anzuzeigenden Datumsbereich gleich Server-seitig verarbeiten (Listing 9).

Listing 9

Alternativer Controller

01 class EventsController < ApplicationController
02
03   respond_to :json
04
05   def index
06    respond_with(Event.where("starts_at <= ? OR ends_at >= ?", params[:end_date], params[:start_date]))
07   end
08
09 end

Gleichwohl demonstriert die Anwendung die Flexibilität von Rails, die es einfach macht, als Alternative zu einer normalen Webanwendung eine Single-Page-App plus Json-API zu bauen. Das ist gerade in den jüngsten Rails-Versionen deutlich leichter geworden.

Eine der Stärken von Rails ist die gute Integration von automatisierten Tests. Die Beispielanwendung ist keine Ausnahme: Der Test für den »ImportWorker« importiert versuchsweise Testdaten, die nicht im Web, sondern einfach in einer Datei, im Rails-Jargon Fixture genannt, abgelegt sind. Ermöglicht wird dies durch das so genannte Mocken des eigentlichen HTTP-Requests mit Hilfe der Mocking-Bibliothek Mocha [11].

Unter »/test/integration« gibt es zudem einen Integrationstest, der die Bibliothek Capybara und den Selenium-Webdriver [12] verwendet, um echte Frontend-Integration-Tests in einem Webbrowser auszuführen, was bei dieser Art Anwendung fast zwingend notwendig ist.

Zusätzlich zu diesen Rails-Tests bringt die Beispielanwendung auch noch einen Javascript-Test mit. Er ist mit dem Projekt Karma [13] realisiert, das aus dem Angular.js-Umfeld kommt. Die Besonderheit von Karma ist, dass es die Tests parallel in einer ganzen Reihe von Browsern ausführen kann und die Ergebnisse anschließend zusammenführt. All diese automatisierten Tests der Anwendung laufen auch auf der gehosteten Continuous-Integration-Plattform Travis-CI [14].

Abbildung 3: Das Testwerkzeug Karma prüft das Javascript der Anwendung parallel in mehreren Webbrowsern.

Abbildung 3: Das Testwerkzeug Karma prüft das Javascript der Anwendung parallel in mehreren Webbrowsern.

Die von der Redaktion gestellte Programmieraufgabe ist keine besonders passende Anforderung an Rails. Viele Ruby-Entwickler hätten für diese Aufgabe vielleicht eher ein Mikroframework wie Sinatra eingesetzt und die Json-Anfragen einfach durchgereicht oder nur in einem Memory-Cache zwischengespeichert.

Ausblick

Rails spielt seine Stärken vor allem dort aus, wo schnell, effizient, aber auch sorgfältig Webanwendungen entstehen sollen – nicht umsonst ist Rails insbesondere bei Startups sehr beliebt. Wer sich an die von Rails vorgegebenen Konventionen hält, ist als Entwickler außerdem deutlich weniger mit dem Schreiben des typischen Boilerplate-Codes beschäftigt.

Natürlich schläft die Konkurrenz nicht. Die Rails-Gemeinde ist jedoch immer noch sehr weit vorne dabei, wenn es darum geht, Trends gerade im Tooling zu setzen. Die Asset-Pipeline, in diesem Artikel kurz erwähnt, ist mit Rails 4 endlich erwachsen geworden und sorgt zusammen mit dem gesamten Ökosystem und Tools wie Sass, Compass oder Coffeescript dafür, dass Frontend-Entwicklung einen ähnlichen Stellenwert erhält wie das Backend. Dennoch bleibt abzuwarten, wie sich das gesamte Web-Tooling in den nächsten Jahren verändert und ob die Rails-Community Wege findet, aktuelle Entwicklungen wie die Javascript-Tools Grunt und Bower besser zu integrieren. Ansonsten könnte sie am Ende mit eigenen Insellösungen dastehen. (mhu)

Der Autor

Jan Krutisch http://jan.krutisch.de ist freiberuflicher Software-Entwickler und Autor. Er arbeitet momentan vor allem für Verlage an Themen wie mobile Websites und E-Books.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben