2011年3月8日火曜日

HTML 5 を使ってモバイル Web アプリケーションをオフラインで動作させる

前提条件

この記事では、最新の Web 技術を使用して Web アプリケーションを作成します。ここで紹介するコードの大部分は単なる HTML と JavaScript、そして CSS であり、すべての Web 開発者にとってコアとなる技術です。記事の内容に従うために必要なもののうち、最も重要なものはテストを実行する際に使用するブラウザーです。この記事のコードの大部分は最新のデスクトップ・ブラウザーで実行しますが、いくつか明らかな例外があります。もちろん、モバイル・ブラウザーでもテストする必要があり、そのために iPhone と Android の最新 SDK が必要です。この記事では iPhone SDK 3.1.3 と Android SDK 2.1 を使用しました。リンクは「参考文献」セクションを参照してください。

なぜオフラインでアプリケーションを動作させるのか

よく使われる頭字語

  • Ajax: Asynchronous JavaScript + XML
  • API: Application Programming Interface
  • CSS: Cascading StyleSheet
  • HTML: HyperText Markup Language
  • HTTP: HyperText Transfer Protocol
  • JSON: JavaScript Object Notation
  • JSONP: JSON with padding
  • MIME: Multipurpose Internet Mail Extensions
  • PDF: Portable Document Format
  • SDK: Software Developer Kit
  • UI: User Interface
  • URL: Uniform Resource Locator
  • W3C: World Wide Web Consortium
  • WAP: Wireless Application Protocol
  • XML: Extensible Markup Language

オフラインの Web アプリケーションは、いくつかの理由からユーザーと開発者の両方にとって魅力的です。多くの開発者にとって、プラットフォームごとにネイティブ・アプリケーションを作成するよりも、非常に人気のあるスマートフォンの全機種で動作する 1 つの Web アプリケーションを作成する方が好ましいはずです。ただし開発者にとって都合が良いからといって、ユーザーがそれを求めているとは限りません。モバイル Web アプリケーションをユーザーにとっても好都合なものにするためには、ネイティブのモバイル・アプリケーションで提供される機能と同じ機能の多く (あるいはその大部分) をモバイル Web アプリケーションで提供できる必要があります。もちろん、オフライン動作はそうした機能の 1 つです。一部のアプリケーションはインターネットから得られるデータやサービスに大きく依存し、それはモバイル Web アプリケーションかネイティブ・アプリケーションかを問いません。そうしたアプリケーションでは、インターネットへの接続が良好でなければ機能が低下するのは当然です。ただし接続が良好でないからといってアプリケーションがまったく動作しないようでは困ります。しかし従来の Web アプリケーションでは完全に動作しなくなってしまいます。

オフライン機能によって、モバイル Web アプリケーションはネイティブ・アプリケーションに近いものになります。オフライン機能には他にもメリットがあります。Web ブラウザーは従来から、静的なリソースを必ずキャッシュしてきました。Web ブラウザーは Web サーバーから送信される HTTP レスポンス・ヘッダーのメタデータを利用することで、ページの描画に必要な HTML、JavaScript、CSS、画像を取得しています。ページの描画に必要なものがすべてキャッシュされていると、そのページのロードは非常に速くなります。ただしキャッシュされていないものが何かあると、すべてが劇的に遅くなります。そうしたことはユーザーの期待に反して頻繁に発生します。場合によると、1 つの CSS ファイルと他のすべての CSS ファイルとで Cache-Control ヘッダーが異なっていたり、あるいは割り当てスペースが足りなくなった結果、ブラウザーがキャッシュをクリアーしたり、といったことが起こります。

オフライン・アプリケーションの場合には、必ずすべてがキャッシュされます。ブラウザーは必ずキャッシュからすべてをロードしますが、キャッシュからロードしてはならないものをユーザーが制御することもできます。Ajax でよく使われる細工として、Ajax の GET リクエストに余分なタイムスタンプ・パラメーターを追加し (もっと悪質な方法としては GET を使用することが適切な場合に POST を使用し)、ブラウザーにレスポンスをキャッシュさせない方法があります。こうした細工はオフライン対応の Web アプリケーションには必要ありません。

