PhoneGapとSymfony2で始めるオフライン対応アプリ開発
AndroidやiPhoneのブラウザではHTML5が全てサポートされているわけではないので、PhoneGapでブラウザの差を吸収しつつ、HTML5/JavaScriptで作成したものをアプリ化して、ネットワークに接続されていない状態でも使えるようにします。
HTML5単体でもApplicationCacheなどはあるものの、初回読み込みは必要なので、完全にオフラインで動作させることはできません。
Indexed DB APIはAndroid標準ブラウザでは使えませんので、オフライン状態でかつローカルのデータベースを使いたい場合はPhoneGapやTitaniumなどを使う必要があります。
今回はPhoneGapとjQuery Mobileを使用してオフラインアプリをAndroid向けに開発します。Android向けなんですが、たぶんiOSでも動作します。
ひとまずAndroidでビルドしてみます。
環境はMacですが、Windowsでもさほど変わらない(はず)
Android SDKのインストール
SDKのダウンロードはこちら http://developer.android.com/sdk/index.html
EclipseにADTプラグインを追加
Eclipseの Help > install new software から https://dl-ssl.google.com/android/eclipse/ このURLを指定する。
Android SDK ManagerでToolsとAndroidプラットフォームのSDKをインストール。
PhoneGapの導入
こちら http://phonegap.com/download からPhoneGap1.7を頂戴します。
で、このスタートガイドに従ってHello World的なものをやります。
http://docs.phonegap.com/en/1.7.0/guide_getting-started_android_index.md.html#Getting%20Started%20with%20Android
1. 適当にAndroidプロジェクトを作る
2. assets/www ディレクトリを作成する
3. libs ディレクトリを作成する
4. cordova-1.7.0.js を assets/www/ に配置
5. cordova-1.7.0.jar を libs/ に配置
6. PhoneGapの xml というフォルダを res/ に配置
7. JavaのBuild Pathのライブラリで、libs/cordova-1.7.0.jar を追加
Activityを書き換える
package reoring.phonegap.test23; import android.os.Bundle; // PhoneGap import org.apache.cordova.*; public class PhoneGapTest23Activity extends DroidGap { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.loadUrl("file:///android_asset/www/index.html"); } }
assets/www/index.html を書く
<html> <head> <title>PhoneGap Test</title> <script type="text/javascript" charset="utf-8" src="cordova-1.7.0.js"></script> </head> <body> <h1>Hello PhoneGap on Android</h1> </body> </html>
jQuery Mobileを配置
assets/js/jquery.mobile に jQuery Mobileからダウンロードしてきたアーカイブを展開します。
jquery.mobile-1.1.0.css などがあればOK
HTMLを書き換え
<!DOCTYPE HTML> <html> <head> <title>PhoneGap Test</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="js/jquery.mobile/jquery.mobile-1.1.0.min.css"/> <link rel="stylesheet" href="js/jquery.mobile/jquery.mobile.theme-1.1.0.min.css"/> <script type="text/javascript" src="js/jquery-1.7.2.min.js"></script> <script type="text/javascript" src="js/jquery.mobile/jquery.mobile-1.1.0.js"></script> <script type="text/javascript" charset="utf-8" src="cordova-1.7.0.js"></script> </head> <body> <div data-role="page"> <div data-role="header"> <h1>タイトルエリア</h1> </div> <div data-role="content"> <p>コンテンツエリア</p> </div> <div data-role="footer"> <h4>フッターエリア</h4> </div> </div> </body> </html>
これでひとまずPhoneGap上でjQuery Mobileが動作しました。
-------------------------------------------
Behat + MinkでATDDを実施
Symfony2のBehat Bundleで受け入れテストを日本語で記述して自動化します。
ブラウザの駆動ドライバとして今回はSahiを使います。
ATDD(Acceptance Test Driven Development)はその名の通り受け入れテストを自動化します。
受け入れテストをソースを書く前に記述して最終的な振る舞いを考慮しながら開発して手戻りを最小限にします。
受け入れテストは日本語で記述することによって振る舞いの確認をお客さんにして頂けるように考慮します。
Sahiのインストール
Tyto Softwareさんのページからダウンロードします。http://sahi.co.in/w/sahi-os-vs-sahi-pro
ダウンロードしたらインストールして、インストールディレクトリにあるsahi.shを起動します。
Macの環境だとbinに移動してからsahi.shを実行しないとなぜかエラーになるので注意してね。
正常に実行できるとコンソールに下記のようなメッセージがでます。
--------
SAHI_HOME: ..
SAHI_USERDATA_DIR: ../userdata
SAHI_EXT_CLASS_PATH:
--------
Sahi properties file = /Users/morireo/sahi/config/sahi.properties
Sahi user properties file = /Users/morireo/sahi/userdata/config/userdata.properties
Added shutdown hook.
>>>> Sahi started. Listening on port: 9999
>>>> Configure your browser to use this server and port as its proxy
>>>> Browse any page and CTRL-ALT-DblClick on the page to bring up the Sahi Controller
-----
Reading browser types from: /Users/morireo/sahi/userdata/config/browser_types.xml
-----
Sahiプロキシがポート9999で立ち上がります。
この状態で http://localhost:9999/ などどやるとStart URL:という画面になるので、これが出ていれば正常に動作しています。
Symfony2でSahiとBehat+minkの設定
test.phonegap.localをVirtualHostの設定でweb/phonegap/に向けます。
behat+minkの設定
app/config/config_test.yml を作ります。
behat: ~ mink: base_url: http://test.phonegap.local/ sahi: host: %sahi_host% browser_name: firefox show_cmd: open %s default_session: sahi # default_session: symfony goutte: ~
FeatureContextを少し書き換える
BehatのFeatureContextを少し書き換えます。extends BehatContextの部分を、MinkContextにします。
namespace Phonegap\TestBundle\Features\Context; use Behat\BehatBundle\Context\BehatContext, Behat\BehatBundle\Context\MinkContext; use Behat\Behat\Context\ClosuredContextInterface, Behat\Behat\Context\TranslatedContextInterface, Behat\Behat\Exception\PendingException; use Behat\Gherkin\Node\PyStringNode, Behat\Gherkin\Node\TableNode; /** * Feature context. * * BehatContext or MinkContext */ class FeatureContext extends MinkContext { public function __construct($parameters) { parent::__construct($parameters); } }
フィーチャの記述
ここからはSymfony2で行います。Symfony2で予めBehatとMinkのバンドルをインストールする必要があります。インストール方法は割愛しますが、https://github.com/reoring/phonegap_bdd_sample に動作するサンプルを置いてあります
下記のようなフィーチャファイルをsrc/Phonegap/TestBundle/Features/phonegap.feature として記述します。
先頭行の# language: ja とすることでフィーチャの記述が日本語で記述できます。
このフィーチャではMinkの組み込みステップを使っています。Appendixに組み込みステップの一覧を記述しています。
# language: ja フィーチャ: PhoneGapのコンテンツをテスト 背景: シナリオ: トップページからメニューへの遷移 前提 ユーザーは "/index.html" を表示している ならば レスポンスに "コンテンツエリア" が含まれていること もし ユーザーが "メニュー" のリンク先へ移動する ならば 画面に "メニュー表示" と表示されていること
これを書いたら実行します。
./app/console behat -e=test src/Phonegap/TestBundle/Features/phonegap.feature フィーチャ: PhoneGapのコンテンツをテスト 背景: # src/Phonegap/TestBundle/Features/phonegap.feature:3 シナリオ: トップページからメニューへの遷移 # src/Phonegap/TestBundle/Features/phonegap.feature:5 前提 ユーザーは "/index.html" を表示している # Phonegap\TestBundle\Features\Context\FeatureContext::visit() ならば レスポンスに "コンテンツエリア" が含まれていること # Phonegap\TestBundle\Features\Context\FeatureContext::assertResponseContains() もし ユーザーが "メニュー" のリンク先へ移動する # Phonegap\TestBundle\Features\Context\FeatureContext::clickLink() ならば 画面に "メニュー表示" と表示されていること # Phonegap\TestBundle\Features\Context\FeatureContext::assertPageContainsText() 1 scenario (1 passed) 4 steps (4 passed) 0m9.213s
実行するとブラウザが自動的に起動してテストが実行されます。
Appendix.
Minkで使える組み込みステップ
Given /^(?:|ユーザーは )"(?P<page>[^\s]+)" を表示している$/
When /^(?:|ユーザーが )"(?P<page>[^\s]+)" へ移動する$/
When /^(?:|ユーザーが )ページをリロードする$/
When /^(?:|ユーザーが )履歴の前のページに戻る$/
When /^(?:|ユーザーが )履歴の次のページヘ進む$/
When /^(?:|ユーザーが )"(?P<button>(?:[^"]|\\")*)" ボタンをクリックする$/
When /^(?:|ユーザーが )"(?P<link>(?:[^"]|\\")*)" のリンク先へ移動する$/
When /^(?:|ユーザーが )"(?P<field>(?:[^"]|\\")*)" フィールドに "(?P<value>(?:[^"]|\\")*)" と入力する$/
When /^(?:|ユーザーが )"(?P<value>(?:[^"]|\\")*)" という値を "(?P<field>(?:[^"]|\\")*)" に入力する$/
When /^(?:|ユーザーが)次のように入力する:$/
When /^(?:|ユーザーが )"(?P<option>(?:[^"]|\\")*)" という値を "(?P<select>(?:[^"]|\\")*)" から選択する$/
When /^(?:|I )additionally select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
When /^(?:|ユーザーが )"(?P<option>(?:[^"]|\\")*)" にチェックをつける$/
When /^(?:|ユーザーが )"(?P<option>(?:[^"]|\\")*)" のチェックをはずす$/
When /^(?:|ユーザーが)パス "(?P<path>[^"]*)" にあるファイルを "(?P<field>(?:[^"]|\\")*)" に添付する$/
Then /^(?:|ユーザーが )(?P<page>[^\s]+) を表示していること$/
Then /^the (?i)url(?-i) should match "(?P<pattern>(?:[^"]|\\")*)"$/
Then /レスポンスコードが (?P>code<\d+) であること/
Then /^the response status code should not be (?P\d+)$/
Then /^(?:|画面に )"(?P<text>(?:[^"]|\\")*)" と表示されていること$/
Then /^(?:|画面に )"(?P<text>(?:[^"]|\\")*)" と表示されていないこと$/
Then /^レスポンスに "(?P<text>(?:[^"]|\\")*)" が含まれていること$/
Then /^レスポンスに "(?P<text>(?:[^"]|\\")*)" が含まれていないこと$/
Then /^"(?P<element>[^"]*)" エレメントに "(?P<text>(?:[^"]|\\")*)" と表示されていること$/
Then /^"(?P<element>[^"]*)" エレメントに "(?P<value>(?:[^"]|\\")*)" という値が含まれていること$/
Then /^(?:|画面に )"(?P<element>[^"]*)" エレメントが表示されていること$/
Then /^(?:|画面に )"(?P<element>[^"]*)" エレメントが表示されていないこと$/
Then /^"(?P<field>(?:[^"]|\\")*)" フィールドに "(?P<value>(?:[^"]|\\")*)" が含まれていること$/
Then /^"(?P<field>(?:[^"]|\\")*)" フィールドに "(?P<value>(?:[^"]|\\")*)" が含まれていないこと$/
Then /^チェックボックス "(?P<checkbox>(?:[^"]|\\")*)" のチェックがついていること$/
Then /^チェックボックス "(?P<checkbox>(?:[^"]|\\")*)" のチェックがはずれていること$/
Then /^(?:|I )should see (?P<num>\d+) "(?P<element>[^"]*)" elements?$/
Then /^最後のレスポンスを表示$/
Then /^最後のレスポンスをブラウザで表示$/