Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace

Trvalé lokální úložiště je jednou z oblastí, kde měly nativní klientské aplikace (desktopové) výhodu nad webovými aplikacemi. Představme si současné možnosti webových aplikací pro lokální ukládání dat.

Tento článek je překladem kapitoly The Past, Present & Future of Local Storage for Web Applications knihy Marka Pilgrima Dive into HTML5, jejíž komunitní překlad probíhá na HTML5.cz a je šířena pod licencí CC-BY-3.0.

Trvalé lokální úložiště je jednou z oblastí, kde měly nativní klientské aplikace (desktopové) výhodu nad webovými aplikacemi. Nativním aplikacím operační systém obvykle poskytuje abstraktní vrstvu pro ukládání a načítání dat specifických pro aplikace, jako jsou nastavení nebo běhové stavy. Jejich hodnoty mohou být uloženy v registrech, INI souborech, XML souborech, nebo na jiném místě v závislosti na platformě. Pokud vaše desktopová aplikace potřebuje lokální úložiště mimo formu klíč/hodnota, můžete připojit vlastní databázi, vymyslet vlastní souborový formát, nebo využít některé jiné řešení.

Webové aplikace v minulosti neměly takovýto luxus. Cookies byly vynalezeny na začátku webové historie a dokonce je lze použít pro trvalé lokální ukládání malého množství dat. Ale mají tři potenciálně odrazující nevýhody:

  • Cookies jsou součástí každého HTTP požadavku, a tím zpomalují vaši webovou aplikaci zbytečným přenosem stále stejných dat
  • Cookies jsou součástí každého požadavku HTTP a tím odesílají nezašifrovaná data přes internet (pokud není celá webová aplikace nabízena přes SSL)
  • Cookies jsou omezeny na přibližně 4 kB dat – dost na zpomalení aplikace (viz výše), ale ne natolik, aby byly dostatečně užitečné

To, co skutečně chceme je:

  • hodně úložného prostoru
  • na straně klienta
  • který přetrvává i po obnovení stránky
  • a není přenášen na server

Před HTML5, byly všechny pokusy o dosažení těchto cílů neuspokojivé.

Stručná historie hacků lokálního úložiště před HTML5

Na počátku byl jen Internet Explorer. Nebo přinejmenším Microsoft chtěl, aby si to svět myslel. Za tímto účelem, v rámci První Velké Války Prohlížečů, Microsoft vyvinul velké množství inovací a začlenil je do Internet Exploreru, prohlížeče, který měl válku prohlížečů ukončit. Jedna z těchto inovací se jmenuje DHTML Behaviors a jeden z těchto behaviorů (způsobů chování) se jmenuje userData.

userData umožňují webovým stránkám uložit až 64KB na jednu doménu, v hierarchické struktuře založené na XML. (Důvěryhodné domény jako jsou intranetové stránky, umožňují uložit 10krát víc dat. A 640 KB by mělo být dost pro každého.) IE nezobrazuje žádný povolovací dialog a není potřeba povolení na zvýšení úložného místa k využití.

V roce 2002 Adobe představilo ve Flashi 6 funkci, která získala nešťastný a zavádějící název „Flash cookies“. V prostředí Flashe je funkce spíše známá jako místní sdílené objekty (Local Shared Objects). Stručně řečeno, Flash objekt dovolil uložení až 100 KB dat na jednu doménu. Brad Neuberg vyvinul raný prototyp Flash-to-JavaScript můstku nazvaný AMASS (AJAX Massive Storage System), ale ten byl omezen některými návrhovými chybami Flashe. V roce 2006 s příchodem ExternalInterface Flashe 8, se stal přístup k LSO z JavaScriptu řádově jednodušším a rychlejším. Brad přepsal AMASS a integroval ho do populárního Dojo Toolkitu pod názvem dojox.storage . Flash poskytuje každé doméně 100 KB úložistě „zdarma“. Kromě toho vyzve uživatele k povolení každého požadavku na zvýšení prostoru pro ukládání dat (1 MB, 10 MB atd.).

V roce 2007 Google uvedl Gears, open source plugin pro prohlížeče zaměřený na rozšíření schopností prohlížečů. (Gears jsme již dříve zmínili v souvislosti s poskytováním geolokačního API v Internet Exploreru.) Gears poskytuje API pro vestavěnou SQL databázi postavenou na SQLite. Po jednom získání oprávnění od uživatele může Gears ukládat neomezené množství dat za doménu v tabulkách databáze SQL.