そう聞くとオフライン・アプリケーションは素晴らしいものに思えるので、作成は複雑に違いないと思う人がいるかもしれません。しかし実は非常に簡単なのです。オフライン・アプリケーションを作成するためには、以下の 3 つを行う必要があります。

  1. オンライン・マニフェスト・ファイルを作成する
  2. このマニフェスト・ファイルに関してブラウザーに指示をする
  3. サーバーで MIME タイプを設定する

オフラインのマニフェスト・ファイル

オフライン・アプリケーションでは、重要なファイルが 1 つあり、それがアプリケーションのキャッシュ・マニフェスト・ファイルです。このファイルによって、正確に何をキャッシュする必要があるか (あるいはオプションとして、何をキャッシュすべきでないか) をブラウザーに指示します。このファイルはアプリケーションに関する事実を記載したソースとなります。リスト 1 は単純なキャッシュ・マニフェスト・ファイルの例を示しています。


リスト 1. 単純なキャッシュ・マニフェスト・ファイル
CACHE MANIFEST # Version 0.1 offline.html /iui/iui.js /iui/iui.css /iui/loading.gif /iui/backButton.png /iui/blueButton.png /iui/cancel.png /iui/grayButton.png /iui/listArrow.png /iui/listArrowSel.png /iui/listGroup.png /iui/pinstripes.png /iui/redButton.png /iui/selection.png /iui/thumb.png /iui/toggle.png /iui/toggleOn.png /iui/toolbar.png /iui/whiteButton.png /images/gymnastics.jpg /images/soccer.png /images/gym.jpg /images/soccer.jpg 

このファイルには、アプリケーションが適切に動作するために必要なファイルがすべて含まれています。そうしたファイルには、HTML ファイル、JavaScript、CSS、画像などがあります。また動画や PDF、XML ファイルなどが含まれる場合もあります。この例に示されている URL がすべて相対 URL であることに注意してください。相対 URL はすべて、キャッシュ・マニフェスト・ファイルに対する相対 URL でなければなりません。この場合では、キャッシュ・マニフェスト・ファイルは Web アプリケーションのルート・ディレクトリーにあります。リスト 2 のディレクトリー構造をリスト 1 の相対 URL と比較してみてください。


リスト 2. Web アプリケーションのディレクトリー構造をテキストで表現したもの
  Name V images     gymnastics.jpg     soccer.png V iui     backButton.png     blueButton.png     cancel.png     grayButton.png     iui.css-logo-touch-icon.png     iui.css     iui.js     iuix.css     iuix.js     listArrow.png     listArrowSel.png     listGroup.png     loading.gif     pinstripes.png     redButton.png     selection.png     thumb.png     toggle.png     toggleOn.png     toolbar.png     toolButton.png     whiteButton.png   manifest.mf   offline.html > WEB-INF 

このアプリケーションが iUI フレームワークを使用していることにお気付きなのではないでしょうか。iUI フレームワークは、モバイル Web アプリケーションで iPhone のネイティブ・アプリケーションのルック・アンド・フィールを実現するための JavaScript+CSS キットとしてよく使われています。リスト 1 リスト 2 とを比べるとわかるように、このフレームワークの JavaScript ファイルと CSS ファイルでは、画像をいくつか使用しています。しかしこれらのファイルは、マニフェスト・ファイルに記載されている限りブラウザーによってすべてキャッシュされ、オフライン・モードで使用することができます。

