HTML5: History API

written by devangelist on November 27, 2012 in API and ASP.NET MVC and Browser and HTML5 and Javascript and jQuery with 4 comments

Browser öffnen, eintippen und los geht’s: Das wohl am meisten genützte Werkzeug im Browser, welches anfänglich nur für URLs gedacht war und mittlerweile zum echten Multifunktionswerkzeug geworden ist. Kaum jemand nimmt sich die Mühe eine längere URL einzutippen. Erscheint die gewünschte Seite nach ein, zwei Anschlägen nicht sofort in den Vorschlägen wird eifrig auf die Enter-Taste gedrückt und eines der ersten, von den Suchmaschinen angezeigten Ergebnissen angeklickt. Ist das erzielte Ergebnis nicht das richtige – kein Problem, es gibt den allgegenwärtigen zurück-Button.

Der logische Ablauf der zurück-Buttons ist relativ einfach: Es wird die zuvor geladene Seite wiederhergestellt. Anders ausgedrückt: Es wird die URL der vorherigen Seite geladen. Sind URLs aber heutzutage nicht überflüssig? Eine moderne Webanwendung braucht doch nur eine URL: Die Domain selbst, sprich eine Startseite. Der Rest wird über Javascript nachgeladen. Diese Singlepage-Applications haben somit den Vorteil, nur wirklich benötigte Daten laden zu können und den ganzen Overhead, der beim kompletten Neu laden der Seiten entsteht, hinter sich zu lassen. Aber selbst die durchdachtesten Anwendungen benötigen einer “zurück” Funktion und die meisten Benutzer kennen nur die aus dem Browser. Was genau soll aber der Browser wiederherstellen, wenn es sich um eine einzelne URL handelt?

Diese Frage versucht eine mittlerweile recht weit verbreitete Technik zu lösen:

Die Hash-bang URLs

Diese URLs beinhalten eine Raute und ein Ausrufezeichen an erster Stelle: “/#!/meine-seite.html”. Dadurch wird die Seite im Browser nicht neu geladen  sondern als Anker interpretiert. Clientseitig, also per Javascript kann ohne Probleme darauf reagiert werden und somit das neu laden verhindert und nur den gewünschten Teil der Seite ausgetauscht werden. Zudem wird die URL-Leiste im Browser ebenfalls aktualisiert und kann somit ohne weiteres kopiert und geshared werden. Twitter ist ein weit verbreitetes Beispiel für diese Art von URLs:

www.twitter.com/robertobez
www.twitter.com/#!/robertobez/

Diese Technik funktioniert natürlich in jedem Browser, hat aber einen großen Nachteil:

Während Javascript einen Hash erkennen kann, wird dies vom Server nicht erkannt. Im obigen Beispiel würde der Server nur die URL www.twitter.com/ erkennen.

Kein Problem für Benutzer mit aktiviertem Javascript, nicht so lustig werden es Suchmaschinen finden, welche dauerhaft dieselben Seiten bekämen. Außerdem ist es ein Hack, also eine Zwischenlösung welche alles andere als schön ist.

Browser History

Eines der Neuerungen in HTML5 ist die History API. In den älteren Versionen gab es bereits einige relativ nutzlose Befehle wie: history.back(), history.forward() und history.go(-2); Nun bekommen diese aber eine ganz neue Bedeutung, denn der Status der Seite kann im History Log gespeichert und darauf reagiert werden, wenn der zurück-Knopf angeklickt wird.

Das globale history Objekt wurde mit folgender Methode erweitert:

history.pushState(data, title, url);

Mit diesen drei Parametern kann der Status der Seite komplett wiederhergestellt werden:

  • data: Kann eine JSON-Struktur sein, welche beim popstate-Eventhandler zurückgegeben wird (dazu gleich mehr).
  • title: Dieser Parameter wird zurzeit von den meisten Browsern ignoriert und sollte besser als zusätzliche Eigenschaft im data-Parameter gesetzt werden, um beim popstate-Event den Titel der Seite manuell zu setzen.
  • url: Die URL, welche auch im Browser erscheint.

Ein einfaches Beispiel welches nahezu jede Anwendung implementiert ist ein paginated-Grid, also ein Grid mit mehreren Seiten, welche über Ajax nachgeladen werden. Ein zugegeben einfaches Spiel mit jQuery und ASP.NET MVC:

Model:

public class ItemsViewModel
{
    public IList<Item> Items { get; set; }
    public int PageCount { get; set; }
}

