Frontend Dependency-Management in Rails

Im letzten Blogpost über Bower sind viele Gründe für die Verwendung eines Dependency-Management Tools für Frontend-Komponenten genannt worden. Dieser Artikel soll dort anknüpfen, wo der letzte aufgehört hat, und eine Reihe von Möglichkeiten aufzeigen, mit denen Bower in ein fremdes Ökosystem integriert werden kann. Denn in der Regel trifft ein Webframework wie Ruby on Rails oder Play! eigene Vorgaben darüber wie Frontend-Artefakte von einer Webanwendung verwaltet und ausgeliefert werden. Im Folgenden wird Rails als populärer Vertreter eines Webframeworks für einige dieser Integrations-Strategien herhalten.

Gems und Bundler

Das auf der Programmiersprache Ruby basierende Webframework Rails besitzt für die Verwaltung von Anwendungspaketen, den sogenannten Gems, ein eigenes Dependency-Management Tool namens Bundler. Ganz ähnlich zu Bower gibt es auch hier eine spezielle Manifest-Datei, das Gemfile, welches im Wurzelverzeichnis der Webanwendung liegt und sämtliche Abhängigkeiten mit ihren Versionen definiert. Mit einer Hand voll Befehlen lassen sich dann die darin definierten Gems installieren und weiter verwalten.

Im Rails Umfeld ist es daher nicht unüblich, dass viele populäre Frontend-Bibliotheken in eigene Gems eingepackt werden, um sie ebenfalls über Bundler installieren und verwalten zu können. Das hat u.a. den Vorteil, dass kein weiteres Tooling für das Auflösen von Frontend Abhängikeiten benötigt wird und sich diese “Wrapper-Gems” in der Regel auch nahtlos über Generatoren, Rake Tasks oder Helper Methoden in das Framework einbinden lassen. Ein großer Nachteil dieses Ansatzes ist, dass solche Gems immer etwas hinter der Entwicklung der eingepackten Frontend-Komponente hinterher hinken. Hinzu kommt, dass es häufig auch noch mehrere unterschiedlich gut gepflegte Gems für ein und dieselbe Frontend-Komponente gibt oder schlimmstenfalls sogar gar kein Gem vorhanden ist.

Rails Asset-Pipeline

Die sogenannte Asset-Pipeline in Rails ist der Teil des Frameworks der sich um die Verwaltung, Verarbeitung, Optimierung und Auslieferung von Frontend Artefakten wie CSS, JavaScript, Bilder und Schriften kümmert. So ist es möglich, automatisch Präprozessoren für das Generieren von CSS und oder JavaScript anzustoßen, Dateien zu konkatenieren und minifizieren sowie bei jeder Änderungen mit einem Fingerprint zu versehen, um das Caching des Browsers optimal auszunutzen.

Rails gibt darüber hinaus bestimmte Konventionen vor, wie Frontend Artefakte abzulegen sind, damit sie automatisch von der Asset-Pipeline auffindbar sind. Anwendungsspezifische Assets sollten daher im Ordner

app/assets/
├── fonts
├── images
├── javascripts
│   └── application.js
└── stylesheets
    └── application.css

abgelegt werden. Die generierten Dateien application.css respektive application.js dienen dabei als sogenannte Index oder auch Manifest Dateien. In ihnen werden ausschließlich Pfade zu anderen Dateien gepflegt, die letzten Endes in einer einzigen Datei zusammengeführt bzw. konkateniert werden sollen. Sie enthalten darüber hinaus idealerweise keinen weiteren Code. Ein Beispiel für eine solche application.js Datei sieht so aus:

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .

Darin wird über die require Direktiven am Ende definiert, dass die Dateien jquery.js, jquery_ujs.js und turbolinks.js, sowie alle Dateien im selben Verzeichnis (require_tree .) in genau dieser Reihenfolge paketiert werden sollen. Die Magie, die sich darum kümmert, diese Direktiven auszuwerten und besagte Dateien in einer Datei aneinander zu kleben sowie auszuliefern, übernimmt intern die Bibliothek Sprockets.

Neben dem app/assets/ Verzeichnis gibt es auch noch zwei weitere Verzeichnisse, in denen sich Assets, die für die Asset-Pipeline zugreifbar sein sollen, ablegen lassen: vendor/assets und lib/assets. Rails unterscheidet neben anwendungsspezifischen Assets per Konvention auch in solche von Drittanbietern sowie eigenen, die aus Gründen die Wiederverwendung in eine Library ausgelagert sind. Werden Assets dieser Konvention entsprechend in der Anwendung abgelegt, können diese ohne weitere Konfiguration oder Angabe von zusätzlichen Pfaden genau so wie Assets unter app/assets in Index Dateien mit der require Direktive eingebunden werden. Ein Beispiel: Das jQuery Plugin typeahead ist unter vendor/assets/javascript/typeahead.js abgelegt und kann dann einfach in der application.js von oben wie folgt eingebunden werden

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require typeahead
//= require_tree .

