Przechowywanie danych po stronie klienta

Wiele razy rozpisywałem się o ciasteczkach jako formie przechowywania danych skryptów pomiędzy sesjami przeglądarki. Ciasteczka mają swoje zalety, jak łatwość zarządzania nimi, ale mają też swoje wady w postaci szeregu ograniczeń.

Do największych ograniczeń ciasteczek na pewno zaliczymy maksymalny rozmiar pojedynczego ciasteczka – 4 kilobajty. Nie jest to imponująca wielkość. Można go szybko przekroczyć w rozbudowanych skryptach. Spotkałem się już ze skryptami dzielącymi dane na kilka ciasteczek, ale tutaj znowu zostajemy ograniczeni do pewnej ustalonej ilości ciasteczek dla domeny. Przykładowo, przez długie lata Internet Explorer zezwalał na zapis tylko 20 sztuk na domenę. Ograniczenie to zwiększono dopiero w okolicach sierpnia 2007 do 50 ciastek za pomocą stosownej łatki systemowej. Limit w pozostałych przeglądarkach kształtuje się w następujący sposób:

  • Mozilla Firefox – 50 ciasteczek. Limit ten można zwiększyć poprzez zmianę wartości opcji network.cookie.maxPerHost. Można też ustawić ogólny limit ilości ciasteczek poprzez opcję network.cookie.maxNumber. Domyślnie jest to 1000 ciasteczek.
  • Opera – 30 ciasteczek
  • Safari – tutaj mamy do czynienia z małą ciekawostką. Przeglądarka nie określa limitu ilości ciasteczek. Zarówno Safari 4 Beta jak i Google Chrome 1.0.154.59 pozwoliły zapisać ciasteczka ponad limit 50 ciasteczek. Nie oznacza to jednak, że można sobie ten fakt wykorzystać. Otóż problem pojawia się po stronie serwera. Niektóre serwery nie są w stanie obsłużyć zbyt obszernych żądań HTTP.

Gdy limit ciasteczek zostanie osiągnięty i spróbujemy zapisać kolejne ciastko, zwykle przeglądarki usuwają najstarsze ciasteczko szykując wolne miejsce dla nowego. Wyjątkiem tutaj jest Mozilla Firefox, której algorytm usuwania ciasteczek nie jest do końca jasny. Na pewno nie można powiedzieć, że zawsze usuwane są najstarsze ciasteczka.

Mechanizm ciasteczek w JavaScript nie informuje też w żaden sposób, czy operacja zapisu nowego ciastka się powiodła. Pozostaje jedynie ręczne sprawdzenie, czy nowe ciasteczko istnieje, ale nie poznamy niestety powodu dla którego nowe ciasteczko nie zostało zapisane. A takich scenariuszy może być wiele, np.

  • Użytkownik wyłączył obsługę ciasteczek
  • Konfiguracja przeglądarki nie zezwala na zapis ciasteczek przez daną domenę
  • Użytkownik nie zaakceptował nowego ciasteczka
  • Próbowaliśmy nadpisać ciasteczko z atrybutem httpOnly.

Na szczęście ciasteczka nie są jedynym mechanizmem przechowywania danych na dysku użytkownika. Ten artykuł przeznaczam na krótki przegląd innych dostępnych mechanizmów.

DOM Storage

Pod hasłem DOM Storage kryje się mechanizm opracowywany w ramach specyfikacji HTML 5 Web Storage. Jak widać po adresie szkicu, zyskał on przychylność W3C i ma szansę stać się oficjalnym standardem.

Ze wspomnianej specyfikacji najbardziej interesuje nas mechanizm localStorage, który pozwala przechowywać dane w relacji klucz-wartość na maszynie użytkownika. Mechanizm ten, z założenia podobny do ciasteczek, eliminuje niedogodności swojego "poprzednika". Przede wszystkim można zapisać dużo więcej danych. Mozilla Firefox oferuje domyślnie 5 megabajtów przestrzeni dysku na przechowywanie danych przez stronę. Taki limit jest możliwy do wypracowania, ponieważ, w odróżnieniu od ciasteczek, dane nie są przesyłane wraz z żądaniami HTTP. Ten magazyn istnieje na wyłączny użytek klienckich skryptów. Użytkownik może zmienić rozmiar magazynu za pomocą opcji dom.storage.default_quota.

