OffTheRoad: Píšeme offline aplikaci s HTML5

V posledních týdnech jsme se setkávali na Zdrojáku u článků o technologiích počítaných do rodiny HTML5. Ukázali jsme si, jak HTML5 napomáhá sémantice, probrali jsme nové možnosti videa a audia a ukázali jsme si i některá API, s nimiž přichází moderní prohlížeče. Je tedy vhodný čas si to vše shrnout.

Úvodní upozornění,
v němž si na své přijdou terminologičtí puristé

HTML5 je buzzword. Nemá smysl o tom polemizovat, je to víceméně tak, jak popisují pánové ze Sencha Labs: když Apple a Google oznámili podporu pro HTML5, stal se z tohoto označení dokonce buzzword měsíce: HTML5 útočí, HTML5 zajistí, HTML5 nastupuje, Apple s HTML5 vytlačuje Flash, kdo nemá HTML5 ve své ledničce, je totálně out a zemře v důsledku požívání prošlých potravin! Odborníci vědí, že nic se nejí tak horké jak se to navaří, takže se podobnými titulky dílem baví, dílem žehrají na jejich nesolidnost.

Nástup HTML5 je dlouhodobý proces, který započal před několika lety, a další dva roky (minimálně) bude probíhat, takže není potřeba bezhlavě zahodit všechny současné weby a předělat je na HTML5, ale na druhou stranu je dobré si uvědomit, že i když koleje ještě neleží a nedá se po nich jezdit plnou parou, tak cesta je vyšlapána a už zhruba víme, kudy povedou.

V rámci diskusí o HTML5 by neměla zapadnout jedna věc, na kterou jsme v seriálu poukazovali už v nultém díle (a zmiňuje to i výše odkazovaný článek), totiž že se pod označení „HTML5“ schovala celá škála specifikací a technik, které většinou nemají s HTML jako takovým, tedy se značkovacím jazykem pro hypertext, skoro nic společného. Samotné („opravdové“) HTML5 specifikuje značky, které můžete použít v HTML souborech, jejich význam a syntax. Zároveň s implementací těchto značek v prohlížečích ale přichází další technologie (geolokace, storages, Web Sockets, Web Workers), které stojí někde na rozhraní mezi prohlížečem a JavaScriptem, ale o kterých se taky často mluví jako o „součásti HTML5“ (některé jimi opravdu byly). Do toho přichází i novinky v CSS3 – animace, transformace apod., které jsou, aby bylo zmatení úplné, také někdy počítány do „tohohle HTML5“. Již odkazovaný Michael Mullany nabádá, a v tomto se k němu připojuji, k odlišování užšího významu označení HTML5 coby samotného značkovacího jazyka – „jádra“ (core), a širšího, kdy označuje i „CSS3“ a další moderní webové technologie. V tom širším významu navrhuje používat termín „technologie z rodiny HTML5“, a tohoto zpřesňujícího označení se budeme snažit držet i my na Zdrojáku.

Dosti terminologie – pojďme přímo k ukázce toho, co lze s těmi „technologiemi z rodiny HTML5“ udělat – ne že by jich nebyl plný web. V naší ukázce nebudeme dělat žádné spektakulární vylomeniny s transformacemi videa při přehrávání (to nechám až na Honzovi Sládkovi), nebudeme se pouštět do žádnejch větších akcí, jen si na jednoduchém příkladu ukážeme použití některých nových JavaScriptových A­PI.

Budujeme aplikaci
a začínáme dojemným příběhem

Za každým správným a úspěšným webem je příběh. Za Facebookem ten o chybě, která se ukázala jako velmi šťastná, když umožnila vytvářet externí aplikace, za Google je příběh o setkání dvou mladých krásných chudých studentů, a tak dál… I za naším ukázkovým webem bude hluboký lidský příběh jak ze společenského magazínu:

