#205 Unobtrusive Javascript
- Download:
- source codeProject Files in Zip (163 KB)
- mp4Full Size H.264 Video (18.8 MB)
- m4vSmaller H.264 Video (13.4 MB)
- webmFull Size VP8 Video (35 MB)
- ogvFull Size Theora Video (25.6 MB)
In unserer Serie über die neuen Features von Rails 3 betrachten wir in dieser Episode das Thema dezentes bzw. unaufdringliches JavaScript (unobtrusive Javascript). Dezentes JavaScript bedeutet, JavaScript- und HTML-Code weitestgehend zu trennen. Im ausgelieferten HTML-Dokument sollten also nach Möglichkeit keine oder so gut wie keine JavaScript-Anweisungen stehen, sondern nur in externen Dateien. Dies funktioniert analog wie die Trennung von Struktur (HTML) und Design (CSS). Bevor wir zur eigentlichen Anwendung in Rails kommen, hier ein kleines Beispiel anhand eines einfachen HTML-Dokuments.
Der nachfolgende Screenshot zeit eine Webseite mit einem Link darauf. Sobald der Link aufgerufen wird, wird ein JavaScript alert
ausgeführt, der “Hello world!” ausgibt.
Der HTML-Code für diese Seite sieht so aus::
<!DOCTYPE html> <html> <head> <title>UJS Example</title> </head> <body> <h1><a href="#" onclick="alert('Hello world!'); return false;">Click Here</a></h1> </body> </html>
In dieser Seite haben wir direkt im HTML-Dokument im onlick
-Attribut JavaScript-Code stehen. Solche Anweisungen sind also keineswegs dezent/unaufdringlich (unobtrusive eben), was nicht gerade ideal ist, da wir Verhalten und Inhalt miteinander vermischen. In den 90er Jahren wurden Webseiten sehr häufig mit <font>
-Tags gestaltet, um Schriftart und -aussehen von Texten einzustellen, da es damals noch kein CSS gab. Daraus resultierte automatisch, dass man, wenn man beispielsweise die Größe aller mit <p>
deklarierten Absätze verändern wollte, nicht selten hunderte über mehrere Dateien verteilte Änderungen durchführen musste. Als die Browser endlich anfingen, CSS zu unterstützen, konnte man das Styling der Seite in Stylesheets auslagern und nur noch die Struktur und den Inhalt der Seite im HTML belassen. So wurde es deutlich einfacher, das Aussehen einer Webseite einheitlich anzupassen.
Dasselbe gilt für JavaScript: Viele kleine Codeschnipsel innerhalb des HTML-Dokuments, meistens in Attributen wie onclick
, vermischt verschiedene Zuständigkeiten und erschwert die Wartung nicht unerheblich. Das Herauslösen von JavaScript in eigenständige Dateien, die im head
-Bereich von HTML-Dokumenten eingebunden werden, vermeidet Code-Duplikate, ermöglicht einfache Refactorings und erleichtert die Erstellung von und die Fehlersuche in komplexen Webanwendungen.
Nur wie machen wir den JavaScript-Code in unserem einfachen Beispiel unaufdringlich? Der zentrale Schritt ist das Verschieben des Codes aus dem onclick
-Attribut in eine eigene Datei und die Verwendung eines JavaScript-Frameworks, hier jQuery, um die Anweisungen mit den richtigen HTML-Elementen zu verbinden. Zeigen wir zuerst die umgewandelte Datei und beleuchten dann die durchgeführten Änderungen.
<!DOCTYPE html> <html> <head> <title>UJS Example</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript" charset="UTF-8"></script> <script type="text/javascript"charset="UTF-8"> $(function () { $('#alert').click(function () { alert('Hello, world!'); return false; }) }); </script> </head> <body> <h1><a href="#" id="alert">Click Here</a></h1> </body> </html>
Zuallererst fällt auf, dass wir die Anweisungen doch nicht in eine eigene Datei verschoben haben. Dies dient hier nur der besseren Übersicht, da sich die Änderungen so leichter darstellen lassen.
Das a
-Element wurde um sein onclick
-Attribut erleichtert und hat stattdessen ein id
-Attribut dazubekommen, damit wir dieses Element leicht vom jQuery-Code aus adressieren können. Weiterhin haben wir im head
-Bereich der Seite die jQuery-Bibliothek eingebunden und darunter die eigentlichen Anweisungen (die selbstverständlich in einer eigenen Datei stehen sollten und auch mit Hilfe eines script
-Tags eingebunden werden sollten). Das Skript beginnt mit dem Aufruf der $
-Methode von jQuery, der eine anonyme Funktion als Parameter übergeben wird. Diese Funktion wird ausgeführt, sobald das DOM der Seite geladen wird und enthält die jQuery-Anweisungen, die anhand der id
das a
-Element finden und eine Funktion an dessen click
-Event anhängen. Diese Funktion wiederum führt den eigentlichen alert
-Aufruf durch und gibt false
zurück, damit der Browser dem Link nicht folgt.
Wenn wir die Seite neu laden, verhält sie sich immer noch genau so wie zuvor: Der alert
wird angezeigt, sobald der Link angeklickt wird.
Auf den ersten Blick drängt sich der Eindruck auf, dass man hierfür unverhältnismässig viel Arbeit investieren muss, um die Trennung durchzuführen, doch dieses simple Beispiel demonstriert schlicht und ergreifend auch nicht die wirklichen Vorteile, die sich mit dezentem JavaScript ergeben. Immerhin haben wir eine Zeile JavaScript-Code genommen und sechs daraus gemacht. Wie gesagt ist dieses Beispiel auch nicht dafür geeignet, um die Vorteile aufzuzeigen, sondern dient lediglich der Demonstration, wie die Technik angewendet wird und wo sich Unterschiede ergeben. Die Vorzüge von unaufdringlichem Code werden erst wirklich deutlich, wenn eine Anwendung viel mehr und komplexeres JavaScript einsetzt, da sich Code-Duplikate viel leichter identifizieren und beheben lassen, wenn man den gesamten Code an einer Stelle hat, anstatt überall in der Anwendung nach einzelnen Fragmenten suchen zu müssen.
Ein großes Problem mit dieser Technik ist allerdings, dass der Code gewöhnlich in einer statischen JavaScript-Datei gespeichert ist. Wie kann man also dynamischen, serverseitigen Inhalt in das Skript einfügen, wo es ja nicht mehr inline im eigentlichen View-Code steht?
In HTML 5 kann man so genannte custom data attributes verwenden, um zu einem bestimmten Element gehörige Daten in der Seite speichern kann. Diese Attribute sind genau wie alle anderen HTML-Attribute, nur ihr Name beginnt immer mit data-
. Um beispielsweise die Nachricht, die angezeigt werden soll, wenn der Link angeklickt wird, von der Rails-Applikation aus dynamisch festzulegen, schreiben wir unseren Code um:
<a href="#" id="alert" data-message="Hello from UJS">Click Here</a>
In the JavaScript we can then alter the alert to show the text from our new attribute:
$(function () { $('#alert').click(function () { alert(this.getAttribute('data-message')); return false; }) });
Nach dem Neuladen der Seite sehen wir die Nachricht aus dem data-
Attribut.
Wie Rails 3 data
-Attribute verwendet
Rails 3 nutzt diese custom data attributes bei der Umsetzung von unaufdringlichem JavaScript-Code, indem Daten an JavaScript übergeben werden können. Betrachten wir nun, wie sich das in einer Rails-Anwendung einsetzen lässt. Unser Beispiel ist eine einfache eCommerce-Anwendung, die eine Liste von Produkten hat, die durchsucht werden kann. Darüber hinaus gibt es noch Links zum Bearbeiten und Löschen von Produkten. Die Löschen-Links scheinen nicht mehr richtig zu funktionieren:
Dies ist ein typisches Problem mit Rails 3 Anwendungen. Wenn Sie gerade eine neue Applikation aufsetzen oder von einer früheren Version von Rails portieren, werden Sie feststellen, dass manche Stellen der Anwendung, an denen JavaScript verwendet wird, nicht mehr funktionieren!
Der view
-Code der den “Destroy”-Link erzeugt ist ein normaler link_to_method
-Aufruf mit einer :confirm
-Option, die einen JavaScript-confirm
-Dialog anzeigt und mit einer :method
-Option, die auf :delete
gesetzt ist, damit der Request nicht als einfacher GET-Request abgesetzt wird, sondern mit dem DELETE-Verb.
<%= link_to "Destroy", @product, :confirm => "Are you sure?", :method => :delete %>
What’s interesting here is the HTML source that this code generates:
<a href="/products/8" data-confirm="Are you sure?" data-method="delete" rel="nofollow">Destroy</a>
In Rails 2 wurde durch jeden link_to
-Aufruf, der als Resultat einen Link auf einen POST-, DELETE- oder PUTRequest haben sollte, jede Menge inline-JavaScript-Code erzeugt, der den confirm
-Dialog auslöste und den entsprechenden Request durchführen sollte. Im Vergleich dazu ist der von Rails 3 ausgelieferte HTML-Code erheblich übersichtlicher und durch die Nutzung von HTML 5 lassen sich auch mehrere Attribute erzeugen, beispielsweise ein data-confirm
-Attribut, das die Bestätigungsnachricht enthält und ein weiteres data-method
, das die eigentliche Methode beinhaltet.
Der Grund, warum die Links nicht funktionieren ist, dass bisher die benötigten JavaScript-Dateien im head
-Bereich der Seite geladen werden und der Link daher einen normalen GET-Request auslöst, da kein JavaScript das click
-Event verändert hat.
Um dieses Problem zu lösen, fügt man einfach eine weitere Zeile in den head
-Bereich ein:
<%= javascript_include_tag :defaults %> <%= csrf_meta_tag %>
Die erste Zeile sollte bekannt sein. Sie veranlasst, dass die Standard-JavaScript-Dateien für Rails-Anwendungen geladen werden. Die Zweite jedoch erzeugt zwei meta
-Tags, die das authenticity-Token enthalten, die nötig sind, um DELETE-Requests durchführen zu können. Laden wir also die Seite neu und betrachten wir den ausgelieferten HTML-Code, um die Ausgaben dieser beiden Zeilen zu sehen:
<script src="/javascripts/prototype.js?1268677667" type="text/javascript"></script> <script src="/javascripts/effects.js?1268677667" type="text/javascript"></script> <script src="/javascripts/dragdrop.js?1268677667" type="text/javascript"></script> <script src="/javascripts/controls.js?1268677667" type="text/javascript"></script> <script src="/javascripts/rails.js?1268677667" type="text/javascript"></script> <script src="/javascripts/application.js?1268677667" type="text/javascript"></script> <meta name="csrf-param" content="authenticity_token"/> <meta name="csrf-token" content="9ImdFvbeW7ih9oKqBDQ3O889q/hJ1q5uajpT4DFDAoA="/>
In der Seite laden wir jetzt also alle JavaScript-Dateien, die unsere Applikation braucht und haben zusätzlich zwei meta
-Tags, die nötig sind, um cross-site request forgery zu unterbinden. Dies versichert, dass PUT- und DELETE-Requests tatsächlich vom richtigen Benutzer kommen und nicht einer anderen Seite oder von einem Hacker.
Sobald diese Vorbedingungen erfüllt sind, sollte der Link endlich wie erwartet funktionieren:
Hinzufügen von AJAX-Funktionalität zum Suchfeld
Als Nächstes verändern wir das Suchformular auf der index
-Seite so, dass es beim Abschicken einen AJAX-Aufruf verwendet anstelle des bisher verwendeten GET-Requests. Der Code für das index
-View lautet:
<% title "Products" %> <% form_tag products_path, :method => :get do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> </p> <% end %> <div id="products"> <%= render @products %> </div> <p><%= link_to "New Product", new_product_path %></p>
Das Formular, das hier verwendet wird, nutzt die Technik, die in Episode 37 vorgestellt wurde. In früheren Versionen von Rails musste man, um AJAX-Funktionalität zu erreichen, den form_tag
durch einen form_remote_tag
ersetzen. Diese Methode generiert allerdings sehr viel inline-JavaScript, was wir jetzt ja gerade vermeiden wollen.
Viele der remote
-Hilfsfunktionen ist nicht mehr verfügbar in Rails 3. Um sie wieder freizuschalten, könnte man das Prototype Legacy Helper Plugin installieren, doch wir gehen hier den neuen Rails-3-Weg.
Als Alternative zum form_remote_tag
bleiben wir beim form_tag
und fügen einen neuen Parameter :remote
hinzu.
<% form_tag products_path, :method => :get, :remote => true do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> </p> <% end %>
Dieser neue Parameter kann auch mit anderen Hilfsfunktionen wie link_to
, button_to
und form_for
verwendet werden. Laden wir die Seite neu und betrachten, wie der neue Formular-Code funktioniert:
<form action="/products" data-remote="true" method="get"> <p> <input id="search" name="search" type="text" /> <input type="submit" value="Search" /> </p> </form>
Das Formular-Element ist dasselbe wie zuvor, hat aber ein neues data-remote
-Attribut. Es gibt kein inline-JavaScript, da das neue Attribut allein schon genug ist, um mit Hilfe des Codes in rails.js einen AJAX- anstelle eines GET-Requests auszulösen.
Als Nächstes müssen wir noch den Code schreiben, der die Antwort des AJAX-Aufrufs verarbeiten kann. Die Liste mit den Produkten ist in einem div
-Container mit der ID products
, also müssen wir einfach den Inhalt dieses div
-Elements aktualisieren, um die angeforderten Suchergebnisse anzuzeigen. Das Formular wird zur index
-Aktion des ProductController
s abgeschickt, also müssen wir einfach ein neues view-Template index.js.erb erzeugen, das JavaScript-Anfragen beantworten kann.
Wir können in dieser Datei beliebigen JavaScript-Code schreiben, der ausgeführt wird, sobald er beim Browser angekommen ist. Der Code zum Aktualisieren des Inhalts des products
-Containers sieht beispielsweise so aus:
$("products").update("<%= escape_javascript(render(@products))%>");
When we reload the page and submit the form the search will be made with an AJAX call and we can see this as the page’s URL doesn’t change when the search is made.
Also kann man in Rails 3 sehr einfach unaufdringlichen JavaScript-Code für AJAX-Aufrufe schreiben, indem man den :remote
-Parameter verwendet und ein JavaScript-Template für die entsprechende Aktion festlegt.
Auswechseln des Frameworks
Zu guter letzt demonstrieren wir noch, wie man die verwendeten JavaScript-Frameworks, die in unserer Applikation verwendet werden, gegen andere austauschen kann. Unsere Applikation verwendet momentan Prototype, das standardmässig bei Rails dabei ist, doch was, wenn wir stattdessen jQuery verwenden möchten?
Zuerst müssen wir die Einbindung im Hauptlayout
<%= javascript_include_tag :defaults %>
mit der folgenden Anweisung ersetzen:
<%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js", "jquery.rails.js" %>
Die erste Datei in er Liste ist die neueste jQuery-Version frisch vom Google-Server. Das alleine genügt allerdings noch nicht, da wir ein Äquivalent zur rails.js-Datei benötigen, die wir schon zuvor verwendet haben, um das Rails-spezifische JavaScript zu verarbeiten. Auf Github gibt es das offizielle jquery-ujs Projekt. Es beinhaltet eine rails.js-Datei, die heruntergeladen werden und in unseren Projekten verwendet werden kann. Wir haben die Datei heruntergeladen und im Ordner /public/javascripts abgelegt und es in jquery.rails.js umbenannt.
Jetzt müssen wir noch sämtliche Skripte, die in unserer Anwendung vorkommen, von Prototype auf jQuery umschreiben, da sich die Syntax der beiden Frameworks unterscheidet. Dies bringt also ein paar Änderungen in index.js.erb mit sich, beispielsweise #products
als Selektor für das div
-Element anstelle von products
oder das Austauschen der update
-Methode von Prototype durch die jQuery-Methode html
.
$("#products").html("<%= escape_javascript(render(@products))%>");
Unsere Anwendung wird sich jetz exakt so verhalten wie zuvor, nur dass unter der Motorhaube jQuery anstatt Prototype verwendet wird.
Stilles Zurückfallen auf nicht-JavaScript-Funktionalität
Wenn ein Benutzer unsere Anwendung verwenden möchte, dessen Browser kein JavaScript aktiviert hat, wird sich das Suchformular wie gehabt verhalten und beim Abschicken einen normalen GET-Aufruf ausführen. Löschen von Produkten wird jedoch nicht funktionieren. Dies ist ein geläufiges Problem, da Rails JavaScript nutzt, um DELETE oder PUT zu simulieren, weil HTML-Links keine anderen als GET-Requests auslösen können. Eine mögliche Lösung ist, den Link mit einem Button zu ersetzen, indem man button_to
verwendet. Dummerweise können Buttons nur schwer in das Layout der Seite eingefügt werden, weswegen in den meisten Fällen nur ein Link in Frage kommt. Eine weitere Technik wurde bereits in Episode 77 gezeigt, wobei eine eigene Bestätigungs-Seite gezeigt wird, bevor die eigentliche Löschung vorgenommen wird.