Mezitím Brad Neuberg a jiní pokračovali dál ve snaze vyvinout z dojox.storage  jednotné rozhraní pro všechny různé pluginy a API. Od roku 2009 dojox.storage  automaticky detekuje (a současně zastřešuje jednotným rozhraním) Adobe Flash, Gears, Adobe AIR a raný prototyp skladování HTML5, které bylo implementováno pouze ve starších verzích Firefoxu.

Pokud jste prozkoumali tato řešení, objevíte podobnost: všechna řešení jsou buď specifická pro jediný prohlížeč, nebo závislá na pluginu třetí strany. Navzdory heroickému úsilí překlenout rozdíly (v dojox.storage) všechny vystavují radikálně odlišné rozhraní, mají různé skladovací omezení a představují různé uživatelské zkušenosti. To je tedy ten problém a HTML5 ustanovuje řešení: zajistit standardizované API implementované nativně a důsledně ve více prohlížečích, aniž byste museli spoléhat na pluginy třetích stran.

Představujeme HTML5 úložiště

To, co jsem si označil jako „HTML5 Storage“, je specifikací pojmenované Webové úložiště, které bylo součástí základní HTML5 specifikace, ale bylo vyčleněno do vlastní specifikace z nezajímavých politických důvodů. Někteří výrobci prohlížečů na něj také odkazují jako na „Local Storage“ nebo „DOM Storage“. Situace s pojmenováváním se více komplikuje se souvisejícími podobně pojmenovanými nově vznikajícími standardy, které zmíním později v této kapitole.

Takže, co je HTML5 Storage? Jednoduše řečeno se jedná o způsob, jakým mohou webové stránky lokálně ukládat páry klíč/hodnota uvnitř webového prohlížeče. Stejně jako cookies i tato data přetrvávají po opuštění webové stránky, uzavření panelu prohlížeče, ukončení prohlížeče a tak podobně. Na rozdíl od cookies nejsou tato data nikdy přenášena na vzdálený server (pokud je tam nepošlete vy sami). Na rozdíl od všech předchozích pokusů o zajištění trvalého místního úložiště je implementováno nativně ve webových prohlížečích, takže je k dispozici, i když nepoužíváte v prohlížeči pluginy třetích stran.

O které prohlížeče se jedná? HTML5 Storage podporuje poslední verze skoro každého prohlížeče… dokonce i Internet Explorer!

HTML5 Storage podporují
IE Firefox Safari Chrome Opera iPhone Android
8.0+ 3.5+ 4.0+ 4.0+ 10.5+ 2.0+ 2.0+

Z vašeho javascriptového kódu máte přístup do HTML5 Storage přes objekt localStorage v objektu global window. Dříve než jej můžete začít používat, měli byste si ověřit, zda ho prohlížeč podporuje.

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

Než aby jste tuto funkci psali sami, můžete k zjištění podpory HTML5 Storage použít Modernizr.

if (Modernizr.localstorage) {
  // window.localStorage je k dispozici!
} else {
  // žádná nativní podpora pro HTML5 storage :(
  // možná zkuste dojox.storage nebo řešení třetích stran
}

Používání HTML5 Storage

HTML5 Storage je založen na párech klíč/hodnota. Uložíte-li data s pojmenovaným klíčem, pak můžete tato data se stejným klíčem získat. Pojmenovaný klíč je řetězec. Data mohou být jakéhokoliv typu, který JavaScript podporuje, včetně stringů, logických hodnot, celých čísel, nebo desetinných čísel. Nicméně data se vždy uloží jako řetězec. Pokud ukládáte nebo načítáte cokoliv jiného než řetězce, budete muset použít funkce jako parseInt() nebo parseFloat() k převedení načítaných dat do očekávaného JavaScriptového datového typu.

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

Volání setItem() s pojmenovaným klíčem, který již existuje, potichu přepíše přecházející uloženou hodnotu. Volání getItem() s neexistujícím klíčem vrátí null spíše než výjimku.

Stejně jako ostatní JavaScriptové objekty, můžete považovat localStorage objekt jako asociativní pole. Namísto použití metody getItem() a setItem(), můžete jednoduše použít hranaté závorky. Například, tento kousek kódu:

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

…může být přepsán s použitím syntaxe hranatých závorek:

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

Existují rovněž metody pro odstranění hodnoty daného pojmenovaného klíče, a vyčištění celého úložného prostoru (což je odstranění všech klíčů a hodnot najednou).

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

Volání removeItem() s neexistujícím klíčem neprovede nic.

