Single Page Applications – Teil 4: Ein Code-Beispiel

25. April 2013

Nachdem in den ersten drei Teilen dieser Artikelserie das Thema Single Page Applications aus verschiedenen Blickwinkeln betrachtet wurde, folgt nun ein praktisches Beispiel: Der Implementierung eines einfachen, aber effektiven Routing-Mechanismus mit jQuery und ein wenig ASP.NET WebForms.

Routing und das Bewahren des Back-Buttons

Ein naheliegendes Problem bei einer Single Page Application, die ja nur aus einer einzigen Seite besteht, ist die Unmöglichkeit, mit dem Back-Button des Browsers zu den zuvor besuchten Unterseiten zurück zu navigieren. Nun gibt es zwar auch viele ASP.NET-WebForms-Anwendungen, auf die diese Aussage zutrifft, doch das hat andere (manchmal durchaus gute) Gründe, auf die an dieser Stelle nicht näher eingegangen werden soll. Bei Single Page Applications ist dieses Problem recht einfach lösbar, indem man logische Pfade (Routes) definiert, wie man sie z. B. von ASP.NET MVC kennt. Anders als bei ASP.NET MVC wird das Routing aber im Wesentlichen nicht auf Server- sondern auf Client-Seite durchgeführt. Mit etwas mehr Aufwand kann man das Routing wie bei ASP.NET MVC für den Kontrollfluss innerhalb der Single Page Application lösen. Die folgende URL könnte z. B. dazu dienen, den Kunden-Datensatz mit der ID 65 im Editiermodus aufzurufen: http://localhost/customer/edit/65

Solche URLs bieten aber nur dann einen echten Mehrwert, wenn man sie an andere Personen weiterschicken oder als Bookmark speichern kann. Das geht bei einer Single Page Application nicht einfach so, weil vom Web-Server erst die URL aufgelöst und dann der JavaScript-Code ausgeführt wird. Deshalb würde der Aufruf dieser URL erst einmal zu einem Fehler führen, da der Web-Server keine entsprechende Seite finden würde. Man muss dem Web-Server also mitteilen, wie er die Route auflösen soll, was bei ASP.NET mit einer Code-Zeile erledigt ist. Allerdings muss man dann auch im JavaScript-Code beim ersten Aufruf der Seite ein entsprechendes logisches Routing durchführen. Alles in allem lässt sich sagen, dass Routing mit einem gewissen Programmieraufwand verbunden ist, der sich bei Single Page Applications aber meistens lohnt. Bei Internet-Seiten ist das Routing zudem für die Suchmaschinenoptimierung wichtig.

Ein praktisches Beispiel

Web-Anwendungen haben oft eine Vielzahl an Unterseiten, zwischen denen man über Links oder Buttons hin- und her navigieren kann. In den meisten Fällen gibt es keinen Grund, warum eine Single Page Application mit diesem den Benutzern vertrautem Prinzip brechen sollte. Statt einzelner Seiten kann man DIVs verwenden, die, je nach Zustand der Anwendung, ein- und ausgeblendet werden. Im HTML-Code könnte das so aussehen:

   1: <div id="ContentWrapper">

   2:     <ul>

   3:         <li><a class="navLink" href="Welcome">Willkommen</a></li>    

   4:         <li><a class="navLink" href="Customer">Kunden</a></li>

   5:         <li><a class="navLink" href="Product">Produkte</a></li>

   6:     </ul>

   7:     <hr />

   8:     <div class="section" id="WelcomeSection">

   9:         <p>Sektion: Willkommen</p>   

  10:     </div>

  11:     <div class="section" id="CustomerSection">

  12:         <p>Sektion: Kunden</p>   

  13:     </div>

  14:     <div class="section" id="ProductSection">

  15:         <p>Sektion: Produkte</p>   

  16:     </div>

  17: </div>

Oben auf der Seite befindet sich also eine Liste von Links (die man mit CSS noch nebeneinander anordnen könnte), gefolgt von einer Liste von Sektionen. Wenn man auf einen Link klickt, soll die dazugehörige Sektion (und nur diese) eingeblendet werden. Im nachfolgenden JavaScript-Teil wird als einziges Hilfsmittel jQuery verwendet werden. Hier und da könnte man sich das Leben mit der einen oder anderen JavaScript-Bibliothek sicher noch einfacher machen, aber die Anwendung ist auch so schon sehr überschaubar.

Im Ready()-Event der Webseite, das ausgelöst wird, wenn diese vollständig geladen ist, wird deren Inhalt nach Sektionen durchsucht, die eine IDs haben. Aus diesen IDs wird, ohne das Suffix “Section”, ein Array gebildet, das einer zuvor (und der Einfachheit halber global) deklarierten Variable zugewiesen wird:

   1: knownSections = $('#ContentWrapper .section[id]').map(function () {

   2:     return this.id.replace(/Section/, '');

   3: }).get();