Kilka słów o API

Magazyn danych dostępny jest dla skryptów pod obiektem window.localStorage. Wspomniany obiekt oferuje kilka własności i metod, za pomocą których można manipulować danymi. Specyfikacja definiuje następujące API:

interface Storage {
   readonly attribute unsigned long length;
   [IndexGetter] DOMString key(in unsigned long index);
   [NameGetter] DOMString getItem(in DOMString key);
   [NameSetter, NameCreator] void setItem(in DOMString key, in DOMString data);
   [NameDeleter] void removeItem(in DOMString key);
   void clear();
};
Zapis danych do magazynu

Dane do magazynu zapisujemy poprzez metodę setItem. Przykładowe wywołanie może wyglądać następująco:

localStorage.setItem('foo', 'bar');

gdzie foo to nazwa klucza, a bar to jego wartość. Jeśli zapis nowej wartości nie powiedzie się, to metoda rzuci wyjątkiem QUOTA_EXCEEDED_ERR. Dwa scenariusze, które mogą powodować wyrzucenie wyjątku to wyłączenie obsługi magazynu przez użytkownika lub przekroczenie pojemności magazynu.

Odczyt danych z magazynu

Aby odczytać zapisane wartości wykorzystujemy metodę getItem, przykładowo:

console.log(localStorage.getItem('foo')); // bar

Gdy spróbujemy odczytać wartość nieistniejącego klucza, getItem zwróci wartość null.

console.log(localStorage.getItem('foo2')); // null
Usuwanie danych z magazynu

Metoda removeItem – jak nazwa wskazuje – usuwa podany klucz z magazynu. Gdy klucz o podanej nazwie nie istnieje, metoda nic nie robi, nie wyrzuca żadnego błędu.

localStorage.setItem('foo','bar'); // zapisujemy klucz w magazynie
console.log(localStorage.getItem('foo')); // bar
localStorage.removeItem('foo');
console.log(localStorage.getItem('foo')); // null

Metoda clear czyści cały magazyn.

localStorage.clear();
Dodatkowe funkcje

Własność – tylko do odczytu – length informuje ile par klucz-wartość jest aktualnie przechowywana w magazynie.

console.log("Aktualnie magazyn przechowuje "+localStorage.length+" wartości");

Z kolei za pomocą metody key możemy dowiedzieć się jaki klucz istnieje pod danym indeksem.

console.log(localStorage.key(0)); // foo

Dzięki metodzie key możemy wypisać wszystkie klucze i ich wartości z magazynu.

for(var i=0, keys = localStorage.length; i < keys; ++i){
	var key = localStorage.key(i);
	console.log(i + ' ' + key + ' = ' + localStorage.getItem(key));
}

Opcjonalny sposób dostępu do danych

Opisane wyżej API gwarantuje przenośność mechanizmu na różne platformy, ale istnieje jeszcze prostsza metoda zapisu i odczytu wartości. Wystarczy potraktować localStorage jako zwykły JavaScriptowy obiekt, do którego przypisujemy klucze wraz z wartościami.
Przykładowe operacje zapisu i odczytu mogą wyglądać następująco:

localStorage.foo = 'bar';
console.log(localStorage.foo); // bar

Trzeba natomiast pamiętać, że przyjmowane są wartości tylko typu String. Przypisanie wartości innego typu będzie skutkować automatycznym rzutowaniem na typ String, czego dowiedzie poniższy fragment kodu:

localStorage.foo = {bar: 'baz'};
console.log(typeof localStorage.foo); // string

Jedyną wadą powyższego rozwiązania jest brak możliwości zapisu kluczy o nazwach length, key, getItem, setItem, removeItem, clear, które są częścią API specyfikacji.

Zdarzenia związane z magazynami

Oprócz obiektu localStorage specyfikacja definiuje zdarzenie StorageEvent, które powinno zostać wygenerowane, gdy zawartość magazynu się zmieni.

