Rozdział 8. Local storage

> Dodaj do ulubionych

Przeszłość, teraźniejszość i przyszłość lokalnego składowania danych przez aplikacje sieciowe

Do rzeczy

Możliwość trwałego przechowywania danych to jedna z cech dająca aplikacjom instalowanym na komputerze przewagę nad aplikacjami sieciowymi. Programy lokalne mogą korzystać z warstwy abstrakcji dostarczanej przez system operacyjny i przy jej użyciu zapisywać oraz odczytywać takie dane, jak preferencje czy stan wykonawczy. Informacje mogą być zapisane w rejestrze, plikach INI, plikach XML oraz w wielu innych miejscach. Jeśli w programie trzeba przechowywać więcej informacji niż tylko pary klucz-wartość, można utworzyć własną bazę danych, wynaleźć własny format plików oraz skorzystać z wielu innych rozwiązań.

Kiedyś luksusy te były niedostępne dla aplikacji sieciowych. Wprawdzie od dawana można używać plików cookie, ale służą one do przechowywania tylko bardzo małych ilości danych. Ponadto pliki te mają trzy poważne wady:

  • Dane cookie są przesyłane w każdym żądaniu HTTP, przez co spowalniają działanie aplikacji zmuszając ją do niepotrzebnego przesyłania w tę i z powrotem tych samych danych
  • Dane cookie są przesyłane w każdym żądaniu HTTP w niezaszyfrowanej formie (chyba że cała aplikacja jest serwowana poprzez SSL)
  • W cookie można przechowywać maksymalnie około 4 KB danych — wystarczy, aby spowolnić aplikację, ale za mało, aby zrobić z tego dobry użytek

My natomiast potrzebujemy:

  • dużo miejsca na dane;
  • miejsca na kliencie;
  • magazynu, który nie znika po odświeżeniu strony;
  • magazynu, który nie jest wysyłany na serwer.

Przed powstaniem języka HTML5 wszelki próby opracowania satysfakcjonującego rozwiązania spełzły na niczym z różnych powodów.

Krótka historia lokalnego przechowywania danych w czasach przed powstaniem HTML5

Na początku był tylko Internet Explorer. A przynajmniej Microsoft chciał, aby świat tak myślał. Firma ta w ramach działań na polu pierwszej wielkiej wojny przeglądarek opracowała wiele wynalazków, które zaimplementowała w swojej mającej zakończyć wszelkie wojny przeglądarce Internet Explorer. Jedną z tych nowości były zachowania DHTML, a jednym z tych zachowaniem było userData.

userData pozwala stronom na przechowywanie do 64 KB danych na domenę w hierarchicznej strukturze XML. (Zaufane domeny, np. witryny intranetowe, mogą przechowywać dziesięć razy więcej danych. A 640 KB powinno wystarczyć już każdemu.) IE nie wyświetla żadnego okna z pytaniem o pozwolenie i nie ma możliwości zwiększenia limitu.

W 2002 r. firma Adobe wprowadziła we Flashu 6 funkcję, która nieszczęśliwie została nazwana „cookie Flasha”. W samym środowisku Flasha funkcja ta występuje pod prawidłową nazwą Lokalne obiekty wspólne (Local Shared Objects). Mówiąc krótko funkcja ta umożliwia Flashowi przechowywanie do 100 KB danych na domenę. Brad Neuberg opracował jeden z pierwszych prototypów mostów łączących Flasha i JavaScript o nazwie AMASS (AJAX Massive Storage System), ale jego możliwości były ograniczone ze względu na niedoskonałości projektowe Flasha. W 2006 r., gdy pojawił się ExternalInterface we Flashu 8, dostęp do LSO z poziomu JavaScriptu stał się znacznie łatwiejszy i szybszy. Neuberg od nawa napisał AMASS i wcielił go do popularnej biblioteki Dojo Toolkit pod przydomkiem dojox.storage. Flash daje każdej domenie 100 KB darmowego miejsca na dane. Ponadto umożliwia użytkownikowi zwiększenie tego limitu o rzędy wielkości — do 1 Mb, 10 Mb itd.

W 2007 r. Google uruchomiło Gears, otwartą wtyczkę rozszerzającą możliwości przeglądarek internetowych. (O Gears wspominałem już przy omawianiu API geolokalizacji w Internet Explorerze.) Gears udostępnia API umożliwiające osadzanie baz danych SQL na silniku SQLite. Po uzyskaniu zgody od użytkownika, Gears może przechowywać nieograniczoną ilość danych na domenę w bazie danych SQL.

