Ostatnimi czasy w świecie front-endu dzieje się naprawdę sporo! Przeglądarki coraz śmielej implementują HTML5/CSS3, użytkownicy coraz szybciej aktualizują przeglądarki, powstają nowe frameworki, narzędzia do programowania 2D czy nawet 3D w JavaScript. Wchodzimy w erę mobilności, zwiększamy interakcję w aplikacjach, pojawiają się nowe typy aplikacji, a wraz z nimi nowe problemy.
Ten wpis jest poświęcony jednemu z takich tematów – manipulacji historią przeglądarki w aplikacjach stricte JavaScriptowych!
Historia w JavaScript
Dotychczasowy obiekt window.history w JavaScript był dość ubogi. Mieliśmy bowiem do dyspozycji kilka biednych metod, tj. window.history.back(), window.history.forward(), window.history.go(1) czy właściwość window.history.length. Szaleństwo!
Jednak wraz z nadejściem HTML5 do przeglądarek wdrożono API pushState. Daje to nam możliwość manipulacji historią przeglądarki bez potrzeby przeładowania całej strony. Jednocześnie wprowadza sporą nowość w dotychczasowej architekturze aplikacji, bowiem możemy jeszcze łatwiej budować aplikacje na wzór desktopowych!
Praktyczny przykład
Dotychczas pisząc aplikacje pomijające synchroniczność protokołu HTTP, czyli umożliwiające poruszanie się po stronie bez przeładowywania całej strony wykorzystywaliśmy hashe w adresie URL.
Dzięki temu mogliśmy identyfikować na której podstronie znajduje się użytkownik i jaką treść mu zaserwować. Technikę tą do dziś wykorzystuje masa serwisów, np. twitter.com. Tak wygląda przykład z życia wzięty:
- http://twitter.com/#!/messages
- http://twitter.com/#!/who_to_follow
Linki takie nie są niestety przyjemne w odczycie, użyteczne, czy też zoptymalizowane pod wyszukiwarki :-) Niestety był to jedyny sposób na radzenie sobie z tego rodzaju problemem.
Na szczęście API pushState pozwalać będzie na wprowadzenie następujących adresów URL (na przykładzie Twittera), pozostawiając możliwość asynchronicznego wczytywania treści:
- http://twitter.com/messages
- http://twitter.com/who_to_follow
Linki takie wyglądają naturalnie, a przede wszystkim nie trzeba walczyć z obsługą historii przeglądarki za pomocą hashy (a było z tym sporo zabawy).
Jak to wygląda w praktyce? Bardzo prosto. Wystarczy do linku kierującego do podstrony ‚Messages’ dodać taką akcję (z wykorzystaniem jQuery):
1 2 3 4 5 6 7 8 9 | $('#messages').click(function(e) { e.preventDefault(); window.history.pushState( { page: 'messages' }, 'Messages', '/messages' ); }); |
Kod taki spowoduje załadowanie do paska adresu (i historii przeglądarki) adresu: /messages (bez sprawdzania czy istnieje taki adres/plik). Musimy jeszcze zaserwować odpowiednią treść do wybranej podstrony i po sprawie – obyło się bez przeładowania całej strony.
Przyjmijmy następnie, że użytkownik opuszcza stronę. Następnie chce powrócić do naszej strony, klika Backspace i znów znajduje się pod adresem /messages. Strona w tym momencie otrzyma zdarzenie popstate zwracające stan obiektu historii (czyli informację, która podstrona/treść powinna zostać załadowana).
history.pushState
Czas na krótkie opisanie metody pushState i argumentów, jakie może przyjmować (a jak już pewnie zauważyłeś, może przyjmować 3 argumenty):
- Stan obiektu – obiekt stanu, który jest zwracany przy zdarzeniu popstate celem identyfikacji aktualnej strony. W pole to możemy wpisać cokolwiek, co później może być przekazane do JSON.stringify. Może to być więc obiekt JSON czy np. string, ważne jest natomiast by nie przekroczyć 640 KB, inaczej zostanie rzucony wyjątek.
- Tytuł – nazwa strony, którą chcemy wczytać do historii przeglądarki. Tytuł ten będzie widoczny w przeglądzie historii przeglądarki.
- Adres URL – w pole to wpisujemy adres URL, który ma pojawić się w pasku adresu przeglądarki. Ważna uwaga: przeglądarka nie będzie sprawdzała czy wczytywany adres faktycznie istnieje – po prostu załaduje go do historii przeglądania. Jednak zamykając przeglądarkę i później wracając do przeglądania, otwierając nieistniejący adres (załadowany przez pushState), pojawi się błąd 404.
history.replaceState
Kolejną nową metodą history jest replaceState. Działanie replaceState jest analogiczne do pushState (przyjmuje takie same parametry), a jedyna różnica polega na tym, iż nie powstaje nowy wpis w historii przeglądarki, a zastępowany jest aktualny!
Krótki przykład działania w ramach objaśnienia:
- Jesteśmy na stronie /index.html i klikamy w link ‚Kontakt’.
- Przechodzimy na /kontakt.html z użyciem pushState. Klikamy ‚Download’.
- Przechodzimy do strony /download.html, lecz tym razem wykorzystaliśmy do przejścia metodę replaceState.
- Klikamy Backspace i wracamy do /index.html.
window.onpopstate
Zdarzenie to pozwala informować stronę o akcji wykonanej przez użytkownika (np. powrót do poprzedniego adresu URL z historii przeglądarki). Tak wygląda przykładowa obsługa zdarzenia:
1 2 3 | window.onpopstate = function(event) { alert("location: " + document.location + ", state: " + JSON.stringify(event.state)); }; |
Przy obsłudze tej metody mamy do dyspozycji jeden parametr – event. Informuje on o zdarzeniu (jakie wykonał użytkownik) przechowywanym w polu state.
Jak już wcześniej wspominałem, pole to zawiera kopię stanu obiektu, które musi być możliwe do oczytania przez JSON.stringify.
Stan obiektu historii możemy także odczytać w każdym momencie przez history.state.
window.onhashchange
Kolejnym zdarzeniem wartym wspomnienia przy okazji omawiania historii w JavaScript jest zdarzenie hashchange.
Zdarzenie to jest wywoływane w momencie zmiany hasha w adresie URL, czyli byłoby użyteczne w aplikacji opartej na hashach (jak Twitter).
Warto tutaj dodać, iż zdarzenie to nie będzie wywoływane w przypadku zmiany stanu historii (history.pushState / history.replaceState).
Korzyści history.pushState
Wraz z wprowadzeniem API pushState i zaimplementowaniem go przez przeglądarki przed developerami pojawiają się ogromne możliwości.
Możemy bowiem tworzyć jeszcze szybsze i prostsze aplikacje, nowocześniejsze, przyjemniejsze w użyciu i zgodne z ogólnie przyjętymi standardami (mowa o SEO, usability i innych).
Nie musimy już przy każdej interakcji użytkownika przeładowywać całej strony ani nie musimy samodzielnie walczyć z historią przeglądarki. To duże ułatwienie.
Wady history.pushState
Niestety nowe możliwości = nowe problemy. I tutaj pojawia się kilka spraw, nad którymi powinniśmy pomyśleć przed wdrożeniem history.pushState:
- Co ze starszymi przeglądarkami? Obecnie API pushState jest obsługiwane przez najnowsze przeglądarki, tj. Chrome 8,9,10, Firefox 4, Safari 5, Safari iOS 4.3.
- Tuszowanie ataków XSS – co nieco w temacie pisał Krzysztof Kotowicz: XSS track got ninja stealth skills thanks to HTML5.
- Pojawią się aplikacje zaśmiecające historię przeglądarki. Ciekawy przykład: RollState Demo :) Co nie znaczy, że wcześniej takich nie było: URL Hunter! :-)
Bonus
Na sam koniec chciałbym przedstawić świetną bibliotekę bardzo, ale to bardzo ułatwiającą sprawę z przestarzałymi przeglądarkami. Mowa o History.js, który korzysta z nowych możliwości HTML5, jak i zapewnia wsparcie starociom!
Czyli, jeśli przeglądarka obsługuje API pushState to nasze adresy będą wyglądały następująco:
- www.mysite.com
- www.mysite.com/?state=1
- www.mysite.com/?state=2
A jeśli nie obsługuje to:
- www.mysite.com
- www.mysite.com/#?state=1&_suid=1
- www.mysite.com/#?state=2&_suid=2
Czyli nie musimy się niczym martwić, bo brudna robota zostaje wykonana za nas. Biblioteka ta jest także przepisana jako plugin dla jQuery (i innych frameworków).
Warto dodać, że biblioteka wspiera nawet takie starocie jak IE6 i posiada kilka dodatkowych możliwości, o czym można poczytać na GitHubie.
Słowem zakończenia
Jak najbardziej zachęcam do korzystania z historii w JavaScript z opisanymi tutaj nowościami, już dzisiaj!
Nie jesteś przekonany, czy warto już rzucać się na taką nowinkę? Potrzebujesz przykładu popularnego serwisu, który już to u siebie wdrożył? Mówisz masz – google.com, github.com :-)
zauwazyles jak czesto konczysz akapit znakiem wykrzyknika? ;)
hehe, zauważyłem – chyba ze mną jest coś nie tak, skoro tak bardzo tym wszystkim się ekscytuję i fascynuję – za dużo emocji wyrzucam w swoich wpisach :D
Jak najbardziej zachęcam do korzystania z historii w JavaScript z opisanymi tutaj nowościami, już dzisiaj!
„A gdy złożysz zamówienie zaraz po programie, dostaniesz ten oto zestaw noży gratis!”
Normalnie telezakupy :)
Hehehehe. Właśnie pracuję nad serwisem wykorzystującym niniejszy fjuczer, stworzę, podeślę link i zobaczycie, że to jest bardzo fajne i nie można przechodzić obok obojętnie! :D
Szacunek, na prawdę szacunek za bloga :) Konkretne treści – widać ile poświęcenia wymagają!
Marcin, dzięki za miłe słowa – to naprawdę motywuje do dalszego działania! Jednak tutaj jest więcej przyjemności niż faktycznej pracy. Poza tym blog ten daje mi motywację do nauki nowych technologii i poznawania kolejnych rzeczy, więc w sumie same plusy ;)
Tak, przyjemność w prowadzeniu bloga i dzieleniu się ciekawostkami / wiedzą / spostrzeżeniami jest ogromna, ale fakt faktem – dłuższą chwilę trzeba jednak poświęcić na przygotowanie porządnego posta, tak aby wszystko było jasne dla czytelników…
Podoba mi się ten opis :). Dzięki za obszerny opis.
Dzięki za miłe słowa shpyo ;)
github korzysta z tego dość dużo :) zainspirowałeś mnie do zgłębienia tej części JS
Super, dzięki za czytelny opis i zebranie podstawowych informacji w jednym miejscu.
szkoda, że nie trafiłem tu wcześniej; 3 dni zmarnowałem na różne wtyczki coby opadnować historię na stronie napędzanej głownie ajax’em. W końcu znalazłem również wtyczkę Benjamina Luptona;
Co do przykładów linków z Twittera: „http://twitter.com/#!/messages”.
One właśnie są zoptymalizowane pod wyszukiwarki. Przynajmniej pod, Googla który oficjalnie oświadczył, że crawlery indeksują linki z hashem, jeżeli po hashu jest „!” ;)
@egzemplarz: dzięki, dobrze wiedzieć :) anyway, Google to nie wszystko i lepiej gdzie się da wpychać History API, a resztę olewać lub dać im w zastępstwie hashe.