Nakonec je zde vlastnost k získání celkového počtu hodnot v úložišti a k iteraci přes všechny klíče podle indexu (k získání názvu každého klíče).

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

Pokud zavoláte key() s indexem mimo 0–( délka-1), tak funkce vrátí  null.

Sledování změn v HTML5 Storage Area

Pokud chcete programově sledovat změny v prostoru úložiště, můžete odchytávat události storage. Události storage jsou vyhozeny z window  objektu kdykoliv zavoláme setItem(), removeItem(), nebo clear() a skutečně něco změníme. Například, když nastavíte položku na její současnou hodnotu nebo zavoláte clear() pokud v úložišti nejsou žádné pojmenované klíče, tak storage nevyhodí událost, protože se ve skutečnosti nic v úložišti nezměnilo.

Storage událost je podporována všude, kde je podporován objekt localStorage, což zahrnuje i Internet Explorer 8. IE 8 nepodporuje W3C standard addEventListener (i když nakonec bylo přidáno v IE 9). Proto k zachycení událostí storage, musíte zkontrolovat který mechanismus událostí prohlížeč podporuje. (Pokud jste to už dříve dělali s jinými událostmi, můžete přejít na konec této části. Odchytávání událostí storage funguje stejně jako každá jiná událost, kterou jste dříve odchytávali. Pokud upřednostňujete užití jQuery nebo nějaké jiné javascriptové knihovny k obsluze událostí, můžete to s událostmi storage dělat stejně.)

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

Zpětné volání Handle_storage funkce bude voláno s StorageEvent  objektem, výjimkou je Internet Explorer, kde jsou události objektu uloženy ve  window.event.

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

V tomto bodě bude proměnná e objektem StorageEvent, což má tyto užitečné vlastnosti.

Objekt StorageEvent
Vlastnost Typ Popis
key string pojmenovaný klíč, který byl přidán, odstraněn nebo změněn
oldValue jakýkoliv předchozí hodnota (nyní přepsaná), nebo null pokud byla přidána nová položka
newValue jakýkoliv nová hodnota, nebo null pokud byla položka odstraněna
url * string stránka, kterou volala metoda, kterou spouští tato změna
* Poznámka: url vlastnost se původně jmenovala uri. Některé prohlížeče byly vydány s touto vlastností před změnou specifikace. Pro maximální kompatibilitu byste měli zkontrolovat zda url vlastnost podporuje a pokud ne, zkontrolujte podporu vlastnosti uri.

Storage akci nelze zrušit (not cancelable). V rámci zpětného volání funkce handle_storage neexistuje žádný způsob, jak zastavit změnu vyvolání. Je to prostě způsob jak prohlížeč říká „Hej, to se právě stalo. Neexistuje nic, co bys mohl udělat; Já jen chtěl abys to věděl.“

Omezení v současných prohlížečích

Dříve, v části o historii hacků místního úložiště pomocí pluginů třetích stran, jsem se zmínil o omezeních jednotlivých technik, jako například omezení velikosti. Právě jsem si uvědomil, že jsem nezmínil nic o omezeních právě standardizovaného HTML5 Storage. Nejdříve vám dám odpovědi a potom je vysvětlím. Odpovědi jsou, v pořadí podle důležitosti, „5 MB,“ „ QUOTA_EXCEEDED_ERR,“ a „ne.“

„5 MB“ je, úložný prostor, který dostane každý origin ve výchozím nastavení. Překvapivě je to stejné napříč prohlížeči, ačkoli je to ve specifikaci HTML5 Storage formulováno jen jako návrh. Jednu věc je důležité si zapamatovat, a to, že ukládáte řetězce a ne data v původním formátu. Pokud ukládáte hodně celých nebo desetinných čísel, může být rozdíl v prezentaci skutečně velký. Každá číslice desetinného čísla je uložena jako znak, ne jako běžná prezentace desetinného čísla.

QUOTA_EXCEEDED_ERR “ je výjimka, kterou dostanete, pokud překročíte kapacitu 5 MB úložného prostoru. „Ne“ je odpověď na další zřejmou otázku, „Mohu se zeptat uživatele na přidělení většího místa?“ V době psaní (Únor 2011) žádný prohlížeč nepodporuje jakýkoliv mechanismus pro webové vývojáře, který by umožnil dotázání se na zvětšení úložného prostoru. Některé prohlížeče (například Opera) umožňují uživateli ovládat, jak velké úložiště může každá stránka využívat, ale je to čistě uživatelem iniciovaná věc, ne něco, co byste vy jako webový vývojář mohli vestavět do aplikace.