Gemfile vs. Vendor Assets

Wie sich vielleicht schon erahnen lässt, kann es zu Problemen kommen, wenn Frontend-Komponenten zum Einen in Form von Gems und zum Anderen über Verzeichnisse wie vendor/assets eingebunden werden. Angenommen, jQuery wird über ein Gem in einer bestimmten Version 1.11.1 in der application.js per require Direktive eingebunden. Daneben liegt ein jQuery Plugin unter vendor/assets/javascript/, das eine jQuery Version < 2.0.0 voraussetzt und welches ebenfalls in application.js eingebunden wird. Wird nun das jQuery Gem auf eine Version >= 2.0.0 aktualisiert, kann es passieren, dass das Plugin in der gepackten application.js im Browser des Nutzers Fehler wirft oder sogar die weitere Ausführung des Skripts verhindert. Dies ist nur ein sehr kleines, konstruiertes Beispiel, welches aber sehr gut verdeutlicht, wie schnell es passieren, dass Abhängigkeiten zwischen einzelnen Frontend-Komponenten in die Brüche gehen und zu Fehlern führen können, wenn sie über die Rails Boardmittel verwaltet werden.

Wie im letzten Blogpost beschrieben, nimmt sich Bower genau dieser Problematik an, in dem es Abhängigkeiten zwischen Frontend-Komponenten separat verwaltet und diese bei Updates entsprechend auflöst oder ein Update gänzlich verhindert. Da Bower seine eigenen Konventionen und Gepflogenheiten voraussetzt, ist es nicht möglich, Bower “Out-of-the-box” in einem Rails Projekt lauffähig zu machen. Für die Integration bieten sich aber dennoch einige Möglichkeiten mit ganz eigenen Vor- und Nachteil an, die im Folgenden beschrieben werden.

Bower on Rails

Die erste und naheliegenste Variante, Bower in Rails zu integrieren, ist, die Boardmittel von Bower zu verwenden. Dazu kann Bower zum Initialisieren einer bower.json Datei im Wurzelverzeichnis der Railsanwendung mit bower init aufgerufen werden. Mit Hilfe einer angepassten Konfigurationsdatei .bowerrc, deren Inhalt z.B. so aussieht

{
  "directory": "vendor/assets/bower_components/"
}

lassen sich dann sämtliche Bower Pakete nicht unter bower_components/, sondern in dem für Rails vorgesehenen Ordner vendor/assets/ installieren. Da unterhalb von vendor/assets/ noch weiter in verschiedene Typen von Assets unterschieden wird, z.B. javascript/, stylesheets/, macht es keinen Sinn bower_components noch tiefer einzubinden. Grund dafür ist, dass Bower keine Unterscheidung zwischen Asset Typen macht, sondern Paket-agnostisch ist – in jedem Paket können also beliebige Asset Typen enthalten sein.

Damit die Rails Asset-Pipeline nun die unter vendor/assets/bower_components/ installierten Pakete einlesen und über eine require Direktive verfügbar machen kann, muss der neue Pfad noch in der Railsanwendung konfiguriert werden. In der Datei config/initializers/assets.rb muss dazu noch die folgende Zeile hinzugefügt werden:

#Add additional assets to the asset load path
Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'bower_components')

Im Anschluß können dann alle über Bower installierten Pakete in den Index Dateien application.js oder application.css wie folgt referenziert werden:

//= require jquery
//= require jquery_ujs
//= require bower-package-name/path/to/main_file

Vorteile und Nachteile

Der oben beschriebene Ansatz hat den Charme, ohne zusätzliche Indirektionen auszukommen: Bower kann so benutzt werden, wie es gedacht ist. Die Konfiguration ist dazu verhältnismäßig simpel und schnell erledigt. Des Weiteren ist es möglich, die benötigten Pakete unter vendor/assets/bower_components/ nach der Installation in die Versionsverwaltung der Wahl einzuchecken und somit einzufrieren. Warum letzteres ein Vorteil sein kann, zeigt dieser Artikel im Detail.

Auf der anderen Seite ist es zwingend erforderlich, dass neben der Ruby-Umgebung auch noch eine node-Umgebung vorhanden sein muss, damit sich Rails und Bower im Zusammenspiel verwenden lassen. Das mag auf dem eigenen Entwicklungsrechner vielleicht nicht so sonderlich tragisch sein, aber spätestens, wenn die Webanwendung auf einem Continious-Integration Server gebaut, getestet und automatisiert in Produktion deployt wird, ist jede zusätzliche Abhängigkeit, die auch auf dem CI-Server installiert werden muss, tendenziell unerwünscht.

