2011年3月8日火曜日

HTML 5 を使ってモバイル Web アプリケーションでローカル・ストレージを使用する

前提条件

よく使われる頭字語

  • API: Application Programming Interface
  • CSS: Cascading StyleSheet
  • DOM: Document Object Model
  • HTML: HyperText Markup Language
  • HTTP: HyperText Transfer Protocol
  • JSON: JavaScript Object Notation
  • JSONP: JSON with Padding
  • SDK: Software Developer Kit
  • UI: User Interface
  • URL: Uniform Resource Locator
  • W3C: World Wide Web Consortium

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

ローカル・ストレージの基本

Web 開発者はクライアントにデータを保存するために長年苦労してきました。そのために乱用されてきたのは、何と言っても HTTP のクッキーです。開発者達は、HTTP 仕様で割り当てられた 4KB の中に、驚くほど大量のデータを詰め込んできました。理由は単純です。対話型の Web アプリケーションでは、さまざまな理由からデータを保存する必要があり、そうしたデータをサーバーに保存するのは非効率的で、セキュリティー上の問題があって、不適切な場合が多いからです。この問題に対して、長年にわたりいくつか別の方法が使われてきました。さまざまなブラウザーには独自のストレージ API が導入されています。また開発者達は、Flash Player のストレージ機能を JavaScript によって公開する、という拡張方法も使用してきました。Google も同様に、さまざまなブラウザー用に Gears プラグインを作成し、このプラグインの中にストレージ API が含まれています。当然ですが、一部の JavaScript ライブラリーはこうした違いを吸収しようとしています。つまりこれらのライブラリーには単純な API が用意され、その API を使用することで、どんなストレージ機能があるのかをチェックします (ストレージ機能は独自のブラウザー API の場合もあれば、Flash のようなプラグインの場合もあります)。

Web 開発者にとって幸いなことに、ローカル・ストレージのための標準がついに HTML 5 仕様に含まれ、この標準が多種多様なブラウザーに実装されています。実際、この標準は最も早く採用された標準の 1 つであり、主なブラウザーすべて (Microsoft® Internet Explorer®、Mozilla Firefox、Opera、Apple Safari、Google Chrome) の最新バージョンでサポートされています。モバイル開発者にとってさらに重要な点として、WebKit ベースのブラウザー (Android (バージョン 2.0 またはそれ以降) を使用した携帯電話や iPhone に採用されています) や他のモバイル・ブラウザー (Mozilla の Fennec など) でもこの標準がサポートされています。こうした点を念頭に置いて、この API について調べてみましょう。

Storage API

localStorage API は非常に単純です。実際、HTML 5 仕様によると、localStorage API はDOM Storage インターフェースを実装しています。このようにローカルであることを区別する理由は、HTML 5 ではDOM Storage インターフェースを実装するオブジェクトとして、localStorage  sessionStorage という 2 つの明確に異なるオブジェクトを規定しているためです。sessionStorage オブジェクトはセッション中のデータのみを保存する Storage 実装です。もっと正確に言えば、sessionStorage にアクセスできるスクリプトの実行が終了すると、ブラウザーはいつでも sessionStorage のデータを削除することができます。これは複数のユーザー・セッションにわたって存続する localStorage とは対照的です。この 2 つのオブジェクトの API は共通なので、ここでは localStorage のみに焦点を絞ることにします。

Storage API は昔ながらの名前と値のペアというデータ構造です。最もよく使われるメソッドは、getItem(name)  setItem(name, value) です。この 2 つのメソッドは想像されるとおりの動作をします。つまり、getItem は名前に関連付けられた値を返すか、何もない場合はヌルを返し、setItem  localStorage に名前と値のペアを追加するか、あるいは既存の値を置き換えます。またremoveItem(name) もあります。removeItem(name) は名前からもわかるように、名前と値のペアが存在する場合はそのペアをlocalStorage から削除し、存在しない場合には何もしません。最後に、名前と値のペアのすべてに対して繰り返し処理をするための API が 2 つあります。1 つはデータの長さを表すものであり、保存されている名前と値のペアの合計数を示します。それに対応するものとして、key(index) メソッドは、ストレージの中で使われているすべての名前の配列から 1 つの名前を返します。