W międzyczasie Brad Neuberg i inni kontynuowali pracę nad dojox.storage z zamiarem utworzenia jednolitego interfejsu do tych wszystkich różnych wtyczek i API. W 2009 r., dojox.storage automatycznie wykrywał (i udostępniał jednolity interfejs) Adobe Flash, Gears, Adobe AIR oraz wczesny prototyp magazynu HTML5, który był zaimplementowany tylko w starszych wersjach Firefoksa.

Gdy przyjrzeć się tym wszystkim rozwiązaniom, można zauważyć pewne prawidłowości: wszystkie są przypisanej jednej konkretnej przeglądarce albo wymagają zewnętrznych wtyczek. Mimo heroicznych wysiłków, aby je ujednolicić w dojox.storage, rozwiązania te radykalnie się między sobą różnią interfejsami, limitami pamięci oraz sposobem działania. Dlatego problem postanowiono rozwiązać w HTML5: opracowano standardowe API, które powinno być jednakowo zaimplementowane we wszystkich przeglądarkach i nie wymagało do obsługi żadnych zewnętrznych wtyczek.

Wprowadzenie do magazynu HTML5

Magazynem HTML5 nazywam specyfikację o nazwie Web Storage, która kiedyś wchodziła w skład specyfikacji HTML5, ale została z niej wydzielona z mało ciekawych politycznych powodów. Niektórzy producenci przeglądarek nazywają to też „Magazynem lokalnym” (Local Storage) i „Magazynem DOM” (DOMStorage). Kwestię nazw dodatkowo jeszcze komplikują pojawiające się powiązane i podobnie nazwane nowe standardy, o których piszę w dalszej części rozdziału.

Czym jest Magazyn HTML5? Mówiąc najprościej jest to technika umożliwiająca stronom internetowym przechowywanie nazwanych par klucz-wartość w przeglądarce internetowej. Dane te, podobnie jak cookie, nie są usuwane po przejściu przez użytkownika na inną stronę, zamknięciu karty, wyłączeniu przeglądarki itd. Jednak w odróżnieniu od cookie, informacje te nie są wysyłane na serwer (chyba, że zdecydujemy sie to zrobić ręcznie). W odróżnieniu od wcześniejszych prób dostarczenia trwałego lokalnego magazynu, ta technologia jest standardowo zaimplementowana w przeglądarkach, dzięki czemu można z niej korzystać bez instalowania jakichkolwiek wtyczek.

Które przeglądarki ją obsługują? Magazyn HTML5 obsługują najnowsze wersje praktycznie wszystkich przeglądarek… Nawet Internet Explorera!

Obsługa Magazynu HTML5
IEFirefoxSafariChromeOperaiPhoneAndroid
8.0+3.5+4.0+4.0+10.5+2.0+2.0+

Z poziomu JavaScriptu dostęp do Magazynu HTML5 można uzyskać poprzez obiekt localStorage globalnego obiektu window. Zanim się go użyje, należy sprawdzić czy jest obsługiwany przez przeglądarkę.

Sprawdzanie obsługi Magazynu HTML5

function supports_html5_storage() {
  try {
    return 'localStorage' in window && window['localStorage'] !== null;
  } catch (e) {
    return false;
  }
}

Zamiast pisać tę funkcję własnoręcznie, do sprawdzania obsługi Magazynu HTML5 można użyć biblioteki Modernizr.

if (Modernizr.localstorage) {
  // własność window.localStorage jest dostępna!
} else {
  // brak standardowej obsługi Magazynu HTML5 :(
  // możesz spróbować użyć dojox.storage albo innego zewnętrznego rozwiązania
}

Używanie Magazynu HTML5

Obsługa Magazynu HTML5 polega na zapisywaniu i odczytywaniu nazwanych par klucz-wartość. Dane zapisuje się pod kluczem o określonej nazwie, a potem można je z powrotem pobrać przy użyciu tego klucza. Nazwa klucza jest łańcuchem. Dane mogą być dowolnego typu JavaScript, czyli mogą to być łańcuchy, wartości logiczne, liczby całkowite i liczby zmiennoprzecinkowe. Chociaż w istocie i tak informacje są zapisywane jako łańcuchy. Jeśli chcesz zapisać coś innego niż łańcuch znaków, musisz użyć funkcji typu parseInt() lub parseFloat(), aby zamienić pobrane dane na odpowiedni typ danych JavaScript.