Najważniejszymi polami obiektu zdarzenia są:

  • key - informuje o kluczu, który został zmieniony
  • oldValue i newValue - przechowują kolejno starą i nową wartość dla danego klucza
  • storageArea - wskazuje na magazyn, który został zmieniony

W zależności od scenariusza powyższe pola przechowują różne informacje. W przypadku aktualizacji istniejącego klucza w magazynie, pole oldValue będzie przechowywać starą wartość, a newValue – wartość, która zostanie zapisana na miejsce starej.

W przypadku, gdy zapisywany klucz nie istniał w magazynie, pole oldValue będzie ustawione na null.

Analogicznie sprawa wygląda w operacji usuwaniu klucza z magazynu, z tą różnicą, że to pole newValue będzie ustawione na null.

W przypadku czyszczenia magazynu za pomocą metody clear, wszystkie trzy pola – key, oldValue i newValue – będą ustawione na null.

StorageEvent zostanie wygenerowany dla wszystkich dokumentów we wszystkich otwartych oknach przeglądarki, które mają dostęp do tego samego magazynu danych.

Zdarzenie nie bąbluje i nie można go anulować.

Aby przechwycić zdarzenie i je obsłużyć, wystarczy poniższy kawałek kodu:

document.addEventListener('storage', function(e){
	if(e.key === null){
		alert('Magazyn został wyczyszczony');
	} else if(e.oldValue === null){
		alert('Do magazynu został dodany nowy klucz "'+e.key+'" z wartością "'+e.newValue+'"');
	} else if(e.newValue === null){
		alert('Z magazynu został usunięty klucz "'+e.key+'" z wartością "'+e.oldValue+'"');
	} else {
		alert('Nastąpiła aktualizacja wartości klucza "'+e.key+'" z wartości "'+e.oldValue+'" na wartość "'+e.newValue+'"');
	}
}, true);

Na chwilę obecną wygląda na to, że ani Firefox ani Internet Explorer ani Safari nie obsługują API zdarzenia StorageEvent. Zdarzenie jest co prawda generowane, ale nie są dostępne omówione wyżej pola obiektu StorageEvent. Safari w ogóle nie generuje zdarzenia.

Zmieniająca się specyfikacja

Specyfikacja HTML5 – na chwilę obecną – jest w trakcie opracowywania i wiele rzeczy ulega nagłym zmianom. Taki los już spotkał część poświęconą magazynom. Początkowo, w miejscu magazynu localStorage istniał magazyn globalStorage. Magazyn ten różnił się od magazynu lokalnego tym, że jego dane można było udostępniać innym domenom. Dla przykładu, jeśli chcieliśmy zapisać dane dostępne tylko z poziomu mojego bloga, wystarczyło zapisać:

globalStorage['rafael.jogger.pl'].foo='bar';

ale można było też pozwolić domenie jogger.pl na odczyt danych poprzez zapisanie

globalStorage['jogger.pl'].foo='bar';

lub wszystkim stronom z w polskiej domenie

globalStorage['pl'].foo='bar';

lub też każdemu bez względu na domenę

globalStorage[''].foo='bar';

Niestety z powodów bezpieczeństwa zastąpiono globalny magazyn lokalnym odpowiednikiem.

Obsługa przez przeglądarki

Magazyn localStorage obsługują przeglądarki Internet Explorer 8, Safari oraz Mozilla Firefox 3.5 (aktualnie w fazie 4 bety). Mozilla Firefox 2.x oraz 3.0 oferują magazyn globalny, z tym, że w trzeciej edycji Firefoksa zablokowany został dostęp do danych pochodzących z domen wyższej hierarchii, tj. z poziomu rafael.jogger.pl nie mam dostępu do magazynu dla jogger.pl i wyższych.

Obiekt localStorage jest tym samym co magazyn globalStorage[location.host].

userData