Když jsem odjížděl na dovolenou, začíná svůj příběh pan Martin, požádal mě Michal BláhaOnTheRoad.to, jestli bych nevyzkoušel jejich webovou aplikaci pro posílání zápisků z cest. Slíbil jsem mu, že se vynasnažím, jak jen to bude možné. Protože pro Nokii N900, kterou jsem používal, nemají v OTR nativní aplikaci, musel jsem používat tu webovou. Věděl jsem, že to na dovolené nebude s internetem jednoduché, ovšem realita předčila i nejčernější sny: Funkční WiFi připojení jsme po celé cestě nenašli ani jednou, datové připojení přes GPRS bylo v roamingu nehorázně drahé, a s místní předplacenou kartou vystačilo tak na tři stažení. Navíc se nám běžně stávalo, že jsme se několik dní po sobě pohybovali v oblastech naprostého rádiového ticha, kdy nehrálo ani autorádio a o GSM signálu jsme si mohli nechat jen zdát… Bylo by fajn mít takovou aplikaci, kam bych mohl zadávat poznámky z různých míst, i když zrovna nejsem připojen, a odeslat je najednou…

Třeba zde není žádný GSM signál. Ani kdekoli jinde v okruhu 300 kilometrů...

Třeba zde není žádný GSM signál. Ani kdekoli poblíž v okruhu 300 kilometrů…

Dojemné, že? Vsadil bych se, že nativní aplikace, kterou OnTheRoad.to mají pro některé vybrané typy mobilů, přesně tohle umožňuje zcela bez problémů – ale nemohu to ověřit; nemám podporovaný stroj. Přemýšlel jsem, zda by bylo možné alespoň částečně podobnou funkčnost zajistit pomocí technologií z rodiny HTML5, a dospěl jsem k názoru, že určitě ano, a že by to nemuselo být ani tak těžké. Pojďme si to projít:

Analýza problému,
spojená s rozpiskou materiálu

Potřebujeme aplikaci, která bude posílat text (omezíme se jen na zprávy, vynecháme multimedia) spolu s informací o naší poloze. Tu zjistíme díky geolokačnímu API. Takže nám stačí formulář, u formuláře okénko na text a odesílací tlačítko. Po odeslání si aplikace vezme text a zjištěnou pozici a pošle ji AJAXem na server.

Chceme ale, aby tohle všechno fungovalo i bez připojení. Zjištění pozice je na prohlížeči – přinejhorším si sáhne pro data z GPS, a nestojíme-li zrovna někde v tunelu, tak nás najde. Potřebujeme, aby prohlížeč dokázal otevřít komplet stránku a nesahal pro ni kamsi na servery – to zajistí AppCache. No a konečně potřebujeme nějaké úložiště pro zprávy, kam si je budeme ukládat, když je nebude možné poslat. Použijeme WebStorage – a je to!

Celou aplikaci pojmeme jako jednoduchou HTML stránku, v ukázce směle vynecháme stylování, takže to bude vypadat trošku jako příspěvek do CSS Naked Day, ale jde nám o technologickou demonstraci. Takže jedno prosté textové pole, jedno tlačítko.

Aby to bylo trošku pohodlnější, tak si někde zobrazíme informaci o tom, jestli jsme zrovna online nebo offline, a zjišťování polohy uděláme na ruční pokyn. Ve své vlastní aplikaci si pak můžete dodělat automatické aktualizace polohy a další vymoženosti, my si jen ukážeme základní funkce. Tedy vzhůru do akce!

Vlastně ne! Nemůžeme začít, dokud nemáme název… Navrhuji, a Michal Bláha se na mne jistě nebude zlobit, pojmenovat naši technologickou ukázku jménem, které bude vyjadřovat jednak offline charakter, jednak inspiraci jeho službou. Budiž tedy zvána OffTheRoad!

Krok první
v němž vznikne HTML kostra

Co potřebujeme pro HTML, to jsme si vlastně už řekli, takže můžeme směle začít kostrou:

<!DOCTYPE html>
<html lang="cs">
<head>
    <meta charset="utf-8">
    <title>OffTheRoad</title>
    <meta name="description" content="Demonstrace offline aplikace">
    <link rel="stylesheet" href="css/main.css" media="all">
    <script src="js/otr.js"></script>
</head>
<body onload="ready()">
<section>
    <header>
        <h1>OffTheRoad</h1>
    </header>
          
    <article>
    </article>
          
    <footer>          
        <p>Demo pro <a href="https://www.zdrojak.cz">Zdrojak.cz</a></p>          
    </footer>
</section>
</body>
</html>

