PhoneGapとSymfony2で始めるオフライン対応アプリ開発

2012年6月6日水曜日 投稿者 Unknown 0 コメント

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&gt;code&lt;\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 /^最後のレスポンスをブラウザで表示$/

 

ラベル: , , , , ,

JSXをSymfony2のAsseticで自動的にコンパイルする

2012年6月4日月曜日 投稿者 Unknown 0 コメント

JSXをSymfony2のAsseticで自動コンパイルしてみますよ。

AsseticはAssetにフィルタを適応する仕組みです。今回はJSXのコンパイル処理をAsseticで自動的にコンパイルしてみます。

まずは、JSX本体の準備

git clone http://github.com/jsx/JSX.git

node.jsが入っていなければインストール

brew install node.js

npm(node package manager)が入っていなければインストール

curl http://npmjs.org/install.sh | sudo sh

それか

brew install npm

JSXのソースをコンパイルして実行してみる

cd JSX
./bin/jsx --executable --output hello.js example/hello.jsx
node hello.js
Hello, world!

 

Symfony2側の用意

Symfony2側のAsseticフィルタクラスの追加

JSX用のフィルタクラス追加します。ここに、vendor/assetic/src/Assetic/Filter/JsxFilter.php を下記内容で追加。

namespace Assetic\Filter;

use Assetic\Asset\AssetInterface;
use Assetic\Util\ProcessBuilder;

/**
 * Compiles JSX into Javascript.
 *
 */
class JsxFilter implements FilterInterface
{
  private $jsxPath;

  public function __construct($jsxPath = '/usr/bin/jsx')
  {
    $this->jsxPath = $jsxPath;
  }

  public function filterLoad(AssetInterface $asset)
  {
    $input = tempnam(sys_get_temp_dir(), 'assetic_jsx');
    file_put_contents($input, $asset->getContent());

    $pb = new ProcessBuilder();
    $pb
      ->inheritEnvironmentVariables()
      ->add($this->jsxPath)
      ->add($asset->getSourceRoot() . DIRECTORY_SEPARATOR . $asset->getSourcePath())
    ;

    $proc = $pb->getProcess();

    $code = $proc->run();
    unlink($input);

    if (0 < $code) {
      throw new \RuntimeException($proc->getErrorOutput());
    }


    $asset->setContent(str_replace($asset->getSourceRoot(), "", $proc->getOutput()));
  }

  public function filterDump(AssetInterface $asset)
  {
  }
}

 

次に、DIコンテナの設定をします

/Users/morireo/Sites/fotofoo_mobile/vendor/bundles/Symfony/Bundle/AsseticBundle/Resources/config/filters/jsx.xml ファイルを編集。

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="assetic.filter.jsx.class">Assetic\Filter\JsxFilter</parameter>
        <parameter key="assetic.filter.jsx.bin">/usr/bin/jsx</parameter>
    </parameters>

    <services>
        <service id="assetic.filter.jsx" class="%assetic.filter.jsx.class%">
            <tag name="assetic.filter" alias="jsx" />
            <argument>%assetic.filter.jsx.bin%</argument>
        </service>
    </services>
</container>

app/config/config.ymlにフィルタ設定の記述

assetic:
    filters:
        jsx:
            jsx_path: /usr/bin/jsx

JSXクラスの記述

Resources/public/jsx/hello.jsx などに書きます

import "js/web.jsx";

class _Main {
  static function main(args : string[]) :void {
    log "Hello, world!";
  }

  static function say() : void {
    var text = dom.window.document.createTextNode("Hello, world!");
    dom.getElementById("hello").appendChild(text);
  }
}

Twigに記述

<html>
<head>
  <title></title>
  {% javascripts debug=true output="/js/hello.js" '@JsxTestBundle/Resources/public/jsx/hello.jsx' filter='jsx' %}
  <script src="{{ asset_url }}"></script>
  {% endjavascripts %}
</head>
<body>

<p id="hello"></p>

<script>
  window.addEventListener("load", function(e) {
    JSX.require("/Resources/public/jsx/hello.jsx")._Main.say$();
  });
</script>

</body>
</html>

これで自動的に.jsxファイルが.jsファイルになってブラウザ上で動作するようになりました。

静的ファイルとして配置するには、./app/console assetic:dump コマンドで web/ 以下に配置されます。プロダクション環境では自動的に再生成されないので、ソースファイルを変更した場合には削除するかassetic:dumpで配置してあげる必要があります。

ラベル: , , ,