Ein weiteres Problem kann entstehen wenn Bower Pakete intern relative Pfade verwenden, um andere Assets zu referenzieren. Gerade im CSS Code kommt es des Öfteren mal vor, dass Hintergrundgrafiken oder Webfonts über relative Pfadangaben eingebunden werden. Diese stimmen aber spätestens dann nicht mehr überein, wenn die Assets für die Produktion vorkompiliert werden und zu Caching-Zwecken Fingerprints im Dateinamen enthalten.

gem bower-rails

Eine weitere Möglichkeit Bower und Rails zu verheiraten besteht über das bower-rails Gem. Dieses legt sich als Wrapper um Bower und lässt sich aus der Railsanwendung heraus wesentlich idiomatischer verwenden. Es bietet eine an Bundler angelehnte DSL, um Abhängigkeiten zu definieren und stellt eine Reihe von Rake Tasks zur Verfügung, mit denen sich einige Bower Befehle aufrufen lassen. Um das Gem zu verwenden, genügt es im Gemfile der Railsanwendung die Zeile

gem "bower-rails", "~> 0.9.1"

hinzuzufügen und bundle install auszuführen. Falls noch keine bower.json Datei in Wurzelverzeichnis der Anwendung liegt, kann diese mit dem passenden Generator

rails g bower_rails:initialize json

generiert werden oder wahlweise auch händisch angelegt werden. Alternativ kann auch ein sogenanntes Bowerfile generiert werden, in dem eine aus dem Gemfile bekannte DSL verwendet werden kann, um die von der Anwendung benötigten Pakete zu definieren. Generiert wird ein solches Bowerfile mit demselben Generator, allerdings ohne den letzten Parameter json.

rails g bower_rails:initialize

Aussehen tut ein solches Bowerfile dann beispielsweise so:

#Puts to ./vendor/assets/bower_components
asset "backbone"
asset "moment", "2.0.0" # get exactly version 2.0.0
asset "secret_styles", "git@github.com:initech/secret_styles" # get from a git repo

#get from a git repo using the tag 1.0.0
asset "secret_logic", "1.0.0", git: "git@github.com:initech/secret_logic"

#get from a github repo
asset "secret_logic", "1.0.0", github: "initech/secret_logic"

#get a specific revision from a git endpoint
asset "secret_logic", github: "initech/secret_logic", ref: '0adff'

Per Default installiert bower-rails darin definierte Pakete nach vendor/assets/bower_components. Dies lässt sich mit Gruppen innerhalb des Bowerfile aber auch anderweitig konfigurieren. So können mit dem folgenden Bowerfile bestimmte Pakete nach vendor/assets/ und Andere nach lib/assets/ installiert werden.

#Puts files under ./vendor/assets/bower_components
group :vendor do
  asset "jquery" # Defaults to 'latest'
end

#Puts files under ./lib/assets/bower_components
group :lib do
  asset "backbone", "1.1.1"
end

Über einen ähnlichen Mechanismus lassen sich darüber hinaus auch Entwickungsabhänigkeiten in einem separaten Block definieren:

asset "backbone", "1.1.1"

#Adds jasmine-sinon and jasmine-matchers to devDependencies
dependency_group :dev_dependencies  do
  asset "jasmine-sinon"            # Defaults to 'latest'
  asset "jasmine-matchers"         # Defaults to 'latest'
end

#Explicit dependency group notation ( not neccessary )
dependency_group :dependencies  do
  asset "emberjs"                  # Defaults to 'latest'
end

Damit Bower mit einem derartigen Bowerfile aufgerufen werden kann, müssen die vom Gem bereit gestellten Rake Tasks verwendet werden.

rake bower:install            # to install packages
rake bower:install:deployment # to install packages from bower.json
rake bower:update             # to update packages
rake bower:update:prune       # to update components and uninstall extraneous packages
rake bower:list               # to list all packages
rake bower:clean              # to remove all files not listed as main files (if specified)
rake bower:resolve            # to resolve relative asset paths in components
rake bower:cache:clean        # to clear the bower cache

Neben den Möglichkeiten, Bower über das Bowerfile zu konfigurieren, besteht zusätzlich auch noch die Möglichkeit, weitere Optionen in einer .bowerrc Datei festzulegen. Ferner lassen sich in der generierten Datei config/initializers/bower_rails.rb noch einige Stellschrauben bezüglich des Verhaltens beim Präkompilieren nachjustieren.

Vorteile und Nachteile