HTML5 Storage v akci

Podívejme se na HTML5 Storage v akci. Připomeňme si hru Halma, kterou jsme vytvořili v kapitole o canvasu. S hrou je však malý problém: pokud zavřete okno prohlížeče uprostřed hry, ztratíte svůj postup. Ale s HTML5 Storage můžeme rozehranou hru uložit lokálně v rámci samotného prohlížeče. Zde je živá ukázka. Zahrajte několik tahů, zavřete kartu prohlížeče a znovu ji otevřete. Pokud váš prohlížeč podporuje HTML5 Storage, ukázková stránka si jako kouzlem zapamatuje stav vaší hry včetně počtu tahů, které jste táhli, pozice každého kamene na desce a dokonce i to, zda byl některý kámen vybrán.

Jak to funguje? Pokaždé, když dojde ke změně ve hře, zavoláme tuto funkci:

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 můžete vidět, pokud je rozehraná hra využíváme k uložení objectu localStorage ( gGameInProgress, logická hodnota). Pokud ano, projde pieces ( gPieces, JavaScript Array) uloží čísla řádku a sloupce pro každého pieces. Poté uloží několik dalších stavů hry, včetně toho, který piece je vybrán ( gSelectedPieceIndex, celé číslo), zda je piece uprostřed potenciálně dlouhé řady skoků ( gSelectedPieceHasMoved, a Boolean), a celkový počet odehraných tahů ( gMoveCount, celé číslo).

Při načtení stránky místo automatického volání funkce newGame(), která by resetovala proměnné na startovní hodnoty, zavoláme funkci resumeGame(). Pomocí HTML5 Storage funkce resumeGame() zkontroluje, zda je lokálně uložen stav o rozehrané hře. Pokud ano, obnoví tyto hodnoty pomocí objektu  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;
}

Nejdůležitější částí této funkce je varování, které jsem zmiňoval dříve v této kapitole, které zde zopakuji: Data jsou uložena jako řetězce. Pokud ukládáte něco jiného než řetězce, budete je muset změnit sami při načítání. Například příznak, zda je hra rozehraná ( gGameInProgress), je logická hodnota. Ve funkci saveGameState() jsme jej pouze uložili a nedělali jsme si hlavu s datatypem:

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

Ale ve funkci resumeGame() je třeba považovat hodnotu, kterou jsme dostali od místního úložiště jako řetězec, a ručně ji přetvořit na správnou logickou hodnotu sami:

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

Stejně tak je počet tahů uložený v gMoveCount  jako celé číslo. Ve funkci saveGameState() jsme jej prostě uložili:

localStorage["halma.movecount"] = gMoveCount;

Ale ve funkci resumeGame() musíme změnit hodnotu na celé číslo pomocí funkce parseInt() vestavěné přímo v JavaScriptu:

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

Za hranicemi párů klíč – hodnota: soupeření vizí

Zatímco minulost je plná hacků a zástupných řešení, současný stav HTML5 Storage je překvapivě růžový. Nové API je standardizováno a implementováno ve všech hlavních prohlížečích, platformách a zařízeních. Pro webového vývojáře to je prostě něco, co není vidět každý den, že? Ale existuje něco důležitějšího než „5 megabajtů párů klíč/hodnota,“ a budoucnost trvalého lokálního úložiště je… jak bych to… no, existuje více soupeřících vizí.

Jednou z vizí je zkratka, kterou už asi znáte : SQL. V roce 2007 Google spustil Gears, open source plugin pro většinu prohlížečů, který zahrnoval vestavěnou databázi založenou na SQLite. Tento časný prototyp měl později vliv na vytvoření specifikace Web SQL Database. Web SQL Database (dříve známý jako „WebDB“) poskytuje tenký obal kolem SQL databáze, která vám umožní dělat věci jako je tato z JavaScriptu:

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

Jak vidíte většina akce spočívá v řetězci, který předáte metodě executeSql. Tento řetězec může být jakýkoliv podporovaný SQL, včetně SELECT, UPDATE, INSERT a DELETE. Je to jako klasické programování databází, kromě toho, že to děláte z JavaScriptu! Jaká radost!

Specifikace Web SQL Database byla implementována do čtyř prohlížečů a platforem.

Podpora Web SQL Database
IE Firefox Safari Chrome Opera iPhone Android
· · 4.0+ 4.0+ 10.5+ 3.0+ 2.0+