interface Storage {
  getter any getItem(in DOMString key);
  setter creator void setItem(in DOMString key, in any data);
};

Wywołanie funkcji setItem() z już istniejącą nazwą klucza spowoduje nadpisanie jego aktualnej wartości bez ostrzeżenia. Wywołanie funkcji getItem() z nieistniejącym kluczem spowoduje zwrócenie null, a nie zgłoszenie wyjątku.

Obiekt localStorage, jak wszystkie obiekty w JavaScript, można traktować jako tablicę asocjacyjną. Dlatego zamiast metod getItem() i setItem() można też używać kwadratowych nawiasów. Przykładowo poniższy kod

var foo = localStorage.getItem("bar");
// ...
localStorage.setItem("bar", foo);

…można następująco zapisać przy użyciu nawiasów kwadratowych:

var foo = localStorage["bar"];
// ...
localStorage["bar"] = foo;

Istnieją też metody do usuwania wartości wybranych kluczy i kasowania zawartości całego magazynu (tzn. usuwania wszystkich kluczy i wartości na raz).

interface Storage {
  deleter void removeItem(in DOMString key);
  void clear();
};

Wywołanie metody removeItem() z nieistniejącym kluczem nie wywoła żadnego efektu.

Istnieje też własność pozwalająca sprawdzić liczbę wartości w magazynie oraz iteracyjne przejrzenie wszystkich kluczy wg indeksów (w celu pobrania ich nazw).

interface Storage {
  readonly attribute unsigned long length;
  getter DOMString key(in unsigned long index);
};

Jeśli wywoła się metodę key() z indeksem o wartości nie należącej do przedziału 0–(length-1), to zwróci ona null.

Śledzenie zmian w Magazynie HTML5

Zmiany w magazynie można śledzić wykorzystując zdarzenie storage. Zdarzenie to jest wyzwalane na obiekcie window, gdy wywołana zostaje metoda setItem(), removeItem() lub clear() i efektem tego wywołania jest jakaś zmiana. Przykładowo, jeśli ustawimy element na taką samą wartość, jaką miał albo wywołamy metodę clear(), gdy nie ma żadnych kluczy, zdarzenie storage nie zostanie wyzwolone, ponieważ nic się nie zmieni w magazynie.

Zdarzenie storage jest obsługiwane wszędzie tam, gdzie jest obsługiwany obiekt localStorage, a więc także w przeglądarce Internet Explorer 8. IE 8 nie obsługuje standardu W3C addEventListener (chociaż w IE 9 to naprawiono). W związku z tym, aby skorzystać ze zdarzenia storage, trzeba sprawdzić który mechanizm zdarzeniowy jest obsługiwany przez przeglądarkę. (Jeśli robiłeś to już z innymi zdarzeniami, to możesz pominąć dalszą część tego podrozdziału.) Zdarzenie storage przechwytuje się tak samo, jak każde inne. Jeśli do rejestracji zdarzeń wolisz używać jQuery albo jakiejś innej biblioteki JavaScript, to nie ma problemu.)

if (window.addEventListener) {
  window.addEventListener("storage", handle_storage, false);
} else {
  window.attachEvent("onstorage", handle_storage);
};

Funkcja zwrotna handle_storage zostanie wywołana z obiektem StorageEvent we wszystkich przeglądarkach oprócz Internet Explorera, w którym obiekt zdarzenia jest przechowywany w window.event.

function handle_storage(e) {
  if (!e) { e = window.event; }
}

Teraz zmienna e będzie reprezentowała obiekt StorageEvent mający opisane poniżej własności.

Obiekt StorageEvent
WłasnośćTypOpis
keystringnazwany klucz, który został dodany, usunięty lub zmieniony
oldValuedowolnypoprzednia wartość (teraz nadpisana) lub null, jeśli został dodany nowy element
newValuedowolnynowa wartość albo null, jeśli element został usunięty
url*stringstrona, która wywołała metodę, która z kolei spowodowała tę zmianę
* Uwaga: własność url początkowo nazywała się uri. Niektóre przeglądarki obsługiwały ją jeszcze przed tą zmianą. Dlatego powinno się sprawdzać czy istnieje własność url, a jeśli nie, to również czy istnieje własność uri.

