2011年3月8日火曜日

Web ワーカーを使ってモバイル Web アプリケーションを高速化する

はじめに

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

モバイル機器でのマルチスレッド JavaScript

ほとんどの開発者にとって、マルチスレッド、つまり並行プログラミングは新しいものではありません。最近のほとんどのプログラミング言語では、何らかの形で並行プログラミングをサポートしています。しかし、JavaScript は並行プログラミングをサポートしている言語には含まれません。JavaScript の作成者は、Web ページ上で単純なタスクを実行するように設計された JavaScript のような言語では、並行プログラミングはあまりに問題が多く、不必要であると考えたのです。しかし Web ページが Web アプリケーションへと進化するにつれ、JavaScript によって実行されるタスクの複雑さのレベルは他の言語の場合と同程度になりました。また、並行プログラミングをサポートする他の言語を扱う開発者達は、スレッドやミューテックスなど、並行プリミティブに付随する異常なほどの複雑さに苦労しがちでした。実際最近では、Scala、Clojure、F# など、いくつかの新しい言語が登場しており、それらの言語ではすべて、並行処理を簡単に行えることが特徴になっています。

よく使われる頭字語

  • Ajax: Asynchronous JavaScript + XML
  • API: Application Programming Interface
  • CSS: Cascading stylesheet
  • DOM: Document Object Model
  • HTML: Hypertext Markup Language
  • REST: Representational State Transfer
  • SDK: Software Developer Kit
  • UI: User Interface
  • URL: Uniform Resource Locator
  • W3C: World Wide Web Consortium
  • XML: Extensible Markup Language

Web ワーカー仕様は、単に JavaScript や Web ブラウザーに並行処理を追加するためのものではなく、並行処理をスマートに追加するための仕様であり、この仕様に従うことで、問題を引き起こすツールを使う必要がなくなります。例えばデスクトップ・アプリケーションの開発では、長年マルチスレッドを使ってアプリケーションから I/O リソースにアクセスし、そうした I/O リソースが利用可能になるまで待機する間に UI をフリーズさせないようにしていました。しかしそうしたアプリケーションでは、複数のスレッドによって (UI を含む) 共有リソースが変更される結果、アプリケーションがフリーズしたりクラッシュしたりすることがよくありました。Web ワーカーを使用すると、そうした問題は起こりえません。生成されるスレッドは、メインの UI スレッドと同じリソースにアクセスすることができません。実際、生成されるスレッドの中のコードは、メインの UI スレッドによって実行されるコードと同じファイル上にはありません。

それどころか、その外部ファイルをコンストラクターの一部として提供する必要さえあります (リスト 1)。

このプロセスでは、以下の 3 つを使います。

  1. メイン・スレッドで実行される、Web ページ用の JavaScript (ここではページ・スクリプトと呼びます)。
  2. ワーカー・オブジェクト。これは JavaScript オブジェクトであり、このオブジェクトを使って Web ワーカー機能を実行します。
  3. 新たに生成されるスレッドで実行されるスクリプト。ここではワーカー・スクリプトと呼びます。

では、まずページ・スクリプトを見てみましょう (リスト 1)。