Sami vidíte: žádné triky a čáry, jen standardní HTML5 pahýl. Máme připraveno načtení stylu ze souboru main.css, javascriptu z otr.js, a máme i ošetřené vyvolání funkce po načtení stránky. Připravili jsme si hlavičku a patičku a to hlavní, co teprve uděláme, vložíme do elementu  article.

Ale předtím, než budeme něco vkládat, tak ošetříme jednu důležitou věc – zjistíme, jestli prohlížeč má vůbec zapnutý JS a jestli podporuje potřebné technologie. Použijeme standardní fígl, kde se část „pouze pro JS“ uzavře do elementu, který je schovaný, a v jiném elementu zobrazujeme upozornění. Po načtení stránky pomocí JavaScriptu první element zobrazíme a druhý skryjeme. Takže si HTML upravíme takto:

  <article id="js">
... sem to přijde ...
  </article>
  <article id="nojs">
    <p>Váš prohlížeč má vypnutý JavaScript nebo nepodporuje některou z potřebných technologií.</p>
  </article>

a do souboru otr.js zapíšeme:

function ready() {
  //test prohlizece
  if (!navigator.geolocation || !(('localStorage' in window) && window['localStorage'] !== null) || !window.applicationCache || !window.JSON) {
    var e = document.getElementById('js');
    e.style.display="none";
  } else {
    var e = document.getElementById('js');
    e.style.display="block";
    e = document.getElementById('nojs');
    e.style.display="none";
    // Sem přijdou další funkce, které se mají provést po načtení
  }
}

Funkce ready() se zavolá po načtení stránky a zkontroluje, je-li dostupné geolocation API ( navigator.geolocation), je-li dostupné localStorage ( ('localStorage' in window) && window['localStorage'] !== null), jestli existuje podpora pro AppCache ( window.applicationCache), a nakonec i podporu pro jednu věc, o níž jsme se zatím nebavili, a tou je JSON API, které nám umožní snadno převádět objekty na JSON a zpět.

Aby byl trik opravdu působivý a fungoval, jak chceme, zapíšeme do main.css jednoduché pravidlo:

#js {display:none;}

čímž způsobíme, že element s ID „js“ bude po načtení neviditelný.

Tím je kostra hotová, můžete si ji vyzkoušet. Pokud ve vašem prohlížeči proběhnou patřičné testy, uvidíte… tedy vlastně neuvidíte nic, a to je dobré znamení. Pokud se vám ukáže, že není podporováno, použijte plnohodnotný prohlížeč, hodný roku 2010.

Krok druhý
v němž zjistíme, kde jsme

Pravidelní čtenáři už znají geolokační API a umí jej používat. My sáhneme k učebnicové implementaci a zařídíme si zjišťování pozice tím nejjednodušším způsobem. Nejprve si připravíme místo v HTML, kde si budeme zobrazovat pozici a kde bude tlačítko pro její zjištění. A protože jde už o funkci, která bude fungovat jen s JavaScriptem, vložíme si ji do příslušného elementu   <article id="js">:

<p>Pozice: <span id="loc">zjišťuju</span> <button onclick="getpos()">obnovit</button></p>

Implementace v JavaScriptu bude rovněž přímočará:

//globální proměnná s pozicí
var loc=null;
//callback pro getCurrentPosition - uloží do loc a zobrazí
function showPosition(position) {
  var e = document.getElementById('loc');
  e.innerHTML = ('[' + position.coords.latitude + ' ; ' + position.coords.longitude + ']');
  e.style.backgroundColor='#FFFFFF';
  loc = {lat: position.coords.latitude, lon:position.coords.longitude};
}
//zjistí současnou pozici
function getpos() {
  navigator.geolocation.getCurrentPosition(showPosition);
  var e = document.getElementById('loc');
  e.innerHTML='Zjišťuju';
  e.style.backgroundColor='#FFCCCC';
}

Globální proměnná loc se nám bude hodit, až budeme posílat zprávy – přibalíme k nim i pozici. Funkce getpos() spustí zjišťování pozice (a upozorní uživatele na to, že se zjišťuje), callback showPosition() uloží zjištěné souřadnice do proměnné loc a zobrazí je na stránce. Volání funkce getpos() můžeme vložit i do funkce ready(), aby se pozice zjistila automaticky po startu.