Zdarzenia storage nie można anulować. W funkcji zwrotnej handle_storage nie da się zapobiec zmianie. Zdarzenie to jest po prostu informacją od przeglądarki, że coś się wydarzyło. Nie można temu zapobiec, bo to już się dokonało.

Ograniczenia aktualnych przeglądarek

W części poświęconej historii lokalnych magazynów opartych na zewnętrznych wtyczkach wspomniałem, że każda z tych technologii ma jakieś ograniczenia, np. dotyczące ilości dostępnego miejsca. Właśnie sobie przypomniałem, że nie napisałem jeszcze nic o ograniczeniach standardowego już Magazynu HTML5. Najpierw udzielę odpowiedzi na pytania, a potem podam szczegółowe wyjaśnienia. Odpowiedzi w kolejności od najważniejszej brzmią „5 megabajtów”, „QUOTA_EXCEEDED_ERR” oraz „nie”.

„5 megabajtów” to ilość miejsca, jaką domyślnie otrzymuje każde źródło. Mimo że w specyfikacji HTML5 wartość ta jest tylko zaleceniem, różne przeglądarki zaskakująco ściśle się go trzymają. Należy pamiętać, że dane są przechowywane w formacie łańcuchowym,a nie swoim oryginalnym. Jeśli zapisuje się dużo liczb, różnica w sposobie reprezentacji może mieć znaczenie. Każda cyfra liczby zmiennoprzecinkowej jest osobnym znakiem, co jest różne od standardowej reprezentacji liczb zmiennoprzecinkowych.

QUOTA_EXCEEDED_ERR” to wyjątek zgłaszany po przekroczeniu limitu 5 megabajtów. „Nie” to odpowiedź na automatycznie nasuwające się pytanie: „Czy można poprosić użytkownika o udostępnienie większej ilości miejsca?” W czasie pisania tego tekstu (luty 2011 r.) żadna przeglądarka nie pozwala na poproszenie o dodatkową przestrzeń. W niektórych przeglądarkach (np. Operze) użytkownik może ustawiać ilość pamięci dostępnej dla każdej witryny, ale inicjatywa musi wyjść od użytkownika i nie da się w żaden sposób połączyć tego z aplikacją sieciową.

Magazyn HTML5 w akcji

Zobaczmy, jak można wykorzystać Magazyn HTML5. Przypomnij sobie grę Halma z rozdziału o elemencie canvas. Ma ona jedną wadę: jeśli zamkniemy przeglądarkę w trakcie gry, wszystko stracimy. Dzięki Magazynowi HTML5 można zapisać potrzebne informacje w przeglądarce, aby po jej ponownym uruchomieniu odtworzyć poprzedni stan gry. Tutaj znajduje się działająca demonstracja. Wykonaj kilka ruchów, zamknij kartę, a potem otwórz ją z powrotem. Jeśli twoja przeglądarka obsługuje Magazyn HTML5, na stronie powinien zostać zapamiętany i odtworzony stan gry, włącznie z liczbą wykonanych ruchów, pozycjami pionków oraz tym, który z nich jest aktualnie zaznaczony.

Jak to działa? Za każdym razem, gdy w grze coś się zmienia, wywoływana jest poniższa funkcja:

function saveGameState() {
    if (!supportsLocalStorage()) { return false; }
    localStorage["halma.game.in.progress"] = gGameInProgress;
    for (var i = 0; i < kNumPieces; i++) {
	localStorage["halma.piece." + i + ".row"] = gPieces[i].row;
	localStorage["halma.piece." + i + ".column"] = gPieces[i].column;
    }
    localStorage["halma.selectedpiece"] = gSelectedPieceIndex;
    localStorage["halma.selectedpiecehasmoved"] = gSelectedPieceHasMoved;
    localStorage["halma.movecount"] = gMoveCount;
    return true;
}

Jak widać użyty jest w niej obiekt localStorage do zapisywania czy trwa właśnie jakaś rozgrywka (wartość logiczna gGameInProgress). Jeśli tak, następuje iteracja przez pionki (gPieces — tablica JavaScript) i zapisanie numeru wiersza i kolumny każdego pionka. Następnie zapisywane są dodatkowe informacje o stanie gry, np. to który pionek jest wybrany (gSelectedPieceIndex — liczba całkowita), czy pionek ten znajduje się w środku potencjalnie długiej serii skoków (gSelectedPieceHasMoved — wartość logiczna) oraz jaką liczbę ruchów wykonano do tej pory (gMoveCount — liczba całkowita).