リスト 1 を見て気付く、もう 1 つの重要なものがバージョン情報です。このバージョン情報は HTML5 仕様の一部ではありません。実際、この情報はファイルの中のコメントにすぎません。しかし、このような情報を含めることは重要です。こうした情報をファイルに含めることによって、アプリケーションの新バージョンがあることをブラウザーに伝えるのです。例えば、何らかの HTML や JavaScript、さらには 1 つだけ画像を変更したとします。もしマニフェスト・ファイルの内容を変更しないと、ブラウザーは変更されたリソースの新バージョンをロードしようとしません。キャッシュ・マニフェスト・ファイルには有効期限がないため、ユーザーがキャッシュをクリアーしないと、またはマニフェスト・ファイルの変更をクリアーしないと、すべてがキャッシュされたまま保持されます。ブラウザーは新しいマニフェスト・ファイルがないかどうかを調べます。新しいマニフェスト・ファイルがあることを示すためには、既存のマニフェスト・ファイルに何らかの変更を加えます。ページの HTML ファイルを変更する例に戻ると、HTML を変更してマニフェスト・ファイルのバージョン・ストリングを変更すると、ブラウザーはリソースが変更されたこと、そしてそれらのリソースを再度ダウンロードする必要があることを認識します。コメントの中にバージョン番号を記入しておくと、リソースのライフサイクルを容易に管理することができます。

マニフェスト・ファイルに関してブラウザーに指示する

Web アプリケーションのオフライン・キャッシュを有効にする上では、もう 1 つ欠けているものがあります。Web ブラウザーは、ユーザーがキャッシュの有効化を望んでいることを認識し、どこにキャッシュ・マニフェスト・ファイルがあるのかを把握しなければなりません。リスト 3 は、それを非常に容易に行う方法を示しています。


リスト 3. オフラインで動作可能な Web ページ
<!DOCTYPE html> <html> <html manifest="manifest.mf">     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>     <meta name="viewport" content="width=device-width; initial-scale=1.0;          maximum-scale=1.0; user-scalable=0;"/>     <meta name="apple-touch-fullscreen" content="YES" />     <link rel="apple-touch-icon" href="/iui/iui-logo-touch-icon.png" />     <style type="text/css" media="screen">@import "/iui/iui.css";</style>     <script type="application/x-javascript" src="/iui/iui.js"></script>     <title>Let's do it offline</title> </head> <body>     <div class="toolbar">         <h1 id="pageTitle">Going offline</h1>         <a id="backButton" class="button" href="#"></a>     </div>     <ul id="menu" title="Sports" selected="true">         <li><a href="#gym"><img height="80" width="80"  src="/images/gym.jpg" align="middle"/>             <span style="display:inline-block;  vertical-align:middle">Gymnastics</span></a></li>         <li><a href="#soccer"><img src="/images/soccer.jpg"  align="middle"/>         <span style="display:inline-block;  vertical-align:middle">Soccer</span></a></li>     </ul>     <div id="gym" title="Gymnastics" class="panel">         <img src="/images/gymnastics.jpg" alt="Boys Gymnastics"/>     </div>      <div id="soccer" title="Soccer" class="panel">         <img src="/images/soccer.png" alt="Boys Soccer"/>     </div> </body> </html> 

この HTML で最も重要な点は、ルートにある html 要素です。この要素に manifest という属性があることに注意してください。この属性によって、この Web ページがオフラインで動作できることをブラウザーに伝えます。マニフェスト・パラメーターの値は、この Web ページのキャッシュ・マニフェスト・ファイルの URL です。この URL も、完全な URL の場合もありますが、この例では (指定の Web ページに対する) 相対 URL です。ここでもう 1 つ注意する点は、このページの DOCTYPE です。この DOCTYPE は HTML 5 Web ページに対する正式な DOCTYPE です。オフライン Web アプリケーションの仕様では、この DOCTYPE を使うようにという指示はありません。しかし皆さんには、この DOCTYPE を使うことをお勧めします。この DOCTYPE を使わないと、一部のブラウザーはそのページが HTML5 のページだと認識せず、キャッシュ・マニフェスト・ファイルを無視する可能性があります。この HTML の他の部分は iUI の使い方を示した簡単な例にすぎません。図 1 は、このページを iPhone シミュレーターで表示した場合を示しています。


図 1. iPhone シミュレーター上で実行されるオフライン Web アプリケーション
iPhone シミュレーター上で実行されるオフライン Web アプリケーションのスクリーン・キャプチャー。Sports アプリケーションとして、Gymnastics (体操) と Soccoer (サッカー) という選択肢があります。 