こうした単純な API を利用することで、多くのことを実現することができます。例えば、機能をパーソナライズしたり、あるいはユーザーの動作を追跡したりすることができます。そうした使い方もモバイル Web 開発者にとって重要ですが、それよりもはるかに重要な使い方として、キャッシングがあります。localStorage を利用すると、サーバーのデータをクライアントのローカル・マシンに容易にキャッシュすることができます。こうすることで、サーバーへの呼び出しに時間がかかる場合にも待ち時間を減らすことができ、またサーバーに要求するデータの量を最小限にとどめることができます。では、こうしたキャッシングを localStorage を使って実現する例を見てみましょう。

例: ローカル・ストレージを使ったキャッシング

この例は、この連載の第 1 回で作成し始めたサンプル・アプリケーションを基に作成します。第 1 回では、ジオロケーション API を使ってユーザーの位置情報を取得し、近くで投稿された Twitter のツイートを検索する方法を説明しました。今回は、その例を単純化して、パフォーマンスを改善します。まず、この例からジオロケーションの部分を削除し、Twitter 検索の部分のみにします。リスト 1 は単純化した Twitter 検索アプリケーションを示しています。


リスト 1. 最も基本的な Twitter 検索
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name = "viewport" content = "width = device-width"/> <title>Basic Twitter Search</title> <script type="text/javascript">     function searchTwitter(){         var query = "http://search.twitter.com/search.json?callback =showResults&q=";         query += $("kwBox").value;         var script = document.createElement("script");         script.src = query;         document.getElementsByTagName("head")[0].appendChild(script);     }     // ui code deleted for brevity     function showResults(response){         var tweets = response.results;         tweets.forEach(function(tweet){             tweet.linkUrl = "http://twitter.com/" + tweet.from_user  + "/status/" + tweet.id;         });         makeResultsTable(tweets);     } </script> <!--  CSS deleted for brevity --> </head> <body>     <div id="main">         <label for="kwBox">Search Twitter:</label>         <input type="text" id="kwBox"/>         <input type="button" value="Go!" onclick="searchTwitter()"/>     </div>     <div id="results">     </div> </body> </html> 

このアプリケーションでは、Twitter の検索 API で JSONP がサポートされている点を利用します。ユーザーが検索を実行依頼すると、API が呼び出され、ページへの script タグの追加とコールバック関数名の指定が動的に行われます。こうすることで、Web ページからクロスドメインの呼び出しを行えるようになります。その呼び出しから戻ると、コールバック関数 (showResults) が呼び出されます。Twitter から返されるツイートのそれぞれにリンクの URL を追加し、それらのツイートを表示する簡単なテーブルを作成します。この処理を高速化するために、検索クエリーの結果をキャッシュし、ユーザーがクエリーを実行依頼した場合にはキャッシュされた結果を使用します。まず、localStorage を使ってローカルにツイートを保存する方法を調べてみましょう。

ローカルにツイートを保存する

この基本的な Twitter 検索によって、Twitter 検索 API による一連のツイートが得られます。これらのツイートをローカルに保存して、その基になったキーワード検索と関連付けられると、利用可能なキャッシュが得られたことになります。ツイートを保存するためには、Twitter 検索 API への呼び出しから戻ると呼び出されるコールバック関数を変更すればよいだけです。リスト 2 は変更された関数を示しています。