Krok třetí
kde konečně pošleme zprávu!

Uděláme si na to zase patřičný HTML kód, který přijde do stejného elementu <article> jako předchozí kód:

    <input type="text" id="msg">
    <button onclick="send()">Poslat</button>

a ten obsloužíme nějakým hezkým JavaScriptovým kódem:

function send(){
  var e = document.getElementById('msg');
  var time = Math.round(new Date().valueOf() / 1000);
  var msg = {loc: loc, msg: e.value, timestamp: time};
  sendMessage(msg);
}
//simulujeme AJAX
function sendMessage(msg) {
  alert('AJAX: '+JSON.stringify(msg));
}

Ve funkci send() si složíme zprávu – objekt z vlastního textu, pozice a timestampu. Zprávu pak pošleme na server – tedy bychom poslali, ale to není pro naši ukázku podstatné, takže si jen vyhodíme alert, aby bylo vidět, že se něco děje. Je to krásné, funguje to, hurá… a teď to ještě připravit tak, aby to fungovalo i v offline módu!

Krok čtvrtý
do offlinova

Kdo četl článek o WebStorage, ten ví, jak si ověřit, jestli je prohlížeč online nebo offline. Uděláme si nejprve takovou kontrolku, která nám ukáže, jak na tom jsme:

<p>Stav: <span id="line">Online</span></p>

Umístíme ji hned pod zjišťování pozice. Stav naší aplikace si zobrazíme JavaScriptovou funkcí:

function lineStatus() {
  var e = document.getElementById('line');
  if (navigator.onLine) {
    e.innerHTML='Online';
    e.style.backgroundColor='#FFFFFF';
  } else {
    e.innerHTML='Offline';
    e.style.backgroundColor='#CCCCCC';   
  }
}

Funkci lineStatus() zavoláme ve funkci ready(), aby se stav zobrazil hned od počátku. Budeme ji volat také při každé změně stavu, takže si ve funkci ready() nadefinujeme obsluhu příslušných událostí – ovšem neuděláme to přímo, ale s malou odbočkou přes vyhrazené funkce. Proč, to si ukážeme za chvilku:

function online() {
  lineStatus();
} 
function offline() {
  lineStatus();
} 
function ready() {
  //...test prohlizece
         ( viz výše ......... )
    // Sem přijdou další funkce, které se mají provést po načtení
    //prohlizec je OK, takze pripojime obsluhu udalosti online a offline
    document.body.addEventListener('online', online, false);
    document.body.addEventListener('offline', offline, false);
    document.body.ononline = online;
    document.body.onoffline = offline;
 
    //a na uvod si zjistime pozici
    lineStatus();
    getpos();
  }
}

Pauza
v níž se zamyslíme nad tím, jak to bude dál

Tak, aplikace posílá zprávy s pozicí, když je online, a dokáže poznat, když je offline. Co se ale přesně má stát, když bude offline?

Pokud je offline a uživatel přesto pošle zprávu, měla by se uložit někam do nějaké fronty, odkud se ve chvíli, kdy přejde do online, pošle na server. Znamená to tedy ošetřit posílání zpráv – ve stavu online normálně posílat, ve stavu offline poslat do fronty.

Frontu implementujeme jako pole. Do pole si budeme nové zprávy vkládat pomocí push(), vybírat je budeme pomocí shift(). Nejsnazší by bylo definovat ji jako globální proměnnou var queue=[], ale pak bychom o ni se zavřením prohlížeče přišli, což nechceme. Budeme si ji tedy ukládat do lokálního úložiště.

Vypadá to jednoduše, ale má to háček, který se ukázal, když jsem vše testoval – takové pole plné objektů, uložené do storage, se po vyjmutí ze storage stane objektem Object. Žádné řešení a obcházení (jako vložení pole do objektu) se neosvědčilo, takže nakonec pole před uložením převádím na textovou reprezentaci JSON a vice versa. Není vše s moderními technologiemi tak přímočaré, jak se z článků a specifikací zdá.

Frontu vyčistíme ve chvíli, kdy přijde událost online – proto jsme si prve jako obsluhu událostí neregistrovali přímo lineStatus, ale „mezikroky“ – funkce online a offline. A právě ve funkci online, která je zavolaná po přechodu z offline do online, si zavoláme nějaké to „vysypání přichystaných zpráv z fronty“.