オフライン・アプリケーションのテストは少し面倒です。可能であれば、Web サーバーにアプリケーションをデプロイするのが最も簡単なテスト方法です。そしてそのページに 1 度アクセスし、インターネット接続を切断し、再度そのページにアクセスを試みます。何かが失敗する場合には、何らかのファイルをキャッシュ・マニフェスト・ファイルで指定するのを忘れている可能性があります。このアプリケーションを試す前に、Web サーバーの構成に 1 つ重要な変更を加える必要があります。

Web サーバーの構成

リスト 3 では、Web ページのルート html 要素の manifest 属性を使用することでキャッシュ・マニフェスト・ファイルの場所を示しました。しかしキャッシュ・マニフェスト・ファイルの仕様によれば、ブラウザーがキャッシュ・マニフェスト・ファイルをダウンロードして処理する際には、ブラウザーは追加の検証ステップを実行する必要があります。ブラウザーはキャッシュ・マニフェスト・ファイルの MIME タイプをチェックする必要があり、そのタイプは text/cache-manifest でなければなりません。このために通常必要となるのは、静的ファイルにこの MIME タイプを設定するように Web サーバーを構成するか、あるいは動的に作成したファイルにこの MIME タイプを設定するようなコードを作成するかのいずれかです。前者の方法の方が明らかに効率的ですが、サーバーの構成を制御できない場合には (サーバーが共有環境やホスト環境にある場合など)、後者の方法を行わなければならない場合があります。サーバーを制御することができ、Java™ アプリケーション・サーバーを使用している場合には、MIME タイプの設定を Web アプリケーションの web.xml ファイルの一部として構成することができます。リスト 4 はこの例を示しています。


リスト 4. web.xml を構成して MIME タイプを設定する
<?xml version="1.0" encoding="utf-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <!-- Servlets go here -->     <mime-mapping>         <extension>mf</extension>         <mime-type>text/cache-manifest</mime-type>     </mime-mapping>         <welcome-file-list>         <welcome-file>index.html</welcome-file>     </welcome-file-list> </web-app> 

ここで重要な部分は当然ながら mime-mapping 要素です。このリストでは、.mf 拡張子で終わるすべてのファイルのMIME タイプをtext/cache-manifest にするように指示しています。もちろん、そうしたファイルは、静的コンテンツを提供することに特化したサーバー (Apache Web サーバーなど) から提供した方が一層効率的です。標準的な Apache Web サーバーでは、httpd/conf ディレクトリーにある mime.types ファイルを修正するだけでよいはずです。


リスト 5. mime.types で MIME タイプを設定する
# This file controls what Internet media types are sent to the client for # given file extension(s).  Sending the correct media type to the client # is important so they know how to handle the content of the file. # Extra types can either be added here or by using an AddType directive # in your config files. For more information about Internet media types, # please read RFC 2045, 2046, 2047, 2048, and 2077.  The Internet media type # registry is at <http://www.iana.org/assignments/media-types/>.  # MIME type                     Extensions text/cache-manifest             mf # many more mappings... 

どちらの例でも、マニフェスト・ファイルの拡張子に mf を使っています。これはマニフェスト・ファイルが manifest.mf であったからです。この拡張子はまったく任意です。構成ファイルの中でマッピングに使用されている拡張子とマニフェスト・ファイルの拡張子が一致している限り、.manifest や .foo のような拡張子も可能です。他のアプリケーションや Web サーバーには他の構成メカニズムも使えることに注意してください。これで HTML 5 を使ってオフラインのモバイル Web アプリケーションを作成する上で欠かせない要素については説明したので、オフラインのモバイル Web アプリケーションの機能をさらに活用する、もっと複雑な例を見てみましょう。

高度な例

先ほどの例では、すべてのコンテンツは静的であり、オフライン・モードですべてを表示できるのは便利でした。より一般的なアプリケーションでは、そのアプリケーションのサーバーや Web サービスから動的にデータを読み込める必要があります。この記事の例をより現実的なものにするために、Twitter から少しデータを読み込むことにします。この連載の前回までの記事を読んでいれば、この話題はおなじみのはずです (「参考文献」を参照)。まず、この例の HTML を変更したものを見てみましょう (リスト 6)。