Internet Explorer 8 zaoferował webmasterom magazyn localStorage. Wcześniejsze wersje tej przeglądarki, począwszy od piątej edycji oferowały za to funkcjonalność zwaną userData behavior. Funkcjonalność ta nie zyskała do tej pory zbytniej popularności. W zasadzie przypomniano sobie o niej w momencie przedstawienia magazynów danych w specyfikacji WebApps i wykorzystuje się ją jako zastępnik magazynów w starszych wersjach IE.

W odróżnieniu od magazynów danych, userData nie definiuje globalnego obiektu. W tym mechanizmie wykorzystuje się poszczególne elementy HTML dokumentu. Cały mechanizm polega właśnie na możliwości dodawania własnych danych do elementów, które mogą zostać odtworzone pomiędzy sesjami przeglądarki. Jedynymi elementami, dla których nie można wykorzystać behawioru userData są html, head, title oraz style.

Pojemność

Pojemność – nazwijmy dla uproszczenia – magazynu zależna jest od strefy bezpieczeństwa, w której znajduje się aktualnie wczytany dokument. Poniższa tabela prezentuje poszczególne wielkości:

Pojemności UserData dla poszczególnych stref. źródło
Security ZoneDocument Limit (KB)Domain Limit (KB)
Local Machine1281024
Intranet51210240
Trusted Sites1281024
Internet1281024
Restricted64640

Dla strefy Internet, która nas najbardziej interesuje, mamy 128 kilobajtów na cały dokument i 1 megabajt na domenę, co jest dużo lepiej niż pojemność wszystkich ciasteczek dla domeny, ale znowu sporo mniej niż oferuje to magazyn localStorage.

Jak z tego korzystać?

Aby "zadeklarować" chęć korzystania z danych użytkownika, należy dodać do wybranych elementów dokumentu behawior userData. Można to zrobić na dwa sposoby – poprzez arkusz stylów lub z użyciem skryptu JS.

Fragment arkusza stylów może prezentować się następująco:

#wybrany_element {
	behavior: url(#default#userData);
}

Oczywiście nic nie stoi na drodze dodać behawior do wielu elementów za jednym zamachem. Wystarczy użyć selektora, który obejmie większą grupę elementów strony.

.wybrane_elementy {
	behavior: url(#default#userData);
}

Dodanie behawioru za pomocą JavaScriptu można wykonać poprzez modyfikację obiektu style elementu. Dokładnie tak, jak modyfikuje się dowolną własność CSS elementów.

document.getElementById('wybrany_element').style.behavior = 'url(#default#userData');

Autorzy przeglądarki oddali też do dyspozycji webmasterom specjalną metodę addBehavior, która służy wyłącznie do dodawania behawiorów.

document.getElementById('wybrany_element').addBehavior('#default#userData');

Wykorzystanie arkuszy stylów oraz modyfikacji obiektu style poprzez skrypty można potraktować jako "bezpieczne" operacje. Mam na myśli tutaj własność parserów arkuszy stylów, które mają obowiązek zignorować nieznane własności. W ten sposób nie zostanie wyrzucony żaden błąd, jeśli przeglądarka nie obsługuje behawiorów. Może jest to pewna zaleta, ale takie "ciche" zachowanie nie informuje, czy przeglądarka obsługuje to, czego od niej oczekujemy. Z tego powodu preferuję wykorzystać jednak metodę addBehavior, ponieważ można łatwo stwierdzić, czy przeglądarka obsługuje wymagane mechanizmy.

var element = document.getElementById('wybrany_element');
if(element.addBehavior)
	element.addBehavior('#default#userData');

Microsoft, w swojej dokumentacji w jednym z przykładów sugeruje nawet stworzenie własnego, niestandardowego elementu, np. cache.

<html xmlns:sdk="">
<head>
…
</head>
<body>
<sdk:cache id="localStorage"></sdk:cache>
</body>
</html>

Moim zdaniem, wydaje się to być całkiem sensowne, ale niepotrzebne. W artykule skupię się na całkowitej obsłudze mechanizmu z poziomu skryptów JS.

Wymagania odnośnie inicjalizacji

W powyższych przykładach odwołujemy się do elementów, które istnieją w dokumencie. Jednak konieczność dodania do strony elementu, który będzie służył tylko jako magazyn danych to swego rodzaju śmiecenie dokumentu. Postanowiłem więc wygenerować nowy element skryptowo, zainicjować na nim obsługę behawiorów i nie dodawać go do drzewa dokumentu.