public class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
}

HTML, View:

<div id="gridContainer">
    <table id="grid">
         @foreach(var item in Model.Items)
         {
             <tr>
                  <td>@item.Id</td>
                  <td>@item.Name</td>
             </tr>
         }
     </table>
</div>
<div id="pager">
    @for(var i = 0; i < Model.PageCount; i++)
    {
        <a href="/page/@i">Seite @(i + 1)</a>
    }
</div>

Javascript, zum Beispiel im document load event:

$("#pager a").bind("click", function (event) {
    $("#gridContainer").load($(this).attr("href") + " #grid");
    return false;
});

Don’t try this at home: Die load Funktion bei jQuery ist wirklich sehr verlockend: Sie macht einen get-Request auf die URL und filtert dessen Rückgabe auf einen jQuery-Selektor (in dem Fall “#grid”). Dies kann unter Umständen bei Seiten mit mehr Inhalt zu Problemen führen, denn geladen wird der gesamte HTML-Text. Eleganter wäre eine Rückgabe über JSON bzw. der Einsatz einer Template-Engine für Javascript.
Wenn also auf eine andere Seite gewechselt wird, soll die Rückgabe im Log gespeichert werden:

$("#pager a").bind("click", function (event) {
    paginateGrid($(this).attr("href"));
    return event.preventDefault();
});

var grid = $("#gridContainer");
function paginateGrid(url) {
    grid.load(url + " #grid", function () {
        history.pushState({ content: grid.html() }, null, url);
    });
}

// Elegantere Variante:
function paginateGridPretty(url) {
    var page = url.split("/").pop();
    $.get("/Entries/?page=" + page, function(entries) {
        // ...
        history.pushState({entries: entries}, null, url);
    });
}

Nun befindet sich der HTML-Text des Grids im Browser-Histroy-Stack und kann genauso einfach wieder zurückgeholt werden:

window.addEventListener('popstate', function (event) {
    if (event.state) {
        grid.html(event.state.content);
    }
});

Wenn der zurück-Button angeklickt wird, wird das popstate-Event abgefeuert, in welchem der vorherige Status wiederhergestellt werden kann. Der Besucher sollte, bis auf die Geschwindigkeit, keinen Unterschied zum normalen Zurück merken.

Last but not least: Da das Grid anfänglich bereits serverseitig gerendert wird, muss beim ersten Laden der Seite noch der Stack aktualisiert werden:

history.replaceState({
    content: grid.html()
}, document.title, document.location.href);

Im Unterschied zu pushState wird bei replaceState kein neuer Eintrag hinzugefügt, sondern der aktuelle Status aktualisiert.
Neben den vor- und zurück-Buttons im Browser kann die History API auch direkt mit den folgenden Befehlen angesprochen werden:

history.back(); // Eine Seite zurück.
history.forward(); // Eine Seite weiter.
history.go(-1); // Negative Zahl: zurück, Positive Zahl: vor.
history.length; // Länge des History Logs.

Browser Kompatibilität:

Aktuelle Versionen von Chrome (5+), Safari (5.0+), Opera (11.50+), Firefox (4.0+) sowie der Internet Explorer 10 unterstützen die History API. Für ältere Versionen gibt es ebenfalls Frameworks, welche auf Fallbacks zurückgreifen. (Zum Beispiel History.js)
Es gibt bereits Beispiele für Anwendungen, welche die History-API nützen: Zum Beispiel GitHub und wenn es denn vom Browser unterstützt wird, setzt Twitter mittlerweile auch auf die moderne API anstelle der Hashbang-URLs.

HTML5: History API: 1 Stern2 Sterne3 Sterne4 Sterne5 Sterne 4,67 von 5 Punkte, 6 abgegebene Stimmen.
Loading ... Loading ...

About the Author

Roberto Bez ist passionierter Webentwickler und TechLead bei der HolidayCheck AG. Für Roberto bedeutet das Entwickeln nicht nur Arbeit, sondern auch Freude, Motivation und täglich neue, aufregende Herausforderungen. Besonders gerne setzt er sich mit neuen Webtechnologien sowie Datenbanken aller Art auseinander und versucht diese in die tägliche Anwendungsentwicklung miteinzubringen. Neben dem Entwickeln trifft man ihn gerne Abends beim Laufen oder im Sommer bei Mountainbike-Touren durch die schönen Berge Südtirols.