Sencha Touchでオンライン/オフラインのプロキシを生成する
こんにちは、ゼノフィnakamuraです。
はじめに

Sencha Touch でよくある要件は、ある端末がインターネットへの接続を失っても、アプリケーションが通常通りに動作するという事です。アプリケーションがオフラインで動作するために、Sencha Cmd は、自動的に生成される App Manifest ファイルなど、いくつかの素晴らしいツールを提供しています。しかし、最も大きい課題の一つは、データをどう扱うかという事です。データを扱う方法は数々あり、よくあるのはローカルストレージプロキシと AJAX プロキシを切り替えるというテクニックです。
この記事では、 ProWeb Software 社のトム・クックシー (Tom Cooksey) が同じ効果を実現する方法を説明しますが、その方法では単一のプロキシを利用し、ストアに設定する事で、開発者に完全に分かりやすくします。
プロキシ
このサンプルでは、AJAXプロキシを拡張します。どのプロキシを拡張しても大丈夫ですが、AJAXはよくある要件なので、それを使用します。プロキシに流れるデータを管理するメソッドを、二つだけオーバーライドする必要があります。また、後で説明しますが、いくつかコンフィグ項目も生成します。次のものは、まだロジックは入っていませんが、これから作るクラスです。
/** * Offline Proxy * @extend Ext.data.proxy.Ajax */ Ext.define('proxy.OfflineProxy', { extend: 'Ext.data.proxy.Ajax', alias: 'proxy.offline', config: { storageKey: null, storageFacility: null, online: true }, originalCallback: null, /** * doRequest をオーバーライドして、リクエストを傍受して * 失敗したリクエストを捕らえてオフラインにする * @param operation * @param callback * @param scope * @returns {*} */ doRequest: function(operation, callback, scope) { }, /** * processResponse をオーバーライドして、オンラインの場合に * オフラインストレージにレスポンスを保存させ、レスポンスが失敗した * ときにそこから戻せるようにする * * we can fall back. * @param success * @param operation * @param request * @param response * @param callback * @param scope */ processResponse: function(success, operation, request, response, callback, scope) { } }); |
doRequest()
は、実際にサーバーにリクエストを送ります。
このメソッドをオーバーライドして、デバイスがオフラインであるとか、サーバーにアクセスできないといった状況の時に、インターセプトしてストアーに偽のレスポンスを送ります。
processResponse()
は、サーバーからのレスポンスを解釈します。このメソッドをオーバーライドしたい主な理由は、元々の機能を全て実行して、成功したレスポンスから取得したデータをストレージファシリティに格納したいからです。リクエストが失敗した場合、プロキシに再び上記で説明した、偽のレスポンスを渡すことを指示します。
ストレージファシリティ
プロキシは、指定されたストレージファシリティを利用します。これは、二つのメソッド getItem
と setItem
があるシンプルなシングルトンクラスです。ここで行っているようにAPI を実装すれば、どのストレージファシリティを利用しても動作します。
/** * A class that gives access into WebSQL storage */ Ext.define('storage.WebSQL', { singleton: true, config:{ /** * The database capacity in bytes (can't be changed after construction). 50MB by default. */ capacity:50 * 1024 * 1024 }, /** * @private * websql データベースオブジェクト */ storage:null, connected: false, constructor: function (config) { this.callParent(config); this.storage = openDatabase('storage', '1.0', 'Offline resource storage', this.getCapacity()); this.storage.transaction(function (tx) { tx.executeSql('CREATE TABLE IF NOT EXISTS items (key, value)'); }, function (error) { console.error('WebSQL: Connection Error'); }, function () { console.log('WebSQL: Connected'); }); }, /** * アイテムをストアから取得する * @param key 取得するキー * @param callbacks success と failure コールバックがあるオブジェクト */ getItem:function (key, callbacks) { this.storage.transaction(function (tx) { tx.executeSql('SELECT * FROM items WHERE key = ?', [key], function (tx, results) { var len = results.rows.length; if (len > 0) { callbacks.success(results.rows.item(0).value) } else { callbacks.failure(); // no result } }); }, function (error) { console.log('WebSQL: Error in getItem'); callbacks.failure(error); }); }, /** * ストアのアイテムをセットする * @param key セットするキー * @param value 保存する値 * @param callbacks success と failure コールバックがあるオブジェクト */ setItem:function (key, value, callbacks) { this.storage.transaction(function (tx) { //まず古いバージョンを削除する tx.executeSql('DELETE FROM items WHERE key = ?', [key]); tx.executeSql('INSERT INTO items (key, value) VALUES (?, ?)', [key, value]); }, function (error) { console.log('WebSQL: Error in setItem:' + error.message); callbacks.failure(error.message); }, function () { callbacks.success(); // no value. }); } }); |
特に注目すべき所はありませんが、setItem
と getItem
メソッドは、success と failure のコールバックが提供されるようになる事を意識して下さい。また、コンストラクタに SQL データベースを設定しました。ローカルストレージなどの場合には、この手順は必要ありません。
この動作をもう少し深く検討しましょう。setItem
を見てみましょう:
setItem:function (key, value, callbacks) { this.storage.transaction(function (tx) { //まず古いバージョンを削除する tx.executeSql('DELETE FROM items WHERE key = ?', [key]); tx.executeSql('INSERT INTO items (key, value) VALUES (?, ?)', [key, value]); }, function (error) { console.log('WebSQL: Error in setItem:' + error.message); callbacks.failure(error.message); }, function () { callbacks.success(); // no value. }); } }); |
ここでセットしたいキー (ストレージのキーはプロキシで設定します) 、新しい値 (この場合はシリアル化された JSON オブジェクトです) 、コールバックセットしたオブジェクトをパラメータとしています。このキーの古い参照を削除し、新しい値を挿入します。
次の2行は、
tx.executeSql('DELETE FROM items WHERE key = ?', [key]); tx.executeSql('INSERT INTO items (key, value) VALUES (?, ?)', [key, value]); |
ローカルストレージを利用する場合は次の様になります。
localstorage.removeItem(key); localstorage.setItem(key, value); |
このトランザクションが成功すると、パスされた success コールバックを呼び出し、そうでなければ failure コールバックを呼び出します。
getItem
同様に動作します。
getItem:function (key, callbacks) { this.storage.transaction(function (tx) { tx.executeSql('SELECT * FROM items WHERE key = ?', [key], function (tx, results) { var len = results.rows.length; if (len > 0) { callbacks.success(results.rows.item(0).value) } else { callbacks.failure(); // no result } }); }, function (error) { console.log('WebSQL: Error in getItem'); callbacks.failure(error); }); } |
こちらは二つのパラメータ、key、callbacks しかありません。この key は、アイテムを取得するために使い、取得できたら、その値で success コールバックを呼び出します。それ以外は、failure コールバックを呼び出します。
最終的なプロキシ
ストレージファシリティができましたので、プロキシを完成させられます。プロキシは、ストレージファシリティを使って、プロキシが設定されたら、そこに渡されます。
doRequest: function(operation, callback, scope) { var that = this, passCallback, request, fakedResponse = {}; this.originalCallback = callback; function failedRequest() { fakedResponse.status = 500; fakedResponse.responseText = 'Error'; fakedResponse.statusText = 'ERROR'; that.processResponse(false, operation, request, fakedResponse, passCallback, scope); } if(this.getOnline()) { console.log('PROXY: Loading from online resource'); return this.callParent(arguments); }else{ console.log('PROXY: Loading from offline resource'); request = this.buildRequest(operation); passCallback = this.createRequestCallback(request, operation, callback, scope); if(this.getStorageKey() && this.getStorageFacility()) { this.getStorageFacility().getItem(this.getStorageKey(), { success: function(dataString) { fakedResponse.status = 200; fakedResponse.responseText = dataString; fakedResponse.statusText = 'OK'; that.processResponse(true, operation, request, fakedResponse, passCallback, scope); }, failure: failedRequest }); }else{ console.error('No storage key or facility for proxy'); setTimeout(function() { failedRequest(); }, 1); } } }, |
最初にオーバーライドするメソッドは doRequest()
です。
オリジナルの AJAX クラスでは、このメソッドはサーバーへの実際のリクエストを実行するために使われますので、端末がオンラインの場合は、callParent()
を使って親メソッドを呼び出します。
しかし、デバイスがオフラインであれば、我々のオフライン用のストレージファシリティからデータを取得し、processResponse()
を呼び出して、偽のレスポンスを作ります。
processResponse()
は、渡されたものは有効なレスポンスか確認するために問い合わせするので、このレスポンスを偽る必要があります。
適切な http 状態コード (200) を設定し、ストレージファシリティから取り出したデータを responseText
に設定し、statusText
を “OK” に設定して偽ります。
このオブジェクトは、processResponse
メソッドから見ると完全に通常のリクエストレスポンスになっています。
Sencha フレームワークはこのような抽象化は得意なので、コードを綺麗に分離することができます。
processResponse: function(success, operation, request, response, callback, scope) { var that = this; if(success) { console.log('PROXY: Request succeeded'); this.callParent(arguments); if(this.getOnline()) { if(this.getStorageKey() && this.getStorageFacility()) { this.getStorageFacility().setItem(this.getStorageKey(), response.responseText, { success: function() { console.log('PROXY: Data stored to offline storage: ' + that.getStorageKey()); }, failure: function(error) { console.log('PROXY: Error in storing data: ' + that.getStorageKey()); } }); }else{ console.error('PROXY: No storage key or facility for proxy'); } } }else{ if(this.getOnline()) { //もしオンラインの時にリクエストが失敗したら、offline console.log('PROXY: Request failed, will try to fallback to offline'); this.setOnline(false); に戻るべきです this.doRequest(operation, this.originalCallback, scope); }else{ this.callParent(arguments); } } } |
次にオーバーライドするメソッドは、processResponse()
です。ここでもサーバーへのリクエストが成功した場合は、通常の処理をするために callParent()
を呼び出しますが、さらに、リクエストのデータをオフラインのストレージファシリティに保存します。
この処理にはいくつかの段階があります。 まず、もしリクエストの success フラグが true (つまり、サーバーから有効なレスポンスを受け取れた場合) 、プロキシに設定されている online コンフィグをチェックします。 この値は、初期化時にプロキシに渡されます。 このフラグはデフォルトで true になっていて、 プロキシはリクエストが失敗すると、その時点で端末がオフラインになったと解釈します。 フラグが true に設定されて、ストレージファシリティが渡されていたら、取得したデータをそこに格納します。 リクエストが成功する度にこれが行われますので、端末がオフラインになった時には、その時点でアクセスできた最新のデータが常にあることになります。
リクエストが失敗した場合は、online フラグを false に設定し、doRequest
メソッドを再び実行します。online フラグは false に設定されているため、メソッドはストレージファシリティからデータを取得します。
全てを合わせる
ストアーのプロキシを構成する時に、全ての部分を合わせる事ができます。
proxy: { type : 'offline', url : '/test-api/test-resource.json', storageKey : 'buttons', storageFacility : storage.WebSQL, reader : { type : 'json', rootProperty : 'data' } } |
プロキシのエイリアスは proxy.offline
なので、
ご覧の通り、type
を "offline"
に設定しました。
storageKey
は、データを格納するオフラインデータストレージのキーです。この場合には、ストアは "buttons"
と呼ばれていますので、ストレージキーに同じ名前を与えています。ストレージファシリティは前に生成したもので、他は全て通常のプロキシのコンフィグにあるものです。
結果
このコードの動作を確認できるように、 Sencha Touch デモアプリ を開発しました。 さらに、これをデモのスクリーンショットもいくつあります。このサンプルアプリにはツールバーがあり、内容は、サーバー上の JSON ファイルで動作しています。
最初の画像では、ボタンが生成された事が見えますし、コンソールでデータがオフラインのストレージに格納された事を確認できます。
二つ目の画像では、test-resource.json ファイルが使えなくなっています。この場合、404 のレスポンスが戻るようにリネームしました (また、これはデバイスがインターネットにアクセスできないとか、サーバーがダウンしている場合でも同じです) 。コンソールのログを見ると、オフラインのバージョンから読み込んで、ボタンがちゃんと読み込まれました。
まとめ
Sencha のクラスシステムは柔軟性が高いので、ビルトインのコンポーネントやユーティリティの拡張や他の目的で利用する事がとても簡単です。この記事で示した通り、既に定義されているフローに合わせて、必要な追加の機能を加えるだけで、難しくなりそうな課題を簡単に解決できました。その結果、元々のプロキシの力を保った上で、オフラインデータの管理をわかりやすく実現しました。