var cache = document.createElement('cache');
cache.addBehavior('#default#userData');
cache.setAttribute('foo', 'bar'); // ok
cache.save('cache'); // błąd

Niestety, wszystko działało dopóki nie wykonałem metody save na elemencie. Gdy tylko dodamy element do dokumentu, wszystko zacznie działać jak należy. Jak widać, konieczne jest operowanie na istniejącym w dokumencie elemencie a to stwarza kolejny problem – trzeba poczekać aż element na którym chcemy operować zostanie wczytany przez przeglądarkę. Niestety, nie jest możliwe zainicjowanie skryptu od razu, choć można spróbować wykorzystać do tego celu własność document.documentElement.

Zapis danych

Za dodawanie danych do elementu odpowiedzialna jest metoda setAttribute – tak, ta metoda, którą wykorzystujemy do ustawiania atrybutów elementów. Należy jednak pamiętać, żeby po ustawieniu wartości atrybutu wywołać metodę save z argumentem w postaci nazwy magazynu.

element.setAttribute('foo', 'bar');
element.save('localStorage');
Odczyt danych

Odczyt, jak się można domyślić, odbywa się poprzez metodę getAttribute. Tutaj jednak, przed odczytem należy wywołać metodę load, żeby wczytać aktualne dane z magazynu.

element.load('localStorage');
element.getAttribute('foo');
Usuwanie danych

Dane z magazynu usuwamy z użyciem metody removeAttribute.

element.removeAttribute('foo');

Tutaj także pamiętamy o trwałym zapisie zmian.

element.save('localStorage');

Flash cookies/Flash Local Storage

Lokalny magazyn Flash to kolejny mechanizm, który pozwala zapisywać dane w komputerze użytkownika w postaci obiektów współdzielonych (Shared Objects).

W przeciwieństwie do wcześniej omówionych mechanizmów, FLS nie jest natywnie dostępny w przeglądarce, wymagana jest obecność wtyczki Flasha.

FLS oferuje standardowo 100 kilobajtów pamięci na obiekty współdzielone. Gdy dostępna przestrzeń okaże się za mała do zapisania kolejnej porcji danych, użytkownik może zostać poproszony o zwiększenie przydziału.

SharedObject

Aby operować na obiektach współdzielonych, należy zdobyć referencję do obiektu poprzez

var test : SharedObject = SharedObject.getLocal('test');

Gdy obiekt o nazwie test nie istnieje, zostanie automatycznie utworzony. Instancja obiektu oferuje własność data będącą kolekcją kluczy z przypisanymi wartościami.

Aby zapisać dane należy dodać nowy klucz do kolekcji .data

test.data['foo']='bar';

i wywołać metodę .flush()

test.flush();

która wymusi natychmiastowe zapisanie kolekcji na dysku użytkownika.

Aby odczytać dane należy wykonać prostą operację

var value : String = test.data['foo']; // bar

Poszczególne klucze możemy usunąć za pomocą operatora delete.

delete test.data['foo'];
test.flush();

Interakcja pomiędzy JavaScriptem a Flashem

Teraz, gdy wiemy jak odczytywać i zapisywać dane w obiektach współdzielonych, możemy zająć się interakcją pomiędzy skryptami JS i Flashem.

Sprawa nie jest skomplikowana. Wystarczy w pliku ActionScriptu zaimportować pakiety z przestrzeni flash.external.

import flash.external.*;

W przestrzeni nazw znajdziemy klasę ExternalInterface, która jest łącznikiem pomiędzy stroną WWW a plikiem flasha. Najbardziej interesuje nas statyczna metoda addCallback, która otwiera kanał komunikacji JS -> Flash.

Wspomniana metoda oczekuje trzech argumentów (liczba argumentów dotyczy ActionScript 2. W ActionScript 3 sygnatura metody zmieniła się i oczekuje dwóch argumentów):

ExternalInterface.addCallback(methodName:String, instance:Object, method:Function) : Boolean

