Niejednokrotnie tworząc Single Page Application (SPA) zastanawiałeś się jak zapisać niezbędne do działania aplikacji dane po stronie przeglądarki. Być może słyszałeś o localStorage, sessionStorage, cookies czy indexedDB, ale nie do końca wiesz czym się do siebie różnią i w jakim przypadku będą najlepszą opcją dla Twojej aplikacji. Może dopiero zaczynasz swoją przygodę z web developmentem i chcesz dowiedzieć się jakie opcje przechowywania danych posiadają przeglądarki i kiedy w nich korzystać.
Czytając ten artykuł dowiesz się czym są localStorage, sessionStorage, cookies i indexedDB. Poznasz wady, zalety oraz wykorzystanie poszczególnych rozwiązań, a także zobaczysz jak się z nimi pracuje.
Zbudujemy prostą aplikację z trzema przyciskami wywołującymi kolejno zapisywanie, pobieranie i usuwanie danych z każdego z zasobów. Dane, które chcemy zapisać do zasobu wpisujemy w pole Dane do zapisania. Pobrane dane wyświetlą się poniżej. Wszystkie operacje będziemy też obserwować w developer tool przeglądarki.
Przez wiele lat (do momentu pojawienia się HTML5) cookies były jedyną forma przechowywania danych użytkownika w przeglądarce. Jest to zasób w postaci par klucz – wartość zapisywanych jako String na dysku użytkownika.
Dostęp do zasobu: Ciasteczka są wymieniane między serwerem a klientem(przeglądarką) w każdym żądaniu http. Dlatego obie strony tego żądania mają do nich dostęp. Dodatkowo użytkownik może zmodyfikować/wyczyścić ciasteczka w przeglądarce.
Okres przechowywania: Ciasteczka mogą mieć ustawioną datę ważności i wygasać w momencie przekroczenia tej daty. Jeśli data nie jest ustawiona, to ciasteczka wygasają w momencie zakończenia sesji.
Limity wielkości: Minimalna ilość ciasteczek pochodzących z danej domeny określona w standardzie Request for Comments to 20. Przeglądarki mogę zwiększyć ten limit. Zazwyczaj waha się on między 30 a 50 ciasteczek. Niektóre przeglądarki (IE, Opera, Safari) limitują także ilość miejsca, jakie dana domena może wykorzystać na swoje ciasteczka do 4KB.
Zalety: umożliwia wymianę danych między klientem a serwerem. Ma dosyć zrozumiałe API, które jednak mogłoby zostać ulepszone np. poprzez dodanie metody pozwalającej wybrać pojedyncze ciasteczko.
Wady: Cookies nie nadają się do przechowywania skomplikowanych struktur danych. Wszystkie ciasteczka są wymieniane między serwerem a klientem w każdym żądaniu http, dlatego mogą niepotrzebnie zmniejszać przepustowość.
Wykorzystanie: ID sesji, podstawowe dane/preferencje użytkownika. Sprawdzanie tożsamości użytkowników, aby nie musiały być potwierdzane na każdej stronie aplikacji.
Z poziomu JavaScript dostęp do cookies mamy poprzez document.cookie. Jest to dostęp do wszystkich ciasteczek zdefiniowanych dla danej domeny, a nie jak nazwa mogłaby wskazywać do pojedynczego ciasteczka.
Ciasteczko dodajemy wpisując jego nazwę i wartość. Opcjonalnie po średniku możemy dodać datę ważności jako okres podany w sekundach np. max-age=360 lub konkretną datę expires=Fri, 15 Jan 2021 20:00:00 GMT. Skorzystamy z obu opcji zapisując 2 ciasteczka.
dataStoreBtn.addEventListener('click', () => {
document.cookie = `dataInput1=${dataInput.value}; max-age=600`;
document.cookie = `dataInput2=1; expires=Fri, 15 Jan 2021 20:00:00 GMT`;
});
Otwieramy development tool przeglądarki (F12) i sprawdzamy, czy cookies zostały zapisane. W Firefox wszystkie dane zapisane w pamięci przeglądarki znajdziemy w zakładce Dane.
Przystępujemy do pobrania danych. Niestety w odpowiedzi otrzymujemy wszystkie ciasteczka, stąd aby uzyskać wartość pojedynczego ciasteczka musielibyśmy dodatkowo pociąć otrzymany String.
dataRetrieveBtn.addEventListener('click', () => {
let cookieValue = document.cookie;
dataDisplay.innerHTML = cookieValue;
});
Na koniec usuniemy cookie dataInput1 poprzez ustawienie daty ważności na datę przeszłą. W development tool widzimy, że pozostało już tylko jedno ciasteczko dataInput2.
dataDeleteBtn.addEventListener('click', () => {
document.cookie = `dataInput1=${dataInput.value}; expires=Fri, 15 Jan 2021 09:11:17 GMT`;
});
LocalStorage i SessionStorage przechowują dane w postaci pary klucz – wartość. Zarówno klucz jak i wartość przechowane są jako String.
Dostęp do zasobu: Programista ma dostęp do zasobu poprzez kod JavaScript. Użytkownik może zmodyfikować/wyczyścić localstorage i sessionStorage poprzez development tool.
Okres przechowywania: LocalStorage przechowuje dane do momentu ich usunięcia przez programistę lub użytkownika.
SessionStorage jest gromadzony tak długo jak długo trwa sesja, czyli dopóki strona jest otwarta w przeglądarce. Zasób jest czyszczony po zamknięciu zakładki/przeglądarki.
Limity wielkości: Limity są zależne od przeglądarki. Dla przeglądarek desktopowych w większości to 10MB. Jedynie Safari udostępnia 5MB. Dla przeglądarek mobilnych wahają się między 2MB a 10MB.
Zalety: LocalStorage i sessionStroage są bardzo łatwe w użyciu. Posiadają też całkiem spory limit wielkości.
Wady: Oba zasoby nie nadają się do przechowywania skomplikowanych struktur danych.
Wykorzystanie: ID sesji, podstawowe dane/preferencje użytkownika. Zasoby localStorage mogą być wykorzystane do zapewnienia ciągłości użytkowania aplikacji w czasie braku połączenia internetowego lub do identyfikowania powracających na stronę użytkowników miedzy sesjami.
W JavaSript localStorage i sessionStorage to property obiektu window, dlatego w kodzie możemy odwoływać się do nich bezpośrednio. Są bardziej intuicyjne w użyciu od cookies. Przykład przedstawia jedynie localStorage, ponieważ dla sessionStorage wygada on identycznie. Wartość ustawiamy za pomocą metody setItem().
dataStoreBtn.addEventListener('click', () => {
localStorage.setItem('dataInput1', dataInput.value);
localStorage.setItem('dataInput2', 1);
});
W development tool dane localStorage znajdziemy w zakładce Lokalna pamięć, sessionStorage w zakładce Pamięć sesji.
Pobieramy dane metoda getItem(). W przeciwieństwie do ciasteczek, tutaj jesteśmy w stanie pobrać pojedyncze wartości.
dataRetrieveBtn.addEventListener('click', () => {
dataDisplay.innerHTML = localStorage.getItem('dataInput1');
});
Usuwanie danych jest równie proste jak pozostałe operacje.
dataDeleteBtn.addEventListener('click', () => {
localStorage.removeItem('dataInput1');
});
IndexedDB to baza danych wbudowana w przeglądarkę. Może przechowywać pary klucz wartość, obiekty bez konieczności ich serializacji, a także pliki i obiekty blob. Posiada asynchroniczne API operujące na zdarzeniach.
Dostęp do zasobu: asynchroniczne API.
Okres przechowywania: IndexedDB przechowuje dane do momentu ich usunięcia przez programistę lub użytkownika.
Limity wielkości: Tu znowu limity są zależne od przeglądarki. Chrome pozwala na użycie do 80% miejsca na dysku, Firefox – do 50% dostępnej na dysku przestrzeni, IE10 lub nowszy do 250MB, Safari do 1GB.
Zalety: Jest to baza danych, zatem może przechowywać skomplikowane struktury danych.
Wady: API jest nie do końca intuicyjne. W porównaniu do localStorage/sessionStorage czy ciasteczek potrzebujemy o wiele więcej kodu do osiągniecia zamierzonego celu. IE wspiera indexedDB jedynie częściowo.
Wykorzystanie: Przechowywanie skomplikowanych struktur danych, których nie możemy przechowywać w ciasteczkach lub localStorage.
Wykonanie operacji zapisu, pobrania i usunięcia danych w indexedDB będzie wymagało od nas nieco więcej pracy. Najpierw musimy otworzyć połącznie z bazą. W tym celu skorzystamy z metody open() z nazwą bazy i jej wersją podanymi jako argumenty metody. Jeśli baza jeszcze nie istnieje, polecenie ją utworzy. W przeciwnym wypadku otworzy połączenie z istniejącą bazą.
const request = indexedDB.open('ExampleDB', 1);
Metoda zwraca nam request, w którym możemy nasłuchiwać na zdarzenia onsuccess i onerror. W obu mamy dostęp do obiektu event. Dostęp do bazy, na której później będziemy mogli wykonywać operacje mamy poprzez event.target.result.
let dataBase;
request.onsuccess = (event) => {
dataBase = event.target.result;
}
request.onerror = (event) => {
console.log('Error indexedDb', event)
}
Co prawda uzyskaliśmy dostęp do bazy nasłuchując na zdarzenie onsuccess, jednak w tym momencie tylko otworzyliśmy połączenie z bazą. Abyśmy mogli przeprowadzać na niej operacje musimy utworzyć store (odpowiednik tabeli) obsługując zdarzenie onupgradeneeded. Jako argumenty metody createObjectStore podajemy nazwę tworzonego store i obiekt opcji. W opcjach możemy podać keyPath – unikalny identyfikator obiektów i autoIncrement – dodający generator kluczy do store (domyślnie ustawiony na false).
let dataBase;
request.onupgradeneeded = (event) => {
dataBase = event.target.result;
objectStore = dataBase.createObjectStore('zbior', { keyPath: 'id'});
}
W development tool widzimy nowoutworzona bazę.
Do utworzenia nowego rekordu w store musimy wywołać metodę transaction() na naszej bazie. Jako parametry metody podajemy nazwę store i tryb, w którym będziemy pracować. W naszym wypadku będzie to readwrite. Następnie wywołujemy store, do którego dodajemy rekord, a na nim wywołujemy metodę add. Jako argument metody należy podać dodawany obiekt.
dataRetrieveBtn.addEventListener('click', () => {
const request = dataBase.transaction('zbior', 'readonly').objectStore('zbior').get(1);
request.onsuccess = function() {
dataDisplay.innerHTML = request.result.dataInput;
}
request.onerror = function() {
console.log('Nie znaleziono obiektu');
}
});
W zbiorze pojawił się nowy rekord.
Identyczny request zbudujemy do pozyskania danych z bazy zamieniając metodę add() na get(). Metoda get() jako argument przyjmuje unikalny identyfikator obiektu. Dzięki temu pobraliśmy dane z bazy.
dataRetrieveBtn.addEventListener('click', () => {
const request = dataBase.transaction('zbior', 'readonly').objectStore('zbior').get(1);
request.onsuccess = function() {
dataDisplay.innerHTML = request.result.dataInput;
}
request.onerror = function() {
console.log('Nie znaleziono obiektu');
}
});
Podobnie z usuwaniem obiektu. Tym razem metodę get() zamienimy na delete().
dataDeleteBtn.addEventListener('click', () => {
const request = dataBase.transaction('zbior', 'readwrite').objectStore('zbior').delete(1);
request.onerror = function() {
console.log('Nie znaleziono obiektu');
}
});
Po uruchomieniu metody widzimy pusty zbiór.
Jak widać najbardziej wszechstronnym zasobem do zapisywania danych w pamięci przeglądarki jest indexedDB, ponieważ pozwala zapisywać złożone obiekty a limity wielkości zapisu są bardzo duże. Niemniej jednak ich zapisanie kosztowało nas całkiem sporo pracy i linijek kodu, dlatego powinniśmy wykorzystywać indexeDB w ostateczności, kiedy localStorage i cookies nie pozwalają na zapisanie tak skomplikowanych danych. W znakomitej większości przypadków ilość miejsca dostępnego dla localStorage/sessionStorage oraz cokkies jest wystarczająca. Dlatego jeśli potrzebujemy wymiany danych z serwerem używamy cookies, w przeciwnym wypadku localStorage posiada tak proste API, że aż szkoda z niego nie skorzystać 😊.