リスト 2. 検索と保存
function searchTwitter(){     var keyword = $("kwBox").value;     var query = "http://search.twitter.com/search.json?callback =processResults&q=";     query += keyword;     var script = document.createElement("script");     script.src = query;     document.getElementsByTagName("head")[0].appendChild(script); } function processResults(response){     var keyword = $("kwBox").value;     var tweets = response.results;     tweets.forEach(function(tweet){         saveTweet(keyword, tweet);         tweet.linkUrl = "http://twitter.com/" + tweet.from_user + "/status/" + tweet.id;     });     makeResultsTable();     addTweetsToResultsTable(tweets); } function saveTweet(keyword, tweet){     // check if the browser supports localStorage     if (!window.localStorage){         return;     }     if (!localStorage.getItem("tweet" + tweet.id)){         localStorage.setItem("tweet" + tweet.id, JSON.stringify(tweet));     }     var index = localStorage.getItem("index::" + keyword);     if (index){         index = JSON.parse(index);     } else {         index = [];     }     if (!index.contains(tweet.id)){         index.push(tweet.id);         localStorage.setItem("index::"+keyword, JSON.stringify(index));     }  } 

最初の searchTwitter 関数から説明しましょう。この関数は、ユーザーが検索を実行依頼すると呼び出されます。リスト 1 から変更された唯一の点はコールバック関数です。ツイートが返されたら単にそれらを表示するのではなく、それらのツイートを処理する必要があります (表示すると同時に保存する必要があります)。そこで、processResults という新しいコールバック関数を指定します。各ツイートを引数にして saveTweet を呼び出します。また、検索を行うために入力されたキーワードも渡します。こうすることで、ツイートとキーワードとを関連付けます。

saveTweet 関数の中では、まずブラウザーが実際に localStorage をサポートしているかどうかを確認します。先ほど触れたように、localStorage はデスクトップ・ブラウザーでもモバイル・ブラウザーでも広くサポートされています。しかしこうした新しい機能を使用する際には、その機能が実際にサポートされているかどうかを必ずチェックする必要があります。サポートされていない場合には、単純に saveTweet 関数から戻ります。当然、何も保存されませんが、エラーは発生しません。この場合はアプリケーションにキャッシュがない、というだけのことです。localStorage がサポートされている場合には、そのツイートが既に保存されているかどうかを最初にチェックします。まだ保存されていない場合には、setItem を使ってそのツイートを保存します。次に、キーワードに対応する index オブジェクトを取得します。index オブジェクトは単純に、キーワードと関連付けられたツイートの ID で構成される配列です。ツイートのID が index の一部として含まれていない場合には、その ID を追加して index を更新します。

リスト 3 で JSON を保存、ロードする際、JSON.stringify  JSON.parse を使用していることに注意してください。JSON オブジェクト (より正確には、window.JSON) は HTML 5 仕様の一部であり、ネイティブ・オブジェクトとして必ず存在します。JSON オブジェクトのstringify メソッドはすべての JavaScript オブジェクトをシリアライズ・ストリングに変換し、一方 JSON オブジェクトの parse メソッドはその逆変換を行い、シリアライズ・ストリング表現から JavaScript オブジェクトを復元します。localStorage はストリングのみを保存するため、この逆変換が必要なのです。ただし、ネイティブ JSON オブジェクトは localStorage ほど広くは実装されていません。例えば、iPhone にインストールされている最新のモバイル Safari ブラウザー (この記事の執筆時点でバージョン 3.1.3) にはネイティブ JSON オブジェクトはありませんが、最新の Android ブラウザーにはあります。ネイティブ JSON オブジェクトがサポートされているかどうかは容易にチェックすることができ、サポートされていない場合には追加で JavaScript ファイルをロードします。ネイティブで使用されているものと同じ JSON オブジェクトを入手するためには、json.org の Web サイトにアクセスする必要があります (「参考文献」を参照)。こうしたシリアライズ・ストリングがローカルでどう表示されるかを調べるためには、さまざまなブラウザー・ツールを使用して、指定サイトの localStorage に何が保存されているかを調べます。図 1 は、ローカルのキャッシュに保存されたツイートを Chrome の開発者ツールを使って表示したものです。


図 1. ローカルにキャッシュされたツイート
ローカルにキャッシュされたツイートの一覧を表示したスクリーン・キャプチャー (Key フィールドと Value フィールドが表示されています) 

Chrome にも Safari にも開発者ツールが組み込まれており、これらのツールを使うことで、localStorage に保存されている任意のデータを表示することができます。これは localStorage を使用するアプリケーションのデバッグに非常に便利です。これらのツールによって、ローカルにプレーン・テキストで保存されたキーと値のペアを表示することができます。これで Twitter の検索 API から得られたツイートの保存を開始でき、保存されたツイートをキャッシュとして使えるようになったので、あとは localStorage からツイートを読み取ればよいだけです。その方法を次に見てみましょう。

ローカル・データを迅速にロードする

リスト 2 では、getItem メソッドを使って localStorage から読み取る例をいくつか説明しました。今や、ユーザーが検索を実行依頼すると、その検索内容をキャッシュの中で探し、キャッシュされた結果を即座にロードすることができます。もちろん、これまでどおり Twitter の検索 API に対してクエリーを実行することもできます。常に誰かしらがつぶやいており、それらが検索結果に追加されるからです。しかし今や、さらに効率的にクエリーを実行する方法も使えるようになり、まだキャッシュの中にない結果のみを要求すればよくなったのです。リスト 3 は、更新された検索コードを示しています。


リスト 3. 最初にローカルで検索を行う
function searchTwitter(){     if ($("resultsTable")){         $("resultsTable").innerHTML = ""; // clear results     }     makeResultsTable();     var keyword = $("kwBox").value;     var maxId = loadLocal(keyword);     var query = "http://search.twitter.com/search.json?callback=processResults&q=";     query += keyword;     if (maxId){         query += "&since_id=" + maxId;     }     var script = document.createElement("script");     script.src = query;     document.getElementsByTagName("head")[0].appendChild(script); } function loadLocal(keyword){     if (!window.localStorage){         return;     }     var index = localStorage.getItem("index::" + keyword);     var tweets = [];     var i = 0;     var tweet = {};     if (index){         index = JSON.parse(index);         for (i=0;i<index.length;i++){             tweet = localStorage.getItem("tweet"+index[i]);             if (tweet){                 tweet = JSON.parse(tweet);                 tweets.push(tweet);             }         }     }     if (tweets.length < 1){         return 0;     }     tweets.sort(function(a,b){         return a.id > b.id;     });     addTweetsToResultsTable(tweets);     return tweets[0].id; } 

ここで最初に気付く点として、検索が実行依頼されると、新しい loadLocal 関数を最初に呼び出しています。この関数は、キャッシュの中で見つかった最新のツイートの ID を整数値として返します。loadLocal 関数は keyword を引数に取り、この引数はlocalStorage キャッシュの中にある関連のツイートを見つけるために使われます。maxId がある場合には、maxId を使って Twitter へのクエリーを変更し、since_id パラメーターを追加します。こうすることで Twitter API に対し、このパラメーターで指定された ID よりも新しいツイートのみを返すように指示します。これにより、Twitter から返される結果の数を減らせる可能性があります。モバイル Web アプリケーションでは、サーバーへの呼び出しを最適化すると、モバイル・ネットワークの動作が遅くてもユーザー・エクスペリエンスを大きく改善することができます。今度は loadLocal を詳しく調べてみましょう。

loadLocal 関数では、先ほどのリスト 2 で保存されたデータ構造を利用します。まず、キーワードと関連付けられた index を、getItem メソッドを使ってロードします。index が見つからない場合には、キャッシュされたツイートはありません。つまり表示するものは何もなく、クエリーに対する最適化は行われません (それを示すために値 0 を返します)。index が見つかった場合には、その index から ID のリストを取得します。これらの各ツイートはローカルにキャッシュされているため、単純に再度 getItem メソッドを使って各ツイートをキャッシュからロードします。ロードされたツイートは次に、ソートされます。addTweetsToResultsTable 関数を使ってツイートを表示し、最新のツイートの ID を返します。この例では、最新のツイートを取得するコードが、UI を更新するための関数を直接呼び出しています。これを見て憤慨する人がいるかもしれません。ツイートを保存および取得するコードと、ツイートを表示するコードがすべて、processResults 関数によって結合されているからです。この方法に代わる結合の緩い方法として、ストレージ・イベントを使用する方法があります。

ストレージ・イベント

今度はサンプル・アプリケーションを拡張し、キャッシュされる頻度が最も高かった検索項目の上位 10 位までを表示できるようにします。これはユーザーが最も頻繁に実行依頼した検索を反映しているかもしれません。リスト 4 は、この上位 10 位までを計算して表示する関数を示しています。


リスト 4. 検索頻度の高い上位 10 位までを計算する
function displayStats(){     if (!window.localStorage){ return; }     var i = 0;     var key = "";     var index = [];     var cachedSearches = [];     for (i=0;i<localStorage.length;i++){         key = localStorage.key(i);         if (key.indexOf("index::") == 0){             index = JSON.parse(localStorage.getItem(key));             cachedSearches.push ({keyword: key.slice(7), numResults: index.length});         }     }     cachedSearches.sort(function(a,b){         if (a.numResults == b.numResults){             if (a.keyword.toLowerCase() < b.keyword.toLowerCase()){                 return -1;             } else if (a.keyword.toLowerCase() > b.keyword.toLowerCase()){                 return 1;             }             return 0;         }         return b.numResults - a.numResults;     }).slice(0,10).forEach(function(search){         var li = document.createElement("li");         var txt = document.createTextNode(search.keyword + " : " + search.numResults);         li.appendChild(txt);         $("stats").appendChild(li);     }); } 

この関数から、localStorage API の他の側面がわかります。まず、localStorage に保存された項目の合計数を取得し、次にそれらの項目に対して繰り返し処理を行います。保存された項目が index の場合には、その index オブジェクトを解析し、これから処理する対象のデータ (その index と関連付けられたキーワードと、その index の中にあるツイートの数) を表すオブジェクトを作成します。このデータは cachedSearches という配列に保存されます。次に cachedSearches をソートし、最も検索頻度が高かった検索結果が先頭になるようにし、2 つの検索のキャッシュ回数が同じ場合には、大文字小文字を区別せずにアルファベット順のソートを行います。そして頻度の高い順に上位 10 位までの各検索に対して HTML を作成し、それらの HTML を番号付きリストに追加します。ページが最初にロードされた時に、この関数を呼び出すようにしてみましょう (リスト 5)。


リスト 5. ページを初期化する
window.onload = function() {     displayStats();     document.body.setAttribute("onstorage", "handleOnStorage();"); } 

最初の行では、ページがロードされるとリスト 4 の関数を呼び出しています。2 回目のロードでは、さらに興味深いことが起こります。ここで onstorage イベントのイベント・ハンドラーを設定します。onstorage イベントは localStorage.setItem 関数の実行が終了すると起動されます。こうすることで、検索頻度の高い上位 10 位までを再計算することができます。リスト 6 は、このイベント・ハンドラーを示しています。


リスト 6. ストレージ・イベント・ハンドラー
function handleOnStorage() {     if (window.event && window.event.key.indexOf("index::") == 0){         $("stats").innerHTML = "";         displayStats();     } } 

onstorage イベントはウィンドウと関連付けられます。onstorage イベントには、keyoldValuenewValue など、いくつかの便利なプロパティーがあります。こうした自明のプロパティーの他に、onstorage イベントには url (値が変更されたページの URL) と source(値が変更されたスクリプトを含むウィンドウ) があります。url と source は、アプリケーションを開くためのウィンドウやタブ、さらには iFrames がユーザーの環境に複数ある場合には便利ですが、どれもモバイル・アプリケーションではあまり一般的ではありません。リスト 6 に戻ると、実際に必要なプロパティーは key プロパティーのみです。このプロパティーを使用して、index が変更されているかどうかを調べます。変更されている場合には、上位 10 位までのリストをリセットし、displayStats 関数を再度呼び出してリストを再描画します。この手法のメリットは、上位 10 位までのリストが自己完結となり、他の関数はこのリストを認識する必要がないことです。

先ほどlocalStorage  sessionStorage の両方を含む DOM Storage は、一般的に HTML 5 の機能として広く採用されていると説明しました。ただし、ストレージ・イベントはその例外であり、少なくともデスクトップ・ブラウザーでは例外です。この記事の執筆時点で、ストレージ・イベントをサポートするデスクトップ・ブラウザーは Safari 4 およびそれ以降と Internet Explorer 8 およびそれ以降のみです。Firefox、Chrome、Opera ではストレージ・イベントはサポートされていません。ただしモバイルの世界では少し良い状況にあります。最新バージョンの iPhone ブラウザーと Android ブラウザーはどちらもストレージ・イベントを完全にサポートしており、ここで紹介したコードはすべて、両方のブラウザーで完全に動作します。

まとめ

これまで利用できなかったクライアント上の大量のストレージ・スペースが利用できるようになると、開発者の皆さんは制約から解放されたと感じるはずです。さらに、経験の長い Web 開発者が以前から実現したいと思っていたにもかかわらず、変わった手法に頼らないと実現できなかった事柄が実現できるようになります。また、モバイル開発者にとってデータをローカルにキャッシュできるようになることは、非常にエキサイティングなことです。ローカル・キャッシングは、アプリケーションのパフォーマンスを劇的に改善するだけではなく、モバイル Web アプリケーションのもう 1 つのエキサイティングな新機能であるオフライン動作を実現する上でも重要になります。この連載の次回の記事では、このオフライン動作について説明します。

0 件のコメント:

コメントを投稿