Pierwszy argument stanowi nazwę funkcji widoczną dla JavaScriptu.

Drugi odnosi się do instancji obiektu na który wskazywać będzie słowo kluczowe this.

Ostatni argument stanowi referencję do funkcji AS, która zostanie wykonana po wywołaniu przez JS funkcji z pierwszego argumentu.

W ramach pliku datastorage.as utworzymy klasę ze statycznymi metodami, które oddamy do użytku skryptowi JS.

import flash.external.*;
class Hashtable {
	static public function get(name : String, key : String) : String {
		return SharedObject.getLocal(name).data[key];
	}
	
	static public function set(name : String, key : String, value : String) {
		var storage : SharedObject = SharedObject.getLocal(name);
		storage.data[key] = value;
		storage.flush();
	}
	
	static public function remove(name : String, key : String) {
		var storage : SharedObject = SharedObject.getLocal(name);
		delete storage.data[key];
		storage.flush();
	}
	
	static function main(){
		ExternalInterface.addCallback('get', null, DataStorage.get);
		ExternalInterface.addCallback('set', null, DataStorage.set);
		ExternalInterface.addCallback('remove', null, DataStorage.remove);
	}
}

Następnie należy plik z powyższą treścią skompilować. W tym celu wykorzystamy otwarto-źródłowy kompilator Motion-Twin ActionScript 2 Compiler (w skrócie mtasc).

Wystarczy z linii poleceń wydać komendę:

mtasc -swf hashtable.swf -main -version 8 -strict ./hashtable.as

Google Gears

Gears od Google oferuje(ą) kolejny mechanizm, który pozwala przechowywać informacje po stronie klienta. Mam tutaj na myśli moduł DataBase, który jest czymś więcej niż zwykłym magazynem danych. Jest to cały lokalny system bazodanowy, w którym możemy definiować całe struktury tabel. Dla potrzeb prostego magazynu danych, w którym przechowujemy dane w postaci klucz-wartość wystarczy mała tabela o strukturze:

CREATE TABLE IF NOT EXISTS test (
	key TEXT NOT NULL UNIQUE PRIMARY KEY,
	value TEXT NOT NULL
);

Bazodanowe API Google Gears jest synchronicznym API, czyli operacje na bazie danych są wykonywane w "głównym wątku" aplikacji. Jest to wygodny sposób dla oprogramowania, ale fakt ten jest uważany za wadę Google Gears ze względu na możliwe blokady UI przeglądarki do czasu zakończenia długich operacji bazodanowych. W przypadku silnika Webkit, w którym zaimplementowano podobny mechanizm bazodanowy, zdecydowano się na asynchroniczne API.

Jak korzystać z API?

Przyjmując, że wstawiliśmy do dokumentu plik gears_init.js, który dokona inicjalizacji zabawki Google, wystarczy utworzyć instancję bazy, połączyć się z nią i utworzyć tabelę magazynu, jeśli ta jeszcze nie istnieje.

var db = google.gears.factory.create('beta.database');
db.open('database-test');
db.execute('CREATE TABLE IF NOT EXISTS test (key TEXT NOT NULL UNIQUE PRIMARY KEY, value TEXT NOT NULL)');

Po wykonaniu powyższych instrukcji możemy swobodnie operować na utworzonej baize danych.

db.execute('INSERT INTO test VALUES (?, ?)', ['foo', 'bar']);
db.execute('SELECT key, value FROM test WHERE key = ?', ['foo']);
db.execute('DELETE FROM test WHERE key = ?', ['foo']);

Drobny "problem" – który wynika ze specyfiki działania relacyjnych baz danych – istnieje w przypadku dodawania danych. Nadany klucz główny na kolumnę key spowoduje, że drugie z poniższych zapytań nie wykona się, ponieważ dany klucz już istnieje w bazie.

INSERT INTO test VALUES ('foo', 'bar');
INSERT INTO test VALUES ('foo', 'baz');