Krok pátý
implementační

Když máme rozmyšleno, tak je implementace hračka (nevěřili byste, kolik času takové rozmyšlení ušetří…) Nejprve upravíme funkci send():

function send(){
  var e = document.getElementById('msg');
  var time = Math.round(new Date().valueOf() / 1000);
  var msg = {loc: loc, msg: e.value, timestamp: time};
  if (navigator.onLine) {
    sendMessage(msg);
  } else {
    queueMessage(msg);
  }
}

Pak si napíšeme funkce load() a save() pro uložení a načtení fronty do, resp. z localStorage:

function save(queue) {
  localStorage['queue'] = JSON.stringify(queue);
}
function load() {
  return JSON.parse(localStorage['queue']) || [];
}

a nakonec implementujeme frontu – bez velkých triků, neefektivně a naivně – v reálné aplikaci bych pravděpodobně zvolil jiný způsob, ale zde jde spíš o ukázku než o triky:

function queueMessage(msg) {
  var queue = load();
  queue.push(msg);
  save(queue);
  msgcntUpdate();
  alert('Uloženo...');
}
function dequeue() {
  var queue = load();
  while(queue.length) {
    sendMessage(queue.shift());
    save(queue);
    msgcntUpdate();
  }
}

Proti původní úvaze jsem přidal vypsání informace o tom, že zpráva byla uložena, a taky volání funkce msgcntUpdate(), která – jak už název napovídá – aktualizuje počítadlo zpráv ve frontě. Vypadá takto:

function msgcntUpdate() {
  var q = load();
  var e = document.getElementById('msgcnt');
  e.innerHTML = q.length;
}

a odpovídající HTML kousek:

<p>Zpráv k odeslání: <span id="msgcnt">0</span></p>

Pokud jsme nikde neudělali chybu, tak by vše mělo fungovat. Otestujte – aplikace by měla po vytažení síťového kabelu (vypnutí WiFi, odpojení modemu, …) dál ukládat zprávy do fronty, a po opětovném připojení je postupně poslat na server. Tedy vlastně vyhodit alerty. Když tomu tak je (testoval jsem na FF3.6 a na prohlížeči v telefonu Nokia N900), tak můžeme zvolat opatrné Hurá, protože nás čeká už jen poslední část.

Krok poslední
takříkajíc Manifestační

Jak jsme si říkali v článku o AppCache, pravá a plnohodnotná offline aplikace vystaví svůj cache manifest, kde řekne, co všechno se má uložit do AppCache, a tento manifest musí mít MIME typ text/cache-manifest. Pokud se vám nechce šachovat s nastavením serveru nebo nemůžete nastavit MIME typ v .htaccess, můžete použít jednoduchý trik a poslat hlavičku např. pomocí PHP:

<?php header("Content-Type: text/cache-manifest");
?>CACHE MANIFEST
# v1
CACHE:
index.html
css/main.css
js/otr.js

V manifestu určíme, že se má cachovat vlastní stránka, CSS a soubor se skriptem. Teď zbývá už jen v HTML stránce říct, že má nějaký manifest – odkaz tedy vložíme do tagu html:

<html manifest="manifest.php" lang="cs">

A je to.

Opravdu?

Opravdu! Teď máme offline aplikaci, která funguje tak, jak jsme si naznačili v úvodu. Kdo nevěří, ať tam běží.

Pár poznámek

Při vývoji offline aplikací v HTML5 zjistíte několik věcí. Některé jsou příjemné a vzbudí ve vás optimismus a nadšení, ale často se dozvíte něco, co jste ani vědět nechtěli, ale museli to zjistit.

Například zjistíte, že prohlížeče, které podporují AppCache, si její podporu občas vykládají svérázně. Nebo že některému prohlížeči stačí říct „Teď budeš offline“, ale jiný si uvědomí, že mu data netečou, až když je chce poslat a zjistí, že to nejde. Atakdále… Zkrátka nesmíme zapomínat, že pracujeme s horkými novinkami, a protentokrát výjimečně hledat chyby nejen u sebe.

