この記事では、最新の 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 について調べてみましょう。
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. ローカルにキャッシュされたツイート
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 イベントには、key
、oldValue
、newValue
など、いくつかの便利なプロパティーがあります。こうした自明のプロパティーの他に、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 件のコメント:
コメントを投稿