Podczas ładowania strony zamiast funkcji newGame() ustawiającej zmienne na domyślne wartości wywołujemy funkcję resumeGame(). Funkcja resumeGame() sprawdza czy stan gry jest przechowywany lokalnie. Jeśli tak, przywraca zapisane wartości przy użyciu obiektu localStorage.

function resumeGame() {
    if (!supportsLocalStorage()) { return false; }
    gGameInProgress = (localStorage["halma.game.in.progress"] == "true");
    if (!gGameInProgress) { return false; }
    gPieces = new Array(kNumPieces);
    for (var i = 0; i < kNumPieces; i++) {
	var row = parseInt(localStorage["halma.piece." + i + ".row"]);
	var column = parseInt(localStorage["halma.piece." + i + ".column"]);
	gPieces[i] = new Cell(row, column);
    }
    gNumPieces = kNumPieces;
    gSelectedPieceIndex = parseInt(localStorage["halma.selectedpiece"]);
    gSelectedPieceHasMoved = localStorage["halma.selectedpiecehasmoved"] == "true";
    gMoveCount = parseInt(localStorage["halma.movecount"]);
    drawBoard();
    return true;
}

Najważniejszą częścią tej funkcji jest pułapka, o której wspominałem wcześniej, a którą powtarzam jeszcze raz: Dane są przechowywane jako łańcuchy. Jeśli zapiszesz coś innego niż łańcuch, musisz to przekonwertować na właściwy typ. Przykładowo znacznik określający czy trwa właśnie jakaś rozgrywka (gGameInProgress) jest typu logicznego. W funkcji saveGameState() po prostu go zapisaliśmy i w ogóle nie przejmowaliśmy się typem:

localStorage["halma.game.in.progress"] = gGameInProgress;

Ale w funkcji resumeGame() musimy pobraną wartość traktować jako zwykły łańcuch, który trzeba przekształcić w logiczny typ danych:

gGameInProgress = (localStorage["halma.game.in.progress"] == "true");

Analogicznie liczba ruchów jest zapisywana w gMoveCount jako liczba całkowita. W funkcji saveGameState() zapisaliśmy ją tak:

localStorage["halma.movecount"] = gMoveCount;

Ale w funkcji resumeGame() musimy pobraną wartość przekonwertować na typ całkowitoliczbowy za pomocą funkcji JavaScript parseInt():

gMoveCount = parseInt(localStorage["halma.movecount"]);

Świat poza parami klucz-wartość — konkurencyjne koncepcje

Podczas gdy w przeszłości powstawało wiele niezbyt udanych sztuczek i hacków, obecny stan Magazynu HTML5 jest zaskakująco dobry. Nowe API zostało ustandaryzowane i zaimplementowane we wszystkich najważniejszych przeglądarkach, platformach i urządzeniach. Będąc programistą sieciowym nieczęsto jesteś świadkiem takich wydarzeń, prawda? Ale na „5 megabajtach par klucz-wartość” świat się nie kończy i w przyszłości… jak by to powiedzieć… już powstają konkurencyjne koncepcje.

Jedna z nich nosi nazwę, którą na pewno znasz: SQL. W 2007 r. Google uruchomiło Gears, otwartą wtyczkę rozszerzającą możliwości różnych przeglądarek internetowych zawierającą m.in. bazę danych SQLite. Ten wczesny prototyp miał wpływ na kształt specyfikacji Web SQL Database. Web SQL Database (wcześniej zwana WebDB) to cienkie opakowanie dla bazy danych SQL umożliwiające wykonywanie z poziomu JavaScriptu takich działań, jak to:

Kod działający w czterech przeglądarkach

openDatabase('documents', '1.0', 'Local document storage', 5*1024*1024, function (db) {
  db.changeVersion('', '1.0', function (t) {
    t.executeSql('CREATE TABLE docids (id, name)');
  }, error);
});