Samozřejmě, pokud jste v životě použili více než jeden databázový produkt, jste si vědomi, že SQL je víc termín marketingu než tvrdého-a-rychlého standardu. (Někdo by mohl říct totéž o „HTML5,“ ale to nevadí.) Jistě, je zde aktuální specifikace SQL (jmenuje se SQL-92), ale na světě není databázový server, který odpovídá této a pouze této specifikaci. Máme Oracle SQL, Microsoft SQL, MySQL SQL, PostgreSQL SQL, a SQLite SQL. Opravdu, každý z těchto produktů v průběhu doby přidával nové SQL funkce, takže říkat „SQLite SQL“, nestačí, abychom upřesnili, o čem mluvíme. Musíte říct: „verze SQL dodaná s SQLite verze X.Y.Z.“

Toto nás přivádí k následujícímu prohlášení, v současné době uvedenému v horní části specifikace Web SQL Database:

Tato specifikace se dostala do slepé uličky: všichni zaiteresovaní implementátoři použili stejný SQL (Sqlite), ale je potřeba více nezávislých implementací, aby bylo možno pokračovat ve standardizační cestě. Dokud jiný implementátor nebude mít zájem implemnentovat tuto specifikaci, je popis dialektu SQL ponechán jednoduše jen jako odkaz na SQLite, což pro standard není přijatelné.

Vzhledem k těmto okolnostem vám přestavím další vizi pro pokročilé trvalé lokalní úložiště pro webové aplikace: API Indexované Databáze, dříve známé jako „WebSimpleDB“, nyní důvěrně známé jako „IndexedDB“.

API Indexované Databáze odhalují to, co se nazývá object store. Object store sdílí mnoho konceptů s databází SQL. Jsou zde „databáze“ se „záznamy“, a každý záznam má stanovený počet „polí.“ Každé pole má specifikovaný datový typ, který je definován při vytváření databáze. Můžete si vybrat podmnožinu záznamů, poté je vypočítávat „kurzorem“. Změny v object store jsou řešeny v rámci „operace“.

Pokud jste někdy dělali databázové SQL programování, tak vám tyto termíny pravděpodobně znějí povědomě. Hlavní rozdíl je v tom, že object store nemá žádný strukturovaný dotazovací jayzk. Nevytváříte dotazy typu "SELECT * from USERS where ACTIVE = 'Y'". Místo toho můžete použít metodu uvedenou v object store otevření kurzoru nad databází s názvem „ USERS “, projít skrz záznamy, vyfiltrovat pryč záznamy s neaktivními uživateli a použít přístupovou metodu k získání hodnot každého ze zbývajích záznamů. Dřívější průvodce po IndexedDB je dobrý tutorial o fungování IndexedDB porovnávající IndexedDB a Web SQL Database.

V době psaní této knihy je IndexedDB implementováno pouze v beta verzi Firefox 4. (Navzdory tomu, že Mozilla říkala nikdy nebudeme implementovat Web SQL Database.) Google uvedl, že uvažuje o podpoře IndexedDB pro Chromium a Google Chrome. A dokonce i Microsoft říkal, že IndexedDB „je skvělé řešení pro web.“

Co tedy můžete jako webový vývojář dělat s IndexedDB? V současné době prakticky nic jiného než nějaké technologické demo. Ode dneška za rok? Možná něco. Podívejte se na „Další Čtení“, sekce s linky, na některé dobré tutorialy jak začít.

Další čtení

HTML5 storage:

Raná práce Brad Neuberga et. al. (pre-HTML5):

Web SQL Database:

IndexedDB:

Zbytek právě probíhajícího komunitního překladu knihy Marka Pilgrima Dive into HTML5 najdete na HTML5.cz.

Autor si přivydělává tvorbou webových stránek, má rád sci-fi a fantasy.

 

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 24

Přehled komentářů

Rum Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
aaa Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Martin Hassman Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Jiří Kosek Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Martin Hassman Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
okbob Poznamka - aktuální specifikace SQL
MP File API?
Martin Hassman Re: File API?
bauglir Re: File API?
vetesnik Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Kdyby jenom Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Martin Hassman Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
vetesnik Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Martin Hassman Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
vetesnik Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
pedant Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Martin Hassman Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
pedant Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Martin Hassman Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
j Re: Minulost, současnost a budoucnost lokálního úložiště pro HTML5 aplikace
Johnny Huh?
Martin Hassman Re: Huh?
Pilgrim HTML 5?
Martin Hassman Re: HTML 5?
Zdroj: https://www.zdrojak.cz/?p=3626