Jeśli chcemy ujednolicić działanie wszystkich mechanizmów, konieczne będzie zmuszenie bazy z Google Gears do aktualizacji istniejących kluczy, gdy nastąpi wyżej przedstawiona sytuacja. Dokonamy tego poleceniem INSERT OR REPLACE.

INSERT OR REPLACE INTO test VALUES ('foo', 'baz');

Powyższe zapytanie usunie starą wartość (tj. nie nastąpi aktualizacja danych, tylko usunięcie starych danych) i wstawi nowe dane do bazy.

5 w 1

Jak widać, mamy do czynienia z 5 alternatywami (wliczając globalStorage) dla ciasteczek pozwalającymi przechowywać dane po stronie klienta. Każde z prezentowanych rozwiązań definiuje własne API, które trzeba wykorzystać w własnych skryptach. Zanim zaczniemy z któregokolwiek API korzystać, jesteśmy zmuszeni do sprawdzenia co dana przeglądarka obsługuje. Z tego właśnie powodu postanowiłem napisać mały skrypcik, który całą robotę wykona za nas i zaoferuje jednolite API do obsługi magazynów.

Dodanie skryptu do strony

Aby dodać skrypt do strony, wystarczy wstawić odpowiedni plik w ramach nagłówka

<script type="text/javascript" src="storage.js"></script>

a następnie zainicjować go poleceniem:

var magazyn = Hashtable.setup({
	name:'test',
	swf:'hashtable.swf'
});

Obydwie wyżej wymienione opcje (name i swf) są konieczne do poprawnego działania skryptu. O przeznaczeniu poszczególnych opcji dowiesz się w dalszej części artykułu.

Metoda setup zwraca referencję do obiektu z funkcjami zarządzającymi magazynem lub null, gdy nie można było zainicjować żadnego magazynu.

Zwrócony obiekt zawiera 4 funkcje:

  • type() – zwraca String z nazwą wykorzystywanego magazynu
  • get(key) – zwraca String z wartością przypisaną do podanego klucza lub null, gdy klucz nie występuje w magazynie
  • set(key, value) – ustawia wartość value dla podanego klucza key
  • remove(key) – usuwa podany klucz z magazynu

Jeśli drugi raz wywołamy metodę Hashtable.setup (np. z innymi opcjami) zwrócona zostanie referencja z pierwszego wywołania.

Dodatkowo obiekt Hashtable oferuje metodę order, która oczekuje jako argumentu tablicy z nazwami magazynów, które chcemy spróbować zainicjować w podanej kolejności. Metodę wywołujemy przed wywołaniem metody setup.

Hashtable.order(['localstorage','globalstorage','gears','userdata','flash']);

Wszystkie nazwy magazynów podajemy z małych liter. Powyższy przykład prezentuje wszystkie dostępne nazwy.

Jeśli chcemy pominąć jakiś magazyn w próbie inicjacji, wystarczy nie podać go w tablicy przekazanej do metody Hashtable.order, np.

Hashtable.order(['localstorage','globalstorage','gears']);

Opcje skryptu

Opcje skryptu przekazujemy jako obiekt do metody Hashtable.setup. Poniżej przedstawiam przeznaczenie poszczególnych opcji:

OpcjaMagazynPrzeznaczenie
nameGoogle Gearsstanowi nazwę bazy danych i nazwę tabeli w tej bazie
UserDatastanowi nazwę magazynu
Flashstanowi nazwę magazynu oraz identyfikator utworzonego elementu OBJECT. Z tego powodu trzeba zadbać, żeby nazwa była unikalnym identyfikatorem w całym dokumencie
swfFlashstanowi ścieżkę do pliku swf realizującego funkcje magazynu flashowego
writeFlashjeśli ustawiona na true, spowoduje, że element OBJECT zostanie wydrukowany na stronie (document.write). Jeśli ustawiona na false (lub w ogóle nie ustawiona) element OBJECT zostanie wstawiony za pomocą metody appendChild do dokumentu.

Wymagania skryptu

Niektóre mechanizmy, których obsługę zawarłem w skrypcie wymagają spełnienia kilku warunków. Poniżej przedstawiam wymagania pogrupowane wg API:

Google Gears