リスト 6. 変更された HTML
<body onload="init()">     <div class="toolbar">         <h1 id="pageTitle">Going offline</h1>         <a id="backButton" class="button" href="#"></a>     </div>     <ul id="menu" title="Sports" selected="true">         <li><a href="#gym">             <img height="80" width="80" src="/images/gym.jpg" align="middle"/>             <span style="display:inline-block; vertical-align:middle">Gymnastics</span>         </a></li>         <li><a href="#soccer"><img src="/images/soccer.jpg" align="middle"/>             <span style="display:inline-block; vertical-align:middle">Soccer</span>         </a></li>         <li id="online" style="display: none"><img src="/images/online.jpg"/></li>     </ul>     <ul id="gym" title="Gymnastics"></ul>      <ul id="soccer" title="Soccer"></ul> </body> 

この HTML での主な違いは、gym 要素と soccer 要素がリストになっており、そのリストの中身が空である点です。この 2 つのリストに対し、それぞれ体操とサッカーの話題に関する Twitter のツイートを追加します。また、online という id のリスト項目要素によって画像を表示し、その画像によってアプリケーションがオンラインかオフラインかをユーザーに示していることにも注意してください。ただし、この要素はデフォルトで非表示になっています。つまりデフォルト・モードはオフラインです。body 要素は本体がロードされたら init 関数を呼び出すことを規定しています。リスト 7は、この関数を示しています。


リスト 7. JavaScript でページを初期化する
function init(){     if (navigator.onLine){         searchTwitter("gymnastics", "showGymTweets");         searchTwitter("soccer", "showSoccerTweets");         $("online").style.display = "inline";     }      gymTweets = localStorage.getItem("gymnastics");     if (gymTweets){         gymTweets = JSON.parse(gymTweets);         showGymTweets();     }     soccerTweets = localStorage.getItem("soccer");     if (soccerTweets){         soccerTweets = JSON.parse(soccerTweets);         showSoccerTweets();     }     document.body.addEventListener("online", function() {             $("online").style.display= "inline";             applicationCache.update();             applicationCache.addEventListener("updateready", function() {                 applicationCache.swapCache();             }, false);         }, false);     document.body.addEventListener("offline", function() {         $("online").style.display = "none";     }, false);         } 

このコードではまず、オンラインかオフラインかのチェックを行っています。オンラインの場合には、オンライン用の画像を表示します。もっと重要な点として、オンラインの場合には searchTwitter 関数を呼び出し、Twitter からデータをロードします。これも、JSONP を使ってブラウザーから直接 Twitter を検索するための手法です (この手法については、この連載の前回までの記事で説明しました。「参考文献」を参照)。次に、localStorage から既存のツイートをロードしようとします。localStorage を理解していればわかると思いますが、localStorage もオフライン・モードで非常に便利な HTML 5 の機能の 1 つです。ローカル・ストレージについての詳細は、この連載の第 2 回を調べてください (「参考文献」を参照)。新しい検索 (オンラインであることが検出されると起動されます) の場合も、ローカルに保存されたツイートをロードする場合も、showGymTweets 関数と showSoccerTweets 関数が呼び出されることに注意してください。この 2 つの関数は似ています。リスト 8  showGymTweets を示しています。


リスト 8. 体操に関するツイートを表示する
function showGymTweets(response){     var gymList = $("gym");     gymList.innerHTML = "";     if (gymTweets){         if (response){             gymTweets = response.results.reverse().concat(gymTweets);         }     } else {         gymTweets = response.results.reverse();     }     showTweets(gymTweets, gymList);     localStorage.setItem("gymnastics", JSON.stringify(gymTweets)); } 

この関数は、ローカルに保存されたツイートを表示したり Twitter から取得した新しいツイートを表示したりすることができ、両方が存在する場合には両方を組み合わせて表示することもできます。最も重要な点として、この関数はすべてをローカルに保存し、ツイート・データのローカル・キャッシュを構築します。このコードはすべて、ローカルにキャッシュされたデータとサーバーからのライブ・データの両方を管理するための標準的なコードです。このようにすることによって、アプリケーションはオンラインでもオフラインでもスムーズに動作することができます。