Einer der größten Vorteile von bower-rails ist sicherlich die äußerst eingängige Nutzung von Bower mit den aus Rails gewohnten Konventionen und Mechanismen. Die zentrale bower.json wird optional sehr elegant in einem Bowerfile abstrahiert, dass sich ganz analog zu einem Gemfile pflegen lässt. Rake Tasks übernehmen die eigentliche Arbeit und tunneln die Aufrufe zum Tool selbst. Ein weiterer Plus-Punkt ist, dass die Möglichkeiten zur Konfiguration eigentlich keine Wünsche mehr offen lässt.

Negativ fällt auf, dass trotz aller Abstraktionen Bower wie auch node.js noch auf dem System installiert sein müssen, damit beispielsweise die Rake Tasks ausführbar sind. Somit können tendenziell auch alle Probleme auf einer CI-Umgebung auftreten, die auch schon im ersten Integrationsansatz beschrieben worden sind.

rails-assets.org

Als letztes Beispiel für die Integration von Bower in Rails sei noch der Dienst http://rails-assets.org genannt, der damit wirbt, einen verlässlichen Proxy zwischen Bundler und Bower anzubieten. Um den Dienst zu verwenden, reicht es aus, wenige Zeilen im Gemfile der Railsanwendung zu konfigurieren. Dazu wird neben https://rubygems.org eine weitere Source hinzugefügt. Ist Bundler in einer Version >= 1.7.0 vorhanden, kann sogar ein separater Source-Block verwendet werden, in dem Bower Pakete mit dem Präfix rails-assets-* wie alle anderen Gem Abhängigkeiten auch definiert werden können.

gem 'bundler', '>= 1.7.0'

source 'https://rails-assets.org' do
  gem 'rails-assets-bootstrap'
  gem 'rails-assets-angular'
  gem 'rails-assets-leaflet'
end

Eine weitere Konfiguration ist nicht notwendig. Nach einem bundle install lassen sich die Assets wunderbar über Sprockets in der application.js oder application.css einhängen.

Damit diese Magie gelingt, verpackt rails-assets unter der Haube Bower Pakete automatisiert in Gems, was in den meisten Fällen auch exzellent funktioniert. Sogar ggf. verwendete relative Pfade werden automatisch durch Rails spezifische Helper Methoden ersetzt, die dafür sorgen, dass beim Präkompilieren Referenzen nicht auf Grund des Fingerprintings unbrauchbar werden.

Die populärsten Bower Pakete sind inzwischen über rails-assets verfügbar. Unter https://rails-assets.org/components sind alle Asset Gems durchsuchbar, sollte ein Paket noch nicht konvertiert worden sein, kann dies auf einer weiteren Unterseite angestoßen werden. Das anschließende Neu-Indexieren der Registry kann aber durchaus mal ein paar Minuten auf sich warten lassen, hier sollte man also keine Wunder erwarten.

Vorteile und Nachteile

rails-assets scheint auf den ersten Blick die perfekte Lösung für eine unkomplizierte Integration von Bower Paketen zu sein. Es besteht keine Notwendigkeit, irgendwelche anderen Tools oder Laufzeitumgebungen auf dem lokalen System oder der CI-Umgebung zu installieren. Das Setup ist minimal und eine weitere Konfiguration quasi nicht notwendig. Sogar das Einfrieren und Einchecken der Gems ist mit den Bundler Boardmitteln kein Problem.

Es kann durchaus passieren, dass sich ein Paket nicht so verhält, wie man es erwarten würde. Das hängt in der Regel ganz von der Qualität der bower.json Datei eines Pakets ab, die rails-assets beim automatischen Konvertieren auswerten muss. Erfreulicherweise ist das erstaunlich selten der Fall. Ein weiterer potentieller Nachteil kann sein, dass neben https://rubygems.org nun ein zweiter Service notwendig ist, um die Anwendung zu bauen, zu testen und zu deployen. Nach den vergangenen Versuchen, RubyGems mit Schadsoftware zu kompromittieren, ist ein weiteres Scheunentor aus sicherheitstechnischen Gründen vielleicht ein weiterer Grund gegen rails-assets.

Fazit

Neben den oben beschriebenen Möglichkeiten, Bower in Rails zu integrieren, gibt es auch noch diverse andere Ansätze, die aber zum Teil eher experimenteller Natur sind und sich nicht unbedingt für den Einsatz in einem Produktivsystem eigenen. So gibt es beispielsweise mit Halfpipe ein Gem, dass die Rails eigene Asset-Pipeline komplett deaktiviert und durch einen Mix aus Grunt und Bower ersetzt. Für welche Integrations-Strategie man sich auch immer entscheidet, es gilt für jede die Vor- und Nachteile abzuwägen und eine Option zu wählen, die optimal zum umzusetzenden Projekt passt.

TAGS

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen