Anzeigen dynamischer Seiteninhalte im Zusammenspiel mit dem Zurück-Button

Christoph Heidenreich
Christoph Heidenreich Freut sich wenn MySQL zum Einsatz kommt und ist ansonsten im PHP/JavaScript Umfeld unterwegs.

Stell Dir vor, Du klickst dich durch eine Liste von Produkten in einem Online-Shop und beim Scrollen werden immer wieder mehr Artikel nachgeladen. Dies passiert ein paar Mal bis Du einen interessanten Artikel findest und auf diesen klickst. Da der Artikel nicht die gewünschte Farbe hat, gehts wieder zurück zur Liste – mit dem Zurück-Button des Browsers. Du siehst zwar die Liste, aber nicht an der Stelle, an der Du zuletzt warst. Schlimmer noch. Die nachgeladenen Artikel fehlen in der Liste und Du musst Dich wieder durch die Liste scrollen. Das kostet Zeit und bringt Frust mit sich.

Normalerweise kann sich der Browser die Stelle der Seite merken, von der aus der Besucher auf die nächste Seite navigiert ist. Sobald Inhalt nachgeladen wird (z.B. durch Ajax), kann der Browser jedoch diese Funktionalität nicht mehr bieten. Besonders bei Single Page Applications, also Seiten die den Inhalt komplett über JavaScript nachladen, ist eine fehlende Historie eine vernichtende Benutzererfahrung. Die Browser-Historie kennt nur den initialen Zustand der Seite. Der Zurück-Button würde auf die vorherige Seite verweisen. Das könnte z.B. die Trefferliste einer Suchmaschine sein oder eine leere Seite/die Startseite des Browsers, wenn die URL direkt eingegeben wurde. Wünschenswert wäre hingegen, dass der Browser den Benutzer genau an die Stelle zurück bringt, die er vor dem Klicken des Zurück-Buttons gesehen hat.

Wieso ist bei Verwendung von Ajax wieder alles anders als man es gewohnt ist?

Dadurch, dass der Seiteninhalt nachträglich geändert wird, weiß die Browser-Historie nichts von dem geänderten Inhalt. Der Browser macht hierbei alles richtig und zeigt die vorherige Seite so an, wie er sie vom Server bekommen hat. Dies kann z.B. eine Shop-Seite mit den ersten 20 Artikeln sein, obwohl der Besucher zuvor durch Scrollen und Nachladen mehr als diese 20 Artikel angezeigt bekommen hatte. Dynamischer Inhalt ist – wie der Name nahelegt – dynamisch. 

Und was kann man dagegen tun?

Dank der stetigen Weiterentwicklung von Web-Standards gibt es mit HTML5 die Möglichkeit, die Browser-Historie beliebig anzupassen und zwar mit dem "window.history"-Objekt. Dadurch lassen sich aktuelle Einträge anpassen, neue Einträge anlegen und ein Sprung auf einen bestimmten Eintrag ist ebenfalls möglich. Natürlich gibt es auch JavaScript-Frameworks, die sich dem Problem annehmen. Diese sind jedoch nicht immer einfach einzusetzen, da es hierbei sehr stark auf verschiedene Faktoren ankommt, z.B. wie genau das Nachladen des Seiteninhalts umgesetzt ist. In manchen Fällen reichen wenige Zeilen Code, um die gewünschte Funktionalität zu implementieren.

Nützliche Links zum Thema:

Browserkompatibilität

MDN WebGuide

Unser Beispiel-Projekt (Download über Github)

Welche grundlegenden Anpassungen sind notwendig?

Die folgenden Code-Beispiele beziehen sich auf unser Beispiel-Projekt, welches man unter https://github.com/punktDe/demo-browser-history.git herunterladen kann.

In unserem Beispiel haben wir eine Seite mit einem Menü, welches den Inhalt der Seite austauscht. Zusätzlich gibt es einen Link auf eine andere Seite. Ohne die Anpassungen an der Historie konnte die Seite zwar normal bedient werden, aber die Historie wurde nur beim Navigieren über Hyperlinks erweitert. Dadurch war es nicht möglich, durch die Verwendung des Zurück-Buttons den letzten Zustand der Seite zu erhalten.

Im folgenden finden sich die Anpassungen, die dieses Problem behoben haben.

Erweitern der Browser-Historie

Die Browser-Historie muss um neue Einträge über die Methode history.pushState() erweitert werden. In dieser Methode können wir als ersten Parameter dem Browser beliebige Informationen mitliefern, auf die wir dann im Zurück-Event – dem "popstate"-Event – zugreifen können. In unserem Beispiel übergeben wir den nachgeladenen HTML Code sowie die ID des aktiven Menü-Elements in einem Objekt. Zusätzlich kann man den Titel der Seite als zweiten Parameter setzen. Als dritter Parameter kann eine URL hinterlegt werden. Diese kann z.B. auch Informationen wie GET-Parameter oder einen Anchor (.../home.html#about-us) enthalten. In unserem Fall ist dies die aktuelle Seite durch "window.location", da sich die URL der Seite durch Klicken des Menüs nicht verändern wird.

$('#menu a').each(function() {
   $(this).click(function() {
      var activeLinkId = $(this).attr('id');
 
      $.ajax({
         method: "GET",
         url: $(this).data('url'),
         dataType: "HTML"
      })
      .done(function(html) {
         var dataObject = {
            html: html,
            activeLinkId: activeLinkId
         };
 
         // this will add a new entry for the browser history like it would happen if you
         // click on a hyperlink on the page with additional information we can use as needed
         history.pushState(dataObject, null, window.location);
 
         setCurrentPage(html, activeLinkId);
      });
   });
});

Da wir in dem Click-Event wissen, was geklickt wurde und welcher Inhalt nachgeladen wurde, können wir diese Informationen einfach in ein JavaScript Objekt packen und an das neu anzulegende "history" Objekt übergeben. Dies sorgt erst einmal dafür, dass der Zurück-Button nun auf der gleichen Seite bleibt. Jedoch wird weiter nichts passieren, da die URL in unserem Beispiel nicht dafür sorgt, dass sich der Inhalt ändert. Dafür benötigen wir das "popstate"-Event.

Auf den Zurück- und Vorwärts-Button reagieren

Damit nach einem Klick auf den Zurück-Button (oder auch Vorwärts-Button) auch tatsächlich die Seite entsprechend aktualisiert wird, muss ein EventListener für das "popstate"-Event registriert werden. In diesem lässt sich die Seite entsprechend der in der Historie hinterlegten Informationen anpassen. Ohne Anpassungen in dem Event wird nur die über history.pushState() hinterlegte URL aufgerufen. Sofern hier keine Parameter hinterlegt wurden, die den Zustand der Seite beeinflussen, werden die Inhalte auch nicht automatisch nachgeladen.

window.addEventListener('popstate', function(event) {
   if (event.state !== null && event.state !== undefined) {
      setCurrentPage(event.state.html, event.state.activeLinkId);
   }
});

Innerhalb des "popstate"-Events sollten keine Aufrufe von JavaScript erfolgen, die die Historie manipulieren. Ansonsten ändert sich der Historienverlauf obwohl man z.B. den Zurück-Button verwendet hat.

Aktuellen Historie-Eintrag aktualisieren

Wenn eine Seite sich permanent durch verschiedene Klicks ändert, wie z.B. bei einem Online-Shop durch das Setzen von Filtern in der Artikelliste, dann wird die Methode history.replaceState() interessant. Diese aktualisiert den aktuellen Historien-Eintrag mit den gleichen Parametern wie die history.pushState() Methode. Dadurch kann man sicherstellen, dass der letzte aktive Zustand auch korrekt in der Historie hinterlegt wurde.

Anpassungen der Historie beim Laden der Seite

Die bisherigen Anpassungen würden bei einer Single Page Application helfen, also einer Seite, die alles dynamisch nachlädt und keine normalen Hyperlinks besitzt. In unserem Beispiel haben wir deswegen noch einen Hyperlink auf eine Impress-Seite eingebaut. Denn was zeigt der Browser an, wenn zuerst das Menü ein paar mal geklickt wurde, anschließend auf die Impress-Seite über einen Hyperlink gewechselt wurde und nun der Zurück-Button geklickt wird? Richtig, es wird der normale Inhalt der Index.html angezeigt und nicht der letzte Zustand des Menüs mit geändertem HTML.

if (history.state === null || history.state === undefined) {
   // store current page state in the current history
   var dataObject = {
      html: jQuery('#replace-container').wrap('<pseudo/>').parent().html(),
      activeLinkId: ''
   };
 
   // this replaces the current browser history entry with information (html and current active menu id)
   // we use in our popstate event to handle the state of the page on history navigation
   history.replaceState(dataObject, null, window.location);
} else {
   // set the page state by given information from the history state
   setCurrentPage(history.state.html, history.state.activeLinkId);
}

Sofern die Historie für den aktuellen Eintrag keinen history.state Wert enthält, was beim Aufruf der Seite über die Adresszeile oder beim Klicken eines Hyperlinks der Fall ist, dann überschreiben wir die aktuelle Historie mit den entsprechenden Informationen. Falls uns die Informationen vorliegen, können wir davon ausgehen, dass gerade ein Browser-Button (Vor/Zurück) gedrückt wurde und tun das gleiche wie im "popstate"-Event.

Fazit

Je nach Komplexität lässt sich die Seite (lassen sich Seiten mit dynamisch nachladendem Inhalt) so anpassen, dass der Zurück-Button einen vorherigen Zustand der Seite wieder wie gewünscht herstellt. Das sorgt für weniger Frust beim Seiten-Besucher und macht die Seite insgesamt besser bedienbar. Gerade bei Single Page Applications ist das Anpassen der Historie ein großer Schritt in Richtung besserer Usability. Schließlich existiert der Zurück-Button schon länger als das Nachladen von Inhalten und sollte dementsprechend auch weiterhin das tun, was man von ihm erwartet.

Autor: Christoph Heidenreich

Kommentare

Bernd Lehmann • 01.09.2016 - 15:42

sehr anschaulich und verständlich erklärt!


Weitere Beiträge

21. Juni 2016

Agile Coach Camp 17.-19. Juni 2016 in Rückersbach

Es waren einmal zwei ScrumMasterinnen bei punkt.de, die sich nach Fortbildung und Inspiration sehnten. … mehr

13. Juni 2016

Layout-Testing: Wer findet den Fehler zuerst - Sie oder Ihre Nutzer?

Wer hat nicht schon einmal eine Webseite gesehen, deren Layout ein wenig „verrutscht“  oder „kaputt“  … mehr