Poznámku na samostatný odstavec si zaslouží AppCache, resp. ladění manifestu. Popsal to velmi pěkně Mark Pilgrim ve svém článku, a už název podkapitoly „Kill me! Kill me now!“ naznačuje, jak se to s laděním má. Abych vám ušetřil práci, tak vám rovnou prozradím, že nejlepší je dělat manifest až úplně nakonec a pak už nic neměnit, protože prohlížeče na manifest rády aplikují cachovací pravidla a servery zase rády vrací „304 Not Modified“, protože jej považují za statický soubor. Navíc pokud prohlížeč má soubory v AppCache, tak nekontroluje, jestli se náhodou nezměnily na serveru, ale bere ty z cache. Pokud tedy změníte např. HTML, musíte k tomu dodat nový manifest. Nový zde znamená „odlišný než předchozí“. Proto je v tom našem použita poznámka # v1, kterou použijeme v případě potřeby jako číslo revize. Když budeme chtít vynutit zahození staré cache a načtení nové, zvýšíme číslo o 1, čímž to bude prohlížeč brát jako nový manifest a jako signál, že má načíst cache znovu. Pokud tedy nezapomeneme nastavit cachování souboru manifest; pokud zapomeneme, bude aplikace několik hodin vzdorovat, a když už ze zoufalství přepíšete všechno možné, tak jakoby zázrakem začne fungovat – to proto že prohlížeč konečně dostal čerstvý manifest. True story.

K duševnímu zdraví vývojáře rovněž nepřispěje zjištění, co způsobí překlep ve jménu souboru v manifestu. Prohlížeč takový soubor nenajde (logicky), ale nijak to neoznámí. Aplikace funguje bez problémů, ale v cache není. Nebo je v cache pořád stará verze. Pokud třeba přidáte k aplikaci, řekněme, obrázek a uklepnete se v názvu souboru v manifestu, tak můžete strávit několik velmi plodných okamžiků pátráním, proč se aplikace nezměnila, proč je pořád zobrazována původní verze HTML, proč prohlížeč nereaguje ani na změnu revize manifestu, a proč nereaguje ani když posílaný manifest stoprocentně není na serveru cachovaný…

Přes to všechno jsou offline HTML aplikace zajímavou alternativou k nativním, hlavně pro mobilní zařízení.

Doporučené čtení: např. Offline Web Applications od MDC či podobný příklad na použití WebStorage od NetTuts+

Poznámka

Aplikaci najdete v našich ukázkách: OffTheRoad demo. Můžete si ji stáhnout zabalenou: otr.zip. Pokud si ji chcete vyzkoušet z mobilního telefonu a nechce se vám opisovat celou dlouhou adresu, můžete, pokud to váš telefon umožňuje, použít QR kód s URL.

https://www.zdrojak.cz/wp-content/uploads/ukazka/otr

Začal programovat v roce 1984 s programovatelnou kalkulačkou. Pokračoval k BASICu, assembleru Z80, Forthu, Pascalu, Céčku, dalším assemblerům, před časem v PHP a teď by rád neprogramoval a radši se věnoval starým počítačům.

Komentáře: 27

Přehled komentářů

Radek Hulán hezký článek
heptau Na iPhone to bohuzel nefunguje
Martin Malý Re: Na iPhone to bohuzel nefunguje
juraj javascript
Martin Malý Re: javascript
juraj Re: javascript
David Grudl Re: javascript
Martin Malý Re: javascript
David Grudl Re: javascript
Martin Malý Re: javascript
David Grudl Re: javascript
Martin Malý Re: javascript
David Grudl Re: javascript
Martin Malý Re: javascript
David Grudl Re: javascript
Daniel Steigerwald Re: javascript
Daniel Steigerwald Re: javascript
PetrP Re: OffTheRoad: Píšeme offline aplikaci s HTML5
Kentusák Re: OffTheRoad: Píšeme offline aplikaci s HTML5
Segeda N900 a offline aplikace
Martin Malý Re: N900 a offline aplikace
Segeda Re: N900 a offline aplikace
Martin Malý Re: N900 a offline aplikace
HuB Díky za článek
George Funkcia online()
Martin Malý Re: Funkcia online()
George Re: Funkcia online()
Zdroj: https://www.zdrojak.cz/?p=3298