Konieczne jest wstawienie przed skryptem pliku gears_init.js. Mogłem go zintegrować ze skryptem, ale ostatecznie uznałem to za niepotrzebne.

Flash Local Storage

W przypadku Flasha konieczne jest wstawienie do dokumentu elementu OBJECT ze ścieżką do pliku swf. Aby możliwe było dodanie nowego elementu do dokumentu trzeba:

  • Wykonać inicjację po załadowaniu dokumentu (wykorzystać zdarzenie load lub DOMContentLoaded)

lub

  • Ustawić opcję opt.write na true, która spowoduje, że kod elementu zostanie wydrukowany (document.write) na stronie

Druga opcja wiąże się z kolejnym problemem. Internet Explorer (6, 7 i 8) wymusza wydrukowanie elementu w ramach BODY. Jeśli zainicjujemy skrypt w ramach nagłówka dokumentu, obiekt flashowy nie uruchomi się i nie zostaną zainicjowane funkcje zawarte w pliku swf. W takim wypadku próba wykonania operacji na magazynie danych zakończy się błędem.

UserData

Aktualnie mechanizm UserData inicjowany jest na elemencie na który wskazuje własność document.documentElement. Z testów wynika, że sposób ten działa niezależnie od momentu jego inicjacji. Gdyby jednak stwarzał problemy, konieczne będzie zainicjowanie go w momencie załadowania dokumentu (zdarzenie load lub DOMContentLoaded).

Podsumowanie

Na tym kończy się lista znanych mi i obsługiwanych przez przeglądarki magazynów z synchronicznym dostępem do danych. Dodatkowo zdaję sobie sprawę z istnienia mechanizmu zwanego IsolatedStorage będącego częścią Microsoft Silverlight. Ze wspomnianą technologią Microsoftu nie miałem jeszcze do czynienia, ale obiecuję, że postaram się nadrobić „zaległości” i w miarę szybko dodać kolejny mechanizm do zaprezentowanego w tym artykule skryptu.

Oprócz powyższych, istnieje też kilka API z asynchronicznym dostępem do danych, które omówię w kolejnym artykule.


Kategorie: JavaScript 09 maja 2009, 19:44:53 5 komentarzy

Komentarze dla notki “Przechowywanie danych po stronie klienta”

  1. toszcze - 09 maja 2009, 23:38:15

    Bardzo ciekawy tekst. Ale przyczepię się (z przymrużeniem oka, rzecz jasna) do jednego zdania:

    <em>Wyjątkiem tutaj jest Mozilla Firefox, której algorytm usuwania ciasteczek nie jest do końca jasny.</em>

    Jak to – nie jest jasny? Wystarczy zajrzeć do źródeł – przecież to główny plus oprogramowania Open Source ;)

  2. Rafał - 10 maja 2009, 00:13:39

    @toszcze: A wiesz, zaglądałem. Niby powinno być tak jak w innych przeglądarkach (przynajmniej tak z komentarzy w kodzie wnioskuję, bo składni C++ nie trawię i zrezygnowałem z dokładnej analizy)... ale jednak nie jest. Może kiedyś uda mi się to rozgryźć i zrozumieć cały algorytm Firefoksa.

  3. dd - 10 maja 2009, 11:12:35

    Rafał: Jest jeszcze możliwość używania SQL z JavaScriptem – http://dev.w3.org/html5/webstorage/#sql

  4. Rafał - 10 maja 2009, 11:36:09

    @dd: tak, zwróciłem uwagę na tę część szkicu, ale – jak napisałem w podsumowaniu do artykułu – skupiłem się tylko na mechanizmach z synchronicznym dostępem do danych oraz zaimplementowanych przynajmniej przez jedną popularną przeglądarkę. Jedynie w Safari (testuję Safari 4 beta) znalazła się obsługa asynchronicznego API, które mam zamiar opisać w kolejnym artykule.

  5. Kildyt - 29 lipca 2010, 12:21:11

    Bardzo dobry artykuł! Gratulacje!

Pozostaw komentarz

Copyright © 2003-2011 Rafał Kukawski. Powered by Jogger | RSS Subskrybuj