Dieses Array wird innerhalb der Navigationsfunktion genutzt. Diese erhält als Parameter eine URL, deren letztes Element separiert und ausgewertet wird. Das wäre bei http://localhost/Product “Product”. Nun wird ermittelt, ob das Array mit den bekannten Sektionen das Element enthält, was bei “Product” zuträfe. Andernfalls, also z. B. bei URLs wie http://localhost/ oder http://localhost/Test123, würde im Else-Zweig die erste Sektion genommen werden, womit sichergestellt ist, dass sich die Anwendung zu jedem Zeitpunkt in einem definierten Zustand befindet. Mit der HTML5-Funktion window.history.pushState() kann die URL-Zeile des Browsers gesetzt und die entsprechende URL im Browser-Verlauf abgelegt werden. Der restliche Code dient dazu, die anderen Sektionen auszublenden und die ausgewählte Sektion einzublenden.

   1: function showSectionByUrl(incomingUrl) {

   2:     var lastUrlPart = incomingUrl.substr(incomingUrl.lastIndexOf('/') + 1);

   3:     var visibleSection;

   4:  

   5:     if ($.inArray(lastUrlPart, knownSections) > -1) {

   6:         visibleSection = '#' + lastUrlPart + 'Section';

   7:  

   8:     } else {

   9:         window.history.pushState('', '', knownSections[0]);

  10:         visibleSection = '#' + knownSections[0] + 'Section';

  11:     }

  12:     

  13:     $('.section:not(' + visibleSection + ')').css('display', 'none');

  14:     $(visibleSection).css('display', 'block');

  15: }

Nun müssen noch die für die Navigation vorgesehenen Links mit einer Funktion versehen werden. Im ersten Schritt wird dazu das Default-Verhalten der Links verhindert, sodass sie keine Aktion auf dem Server auslösen. Dann wird, sofern die URL von der aktuellen URL abweicht, die weiter oben definierte Navigationsfunktion aufgerufen. Auch diesmal wird window.html.pushState() aufgerufen, um die URL in den Browser-Verlauf einzufügen.

   1: $('.navLink').click(function (e) {

   2:     e.preventDefault();

   3:     var targetUrl = e.target.href;

   4:     

   5:     if (targetUrl != location.href) {

   6:         window.history.pushState(null, null, targetUrl);

   7:         showSectionByUrl(targetUrl);

   8:     }

   9: });

Die Verwendung von window.history.pushState() setzt einen HTML5-fähigen Browse wie Internet Explorer 10 voraus. Wenn man Benutzern, die die Anwendung mit einem älteren Browser aufrufen, hierüber informieren möchte, kann man mit der folgenden Hilfsfunktion ermitteln, ob der verwendete Browser das HTML5-Routing mit window.history.pushState() unterstützt:

   1: function getIsHtml5HistoryApiAvailable() {

   2:     return window.history && ('pushState' in window.history);

   3: }

Damit die Anwendung später auch auf den Back-Button reagiert, muss noch das "Popstate”-Event mit einem Event-Handler versehen werden, der wiederum die Navigationsfunktion aufruft:

   1: $(window).bind('popstate', function () {

   2:     showSectionByUrl(location.href);

   3: });

Zu guter Letzt muss man dafür sorgen, dass der Web-Server auf Urls wie bei http://localhost/Product nicht vergeblich nach einer Webseite mit diesem Namen sucht, sondern dem Anwender die gewünschte Sektion zeigt, sodass dieser die URL wie gewohnt mit Copy & Paste verwenden kann. Hierzu erweitert man die Application_Start()-Funktion in der Global.asax-Datei um ein entsprechendes Routing, das wie folgt aussehen könnte:

   1: private void Application_Start(object sender, EventArgs e)

   2: {

   3:     RouteTable.Routes.MapPageRoute("spa-browse", "{section}", "~/Default.aspx");

   4: }

Damit wäre die kleine Beispielanwendung im Prinzip fertig. Jetzt müsste man die Sektionen natürlich noch mit Inhalten füllen, beispielsweise mit Tabellen, an die man dann via Knockout.js passende Datensätze bindet. Spätestens dann wird man noch etwas mehr serverseitigen Code benötigen, um die Daten per JSON-Call bereitstellen und abspeichern zu können. Da dies aber auch in ASP.NET WebForms sehr einfach realisiert werden kann, ist der Einstieg in die Welt der Single Page Applications gar nicht so schwer. Daher soll an alle Entwickler, die bis zu diesem Teil der Serie durchgehalten haben, der Aufruf ergehen, selbst eine Single Page Application zu schreiben und damit herumzuexperimentieren!