リスト 1. ページ・スクリプトで Web ワーカーを使用する
var worker = new Worker("worker.js"); worker.onmessage = function(message){     // do stuff }; worker.postMessage(someDataToDoStuffWith); 

リスト 1 を見ると、Web ワーカーを使うための基本的なステップは 3 つあることがわかります。第 1 に、ワーカー・オブジェクトを作成し、新しいスレッドで実行されるスクリプトの URL をそのオブジェクトに渡します。このワーカーが実行するコードはすべてワーカー・スクリプトに含まれている必要があり、このスクリプトの URL がワーカーのコンストラクターに渡されます。このワーカー・スクリプトの URL はブラウザーの同一生成元ポリシーの制約を受け、Web ワーカーを作成するページ・スクリプトをロードしたページをロードしたドメインと同じドメインになければなりません。

次に、onmessage 関数を使ってコールバック・ハンドラー関数を指定します。このコールバック関数はワーカー・スクリプトが実行された後に呼び出されます。message はワーカー・スクリプトから返されるデータであり、このメッセージの処理方法は任意です。このコールバック関数はメイン・スレッドで実行されるため、DOM にアクセスすることができます。一方、ワーカー・スクリプトは別のスレッドで実行されるため、DOM にアクセスすることはできません。そのため、ワーカー・スクリプトからメイン・スレッドにデータを送信する必要があります (メイン・スレッドでは、安全に DOM を変更してアプリケーションの UI を更新することができます)。これは、何も共有しないように設計されている Web ワーカーの重要な特徴です。

リスト 1 の最後の行はワーカーの起動方法を示しており、ワーカーの postMessage 関数を呼び出しています。ここでワーカーにメッセージ (この場合も単なるデータ) を渡しています。もちろん、postMessage は非同期関数であり、この関数を呼び出すと即座に制御が返されます。

今度はワーカー・スクリプトを検証しましょう。リスト 2 のコードはリスト 1  worker.js ファイルの内容です。


リスト 2. ワーカー・スクリプト
importScripts("utils.js"); var workerState = {}; onmessage = function(message){      workerState = message.data;       // do stuff with the message     postMessage({responseXml: this.responseText}); } 

これを見ると、ワーカー・スクリプトには独自の onmessage 関数があることがわかります。この関数が呼び出されるのは、メイン・スレッドから postMessage が呼び出されたときです。ページ・スクリプトから渡されたデータは message オブジェクトの中で postMessage関数に渡されます。このデータにアクセスするためには message オブジェクトの data プロパティーを取得します。ワーカー・スクリプトの中でデータの処理を終えたら、postMessage 関数を呼び出し、メイン・スレッドにデータを返送します。メイン・スレッドでも、メイン・スレッドで受信するメッセージのデータ・プロパティーにアクセスすることで、このデータを利用することができます。

ここまでは、単純ながら強力な、Web ワーカーの動作について見てきました。次に、Web ワーカーを使ってモバイル Web アプリケーションを高速化する方法を調べてみましょう。その前に、機器のサポートについて説明する必要があります。つまり、ここで説明しているのはモバイル Web アプリケーションであり、モバイル Web アプリケーションの開発ではブラウザー間の機能の違いに対応することが不可欠なのです。

機器のサポート

Android 2.0 から、Android ブラウザーは HTML 5 の Web ワーカー仕様を完全にサポートしています。この記事の執筆時点で、新しい Android 機器のほとんどには (非常によく使われている Motorola の Droid を含めて) Android 2.1 が使われています。また Web ワーカーは、Maemo オペレーティング・システムを実行する Nokia の機器で使われている Mozilla Fennec ブラウザーや、Windows Mobile 機器でも完全にサポートされています。この中に含まれていない重要なものとして iPhone があります。iPhone OS バージョン 3.1.3 と 3.2 (iPad 上で実行されるバージョンの OS) は、まだ Web ワーカーをサポートしていません。ただし、Safari では既にサポートしています。そのため、iPhone 上で実行される Mobile Safari ブラウザーに Web ワーカーが登場するのも時間の問題でしょう。(特に米国での) iPhone の圧倒的なシェアを考えると、Web ワーカーが存在することを前提とせず、存在を検出できた場合にのみモバイル Web アプリケーションの機能強化に Web ワーカーを使うのが最も適切です。それを念頭に、Web ワーカーを使ってモバイル Web アプリケーションを高速化する方法を調べてみましょう。

ワーカーを利用してパフォーマンスを向上する

スマートフォンのブラウザーでの Web ワーカーのサポートは適切であり、改善されつつあります。そのため、いつモバイル Web アプリケーションにワーカーを使うのか、という疑問が出てきます。答えは簡単で、長い時間を要する処理が必要な場合には、いつでもワーカーを使用することができます。ワーカーの使い方の一部の例として、円周率を 1 万桁まで計算するなど、非常に高負荷の数学計算にワーカーが使われている場合もあります。そうした計算が Web アプリケーションで必要になる可能性は非常に低いと思われ、モバイル Web アプリケーションではもっと低いはずです。しかしリモート・リソースからのデータ取得は非常に一般的であり、この記事の例でもそこに焦点を絞ります。

この例では、Daily Deals (毎日変更される特売品) のリストを eBay から取得します。この特売品リストには、各特売品に関する簡単な情報が含まれています。詳細な情報を入手するためには eBay の Shopping API を使います。ここではこの追加情報を、Web ワーカーを使って先読みします。その間ユーザーは特売品リストを閲覧しながら、気になる商品を 1 つ選択します。こうした eBay データに Web アプリケーションからアクセスするためには、汎用のプロキシーを使ってブラウザーの同一生成元ポリシーを回避する必要があります。このプロキシー用として、簡単な Java サーブレットを使用しました。この記事のコードの中にはこのサーブレットが含まれていますが、ここには示してありません。この記事では、サーブレットのコードよりも、Web ワーカーを扱うコードに焦点を絞りましょう。リスト 3 は、この特売品アプリケーションの基本的な HTML ページを示しています。


リスト 3. 特売品アプリケーションの HTML
<!DOCTYPE HTML> <html>   <head>     <meta http-equiv="content-type" content="text/html; charset=UTF-8">     <meta name = "viewport" content = "width = device-width">     <title>Worker Deals</title>     <script type="text/javascript" src="common.js"></script>   </head>   <body onload="loadDeals()">     <h1>Deals</h1>     <ol id="deals">     </ol>     <h2>More Deals</h2>     <ul id="moreDeals">     </ul>   </body> </html> 

見るとわかるように、これは非常に単純な HTML であり、単なるシェルにすぎません。JavaScript を使ってデータを取得し、UI を生成しています。これはモバイル Web アプリケーション用に最適化された設計です。こうすることですべてのコードと静的なマークアップが機器にキャッシュされ、ユーザーはサーバーからのデータを待つだけでよいからです。リスト 3 で、body がロードされるとloadDeals 関数を呼び出していることに注意してください。この関数によってアプリケーションの初期データをロードします (リスト 4)。


リスト 4. loadDeals 関数
var deals = []; var sections = []; var dealDetails = {}; var dealsUrl = "http://deals.ebay.com/feeds/xml"; function loadDeals(){     var xhr = new XMLHttpRequest();     xhr.onreadystatechange = function(){         if (this.readyState == 4 && this.status == 200){                var i = 0;                var j = 0;                var dealsXml = this.responseXML.firstChild;                var childNode = {};                for (i=0; i< dealsXml.childNodes.length;i++){                    childNode = dealsXml.childNodes.item(i);                    switch(childNode.localName){                    case 'Item':                         deals.push(parseDeal(childNode));                        break;                    case "MoreDeals":                        for (j=0;j<childNode.childNodes.length;j++){                            var sectionXml= childNode.childNodes.item(j);                            if (sectionXml && sectionXml.hasChildNodes()){                                sections.push(parseSection(sectionXml));                            }                        }                        break;                        default:                        break;                    }                }                deals.forEach(function(deal){                    var entry = createDealUi(deal);                    $("deals").appendChild(entry);                });                loadDetails(deals);                sections.forEach(function(section){                    var ui = createSectionUi(section);                    $("moreDeals").appendChild(ui);                    loadDetails(section.deals);                });         }     };     xhr.open("GET", "proxy?url=" + escape(dealsUrl));     xhr.send(null); }  

リスト 4  loadDeals 関数と、このアプリケーションの中で使われるグローバル変数を示しています。ここでは deals という配列と sections という配列を使っています。これらは関連する特売品 (Deals under $10 (10 ドル未満の特売品) など) で構成される追加のグループです。また dealDetails というマップがあり、このマップのキーは (deals データから得られる) Item ID であり、値は eBay の Shopping API から得られる詳細情報です。

最初に、プロキシーに対して Ajax 呼び出しを行います。この呼び出しによって eBay の Daily Deals の REST API を呼び出します。すると特売品リストが XML 文書として得られます。この文書を、Ajax 呼び出しに使われた XMLHttpRequest オブジェクトのonreadystatechange 関数の中で構文解析します。別の 2 つの関数 (parseDeal  parseSection) を使用して XML ノードを構文解析し、もっと扱いやすい JavaScript オブジェクトに変換します。これらの関数はダウンロード可能なコード・サンプル (「ダウンロード」を参照) の中に含まれていますが、単に XML を構文解析する退屈な関数なので、ここには含めてありません。最後に、この XML を構文解析した後、さらに 2 つの関数 (createDealUi  createSectionUi) を使って DOM を変更します。それが終わると、UI は図 1 のようなものになります。


図 1. モバイル機器での Deals の UI
特売品の例を使用して、モバイル機器での Deals の UI を示すスクリーン・キャプチャー。各特売品には Show Details (詳細を表示) ボタンが表示されています。 

リスト 4 に戻り、主要な特売品をロードした後、特売品の各セクションに対して loadDetails 関数を呼び出していることに注意してください。この関数の中で、各特売品の詳細情報を eBay の Shopping API を使ってロードしていますが、このロードを行うのはブラウザーが Web ワーカーをサポートしている場合のみです。リスト 5  loadDetails 関数を示しています。


リスト 5. 特売品の詳細情報を先読みする
function loadDetails(items){     if (!!window.Worker){         items.forEach(function(item){             var xmlStr = null;             if (window.localStorage){                 xmlStr = localStorage.getItem(item.itemId);             }             if (xmlStr){                 var itemDetails = parseFromXml(xmlStr);                 dealDetails[itemDetails.id] = itemDetails;             } else {                 var worker = new Worker("details.js");                 worker.onmessage = function(message){                     var responseXmlStr =message.data.responseXml;                     var itemDetails=parseFromXml(responseXmlStr);                     if (window.localStorage){                         localStorage.setItem(                                         itemDetails.id, responseXmlStr);                     }                     dealDetails[itemDetails.id] = itemDetails;                 };                     worker.postMessage(item.itemId);             }         });     } } 

loadDetails の中では、まず windows オブジェクトの Worker 関数があるかどうかをグローバル・スコープで調べています。Worker 関数がなければ、何もしません。Worker 関数がある場合には、まず localStorage を調べ、この特売品の詳細情報の XML がないかどうかを調べます。これはモバイル Web アプリケーションでローカル・キャシュを使用する場合の一般的な方法であり、この連載の第 2 回で詳細に説明しました (「参考文献」のリンクを参照)。

ローカルで XML が見つかった場合には、その XML を parseFromXml 関数で構文解析し、詳細情報を dealDetails オブジェクトに追加します。ローカルで XML が見つからない場合には Web ワーカーを生成し、postMessage を使って特売品の Item ID を Web ワーカーに送信します。ワーカーがそのデータを取得してメイン・スレッドに返送したら、その XML を構文解析して解析結果をdealDetails に追加し、その XML を localStorage に保存します。リスト 6 には、このワーカー・スクリプト、details.js を示しています。


リスト 6. 特売品の詳細情報のためのワーカー・スクリプト
importScripts("common.js"); onmessage = function(message){     var itemId = message.data;     var xhr = new XMLHttpRequest();     xhr.onreadystatechange = function(){         if (this.readyState == 4 && this.status == 200){             postMessage({responseXml: this.responseText});         }     };     var urlStr = generateUrl(itemId);     xhr.open("GET", "proxy?url=" + escape(urlStr));     xhr.send(null); } 

このワーカー・スクリプトは非常に単純です。Ajax を使ってプロキシーを呼び出すと、eBay の Shopping API が呼び出されます。プロキシーから XML を受信すると、JavaScript オブジェクトのリテラルを使ってその XML をメイン・スレッドに返送します。注意する点として、ワーカーの XMLHttpRequest を使用しますが、返されるものはすべてワーカーの responseText プロパティー上にあり、ワーカーの responseXml プロパティー上にあるわけではありません。これは、ワーカー・スクリプトのスコープには JavaScript による DOM パーサーがないからです。generateUrl 関数 (リスト 7) が common.js ファイルに含まれていることに注意してください。リスト 6 では、importScripts を使うことで common.js をインポートしています。


リスト 7. ワーカーがインポートしたスクリプト
function generateUrl(itemId){     var appId = "YOUR APP ID GOES HERE";     return "http://open.api.ebay.com/shopping?callname=GetSingleItem&"+         "responseencoding=XML&appid=" + appId + "&siteid=0&version=665"             +"&ItemID=" + itemId; } 

これで、(Web ワーカーをサポートするブラウザーの場合に) 特売品の詳細データを追加する方法がわかったので、図 1 に戻り、このデータをアプリケーションの中でどう使うかを調べましょう。各特売品の隣には「Show Details (詳細を表示)」ボタンがあることに注意してください。このボタンをクリックすると、UI が図 2 のように変わります。


図 2. 特売品の詳細を表示する
特売品の詳細を表示した画面のスクリーン・キャプチャー。品目の詳細として、Golla 製の 2 つのポーチ (MP3 プレーヤー、携帯電話、カメラなどを収納) の説明、写真、価格が表示されています。 

この UI は showDetails 関数を呼び出すと表示されます。リスト 8 は、この関数を示しています。


リスト 8. showDetails 関数
function showDetails(id){     var el = $(id);     if (el.style.display == "block"){         el.style.display = "none";     } else {         el.style.display = "block";         if (!el.innerHTML){             var details = dealDetails[id];             if (details){                 var ui = createDetailUi(details);                 el.appendChild(ui);             } else {                 var itemId = id;                 var xhr = new XMLHttpRequest();                 xhr.onreadystatechange = function(){                     if (this.readyState == 4 &&                                        this.status == 200){                         var itemDetails =                                          parseFromXml(this.responseText);                         if (window.localStorage){                             localStorage.setItem(                                               itemDetails.id,                                                this.responseText);                         }                         dealDetails[id] = itemDetails;                         var ui = createDetailUi(itemDetails);                         el.appendChild(ui);                     }                 };                 var urlStr = generateUrl(id);                 xhr.open("GET", "proxy?url=" + escape(urlStr));                 xhr.send(null);                                     }         }     } } 

表示対象となる特売品の ID が渡され、その特売品を表示するかどうかを切り替えます。この関数が初めて呼び出されると、表示対象の特売品の詳細が既に dealDetails マップに保存されているかどうかをチェックします。ブラウザーが Web ワーカーをサポートしている場合には、詳細情報は既に dealDetails マップに保存されているため、その特売品に対する UI が作成されて DOM に追加されます。まだ詳細情報がロードされていない場合や、ブラウザーがワーカーをサポートしていない場合には、Ajax 呼び出しを行って詳細情報のデータをロードします。こうすることで、ワーカーがあってもなくてもアプリケーションは適切に動作します。これはつまり、ワーカーがサポートされている場合には既にデータはロードされており、UI は瞬時に応答するということです。ワーカーがない場合にも UI はロードされますが、このロードには数秒間かかります。

まとめ

Web 開発者にとって、Web ワーカーはもの珍しい新技術に思えるかもしれません。しかしこの記事で説明したように、Web ワーカーを利用することで極めて実用的なアプリケーションを作成することができます。特にモバイル Web アプリケーションには Web ワーカーが有効です。Web ワーカーを使うことでデータの先読みや他の事前操作を実行し、より応答性の高い UI を実現することができます。モバイル Web アプリケーションでは、速度の遅いネットワークを介してデータをロードしなければならない場合があるため、Web ワーカーによる応答性の向上は特に有効です。キャッシング戦略とワーカーを組み合わせると、ユーザーはアプリケーションの応答性の良さに驚くはずです。

0 件のコメント:

コメントを投稿