リスト 7 に戻ると、最後にイベント・ハンドラーを登録しています。ここではオンラインのイベントとオフラインのイベントを登録しています。こうすることで、ブラウザーがオンライン状態またはオフライン状態に切り替わると通知されるようになります。少なくとも、オンライン用の画像の表示/非表示を切り替えることができます。アプリケーションがオフライン動作をした後にオンラインになった場合には、applicationCache オブジェクトにアクセスします。applicationCache オブジェクトは、キャッシュ・マニフェスト・ファイルで宣言されている内容に従って、キャッシュされているすべてのリソースを表しています。この場合には、このオブジェクトの update メソッドを呼び出します。このメソッドはブラウザーに対し、applicationCache の更新を検出したかどうかを調べるように指示します。先ほど触れたように、ブラウザーはキャッシュ・マニフェスト・ファイルが更新されているかどうかを最初にチェックします。キャッシュの更新があるかどうかをチェックするために、別のイベント・リスナーを追加します。キャッシュの更新がある場合には、applicationCache swapCache メソッドを呼び出します。するとキャッシュ・マニフェスト・ファイルに指定されたすべてのファイルがリロードされます。

キャッシュ・マニフェスト・ファイルに関して言えば、この高度な例に最終的な修正を 1 つ加える必要があります。キャッシュ・マニフェスト・ファイルをリスト 9 のように変更する必要があります。


リスト 9. 変更されたキャッシュ・マニフェスト・ファイル
CACHE MANIFEST # Version 0.2 CACHE: offline.html json2.js /iui/iui.js /iui/iui.css /iui/loading.gif /iui/backButton.png /iui/blueButton.png /iui/cancel.png /iui/grayButton.png /iui/listArrow.png /iui/listArrowSel.png /iui/listGroup.png /iui/pinstripes.png /iui/redButton.png /iui/selection.png /iui/thumb.png /iui/toggle.png /iui/toggleOn.png /iui/toolbar.png /iui/whiteButton.png /images/gym.jpg /images/soccer.jpg /images/online.jpg  NETWORK: http://search.twitter.com/ 

この例では、マニフェスト・ファイルに CACHE セクションを明示的に追加しています。マニフェスト・ファイルにはさまざまなセクションを記述することができますが、1 つしかセクションがない場合には、そのセクションは CACHE とみなされ、省略することができます。ここで CACHE セクションを明示的に追加している理由は、このマニフェスト・ファイルには NETWORK セクションもあるからです。こうすることでブラウザーに対し、指定ドメイン (この場合は search.twitter.com) から得られるものはすべてネットワークから取得する必要があり、キャッシュしてはならないことを指示します。この場合は Twitter の検索結果をローカルにキャッシュしているので、もちろんブラウザーがこれ以上クエリーを間接的にキャッシュする必要はありません。こうしておけば、アプリケーションは必ず Twitter から最新のツイートをロードしますが、そうしたツイートの保存も行い、たとえユーザーの機器がインターネットへ接続できなくなった場合でも保存されたツイートをユーザーが利用できるようにします。

まとめ

Web アプリケーションは Mosaic の時代から大きく進歩しました。モバイル Web アプリケーションは、それよりもさらに進化しています。WML (Wireless Markup Language) しか使えない WAP フォンの時代は終わりつつあります。今やモバイル・ブラウザーに対し、大規模なデスクトップ・ブラウザーにすら要求しないことを要求するようになっています。オフライン機能はそうした機能の 1 つです。HTML 5 で規定された標準も大きく進歩し、モバイル Web アプリケーションを非常に容易にオフライン対応にできるようになっています。この連載の次回の記事では、もう 1 つの HTML 5 標準である Web ワーカーについて調べ、Web ワーカーによってモバイル Web アプリケーションのパフォーマンスが大幅に向上することを説明します。

0 件のコメント:

コメントを投稿