Jak widać, większość działań odbywa się w łańcuchu przekazywanym do metody executeSql. Łańcuch może zawierać dowolną obsługiwaną instrukcję SQL, wliczając SELECT, UPDATE, INSERT oraz DELETE. Wygląda to jak zwykłe posługiwanie sie bazą danych, tylko że z poziomu JavaScriptu! A niech mnie!

Specyfikacja Web SQL Database jest zaimplementowana w czterech przeglądarkach i platformach.

Obsługa Web SQL Database
IEFirefoxSafariChromeOperaiPhoneAndroid
··4.0+4.0+10.5+3.0+2.0+

Oczywiście każdy kto używał więcej niż jednej bazy danych wie, że SQL to bardziej marketingowe określenie niż konkretny standard. (Niektórzy to samo mówią o „HTML5”, ale nieważne.) Oczywiście istnieje specyfikacja SQL (nosi nazwę SQL-92), ale nie ma na świecie serwera baz danych, który by był w pełni zgodny z nią i tylko z nią. Istnieją zatem SQL Oracle, SQL Microsoftu, SQL MySQL, SQL PostgreSQL i SQL SQLite. W każdym z tych produktów ciągle wprowadzane są nowe udoskonalenia SQL, przez co nawet stwierdzenie „SQL SQLite” nie jest całkiem precyzyjne. Dla pewności najlepiej jest mówić „wersja SQL dostępna w SQLite w wersji X.Y.Z”.

To wszystko doprowadziło, że na początku specyfikacji Web SQL Database umieszczono następującą informację:

Niniejsza specyfikacja znalazła się w impasie: wszyscy zainteresowani implementatorzy używają tego samego silnika SQL (Sqlite), ale do kontynuacji procesu standaryzacji potrzebnych jest więcej niezależnych implementacji. ZDANIE DO USUNIĘCIA

Na tle tych wydarzeń zarysowuje się kolejna wizja utworzenia zaawansowanych trwałych lokalnych magazynów dla aplikacji sieciowych: API Indexed Database, wcześniej zwane „WebSimpleDB”, a teraz występujące pod nazwą „IndexedDB”.

API Indexed Database udostępnia tzw. magazyn obiektowy. Magazyn ten jest podobny pod wieloma względami do bazy danych SQL. Występują w nim „bazy danych” z „rekordami”, z których każdy ma ustaloną liczbę „pól”. Każde pole ma określony typ danych definiowany podczas tworzenia bazy danych. Można wybrać zbiór rekordów i przejrzeć je przy użyciu „kursora”. Zmiany w magazynie obiektowym są obsługiwane przy użyciu „transakcji”.

Dla każdego, kto używał baz danych SQL wymienione pojęcia brzmią znajomo. Najważniejsza różnica w stosunku do baz SQL polega na tym, że magazyn obiektowy nie ma strukturalnego języka. Nie tworzy się instrukcji typu "SELECT * from USERS where ACTIVE = 'Y'". Zamiast tego używa się udostępnianych przez magazyn metod do otwierania na bazie danych kursora o nazwie USERS, przegląda się rekordy, odfiltrowuje się rekordy nieaktywnych użytkowników oraz pobiera się wartości z pozostałych pól za pomocą metod dostępowych. Dobrym wprowadzaniem do IndexDB jest artykuł An early walk-through of IndexedDB. Znajdziesz w nim porównanie IndexedDB Web SQL Database.

W czasie pisania tego tekstu IndexedDB obsługiwała tylko wersja beta Firefoksa 4. (Dla kontrastu Mozilla zapowiedziała, że nigdy nie zaimplementuje Web SQL Database.) Google twierdzi, że rozważa implementację obsługi IndexedDB w Chromium i Google Chrome. Nawet Microsoft twierdzi, że IndexedDB „jest znakomitym rozwiązaniem dla sieci”.

A co ty, programista sieciowy, możesz zrobić z IndexedDB? Chwilowo nic oprócz opracowania demonstracji zastosowania tej technologii. Za rok? Może coś się zmieni. Poniżej znajdziesz listę odnośników do ciekawych artykułów na ten temat.

Lektura uzupełniająca

Magazyn HTML5:

Wczesne prace Brada Neuberga i in. (przed powstaniem HTML5):

Web SQL Database:

IndexedDB:

Autor: Mark Pilgrim

Źródło: http://diveintohtml5.info/

Tłumaczenie: Łukasz Piwko

Treść tej strony jest dostępna na zasadach licencji CC BY 3.0