My Octopress Blog

A blogging framework for hackers.

Phonegapアプリケーションにネイティブコードを追加する(Android)

Phonegapは、基本的にはJavascriptでクロスプラットフォームなコードを記述しますが、 プラグインとしてネイティブコードを記述することもできます。

この記事では、Phonegapで作成するAndroidアプリケーションに、Backgroun Serviceを 追加する例を記載します。

この例では、

  • Android Platform Specificなリソース(この例ではJava code)の追加
  • plugin.xmlによるAndroidManifest.xmlの変更
  • Webアプリケーション側からのネイティブコードへのアクセス

の3点を行う方法を示します。
他のネイティブ機能追加を行う際も、だいたいこの応用で行えると思います(たぶん)

Phonegapのバージョンは本記事作成時の最新版3.5.0を使用します。

プロジェクト作成

$ cordova create hoge

プラグインディレクトリ作成

$ cd hoge
$ mkdir plugins/com.example.plugin

ネイティブコードを配置

1秒おきに現在時刻をデバッグ出力するスレッドを立ち上げるだけのService実装です。 plugins/com.example.plugin/src/android/com/example/plugin/Service.java に配置します。

package com.example.plugin;

import java.util.Date;

import android.content.Intent;
import android.os.ConditionVariable;
import android.os.IBinder;

public class Service extends android.app.Service {

    boolean running = false;
    ConditionVariable condition;

    static final long WAIT_TIME = 1 * 1000;
    private final Runnable task = new Runnable() {
        public void run() {
            while(running) {
                android.util.Log.e(Service.class.getCanonicalName(), new Date().toString());
                condition.block(WAIT_TIME);
            }
        }
    };

    @Override
    public void onCreate() {
        running = true;
        Thread thread = new Thread( null, task, this.getClass().toString() );
        condition = new ConditionVariable( false );
        thread.start();
        android.util.Log.e(getClass().getCanonicalName(), "service start");
    }

    @Override
    public void onDestroy() {
        running = false;
    }

    @Override
    public IBinder onBind( Intent intent ) {
        throw new UnsupportedOperationException( "Not supported yet." );
    }

}

つづいて、JavaScriptからServiceを立ち上げるためのインターフェイスを用意します。 plugins/com.example.plugin/src/android/com/example/plugin/Plugin.java に配置します。

package com.example.plugin;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONArray;
import org.json.JSONException;

import android.app.Activity;
import android.content.Intent;

public class Plugin extends CordovaPlugin {
    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if (action.equals("startService")) {
            Activity activity = cordova.getActivity();
            activity.startService(new Intent(
                activity.getApplicationContext(),
                Service.class
            ));
            callbackContext.success();
            return true;
        } else {
            return false;
        }
    }
}

plugins/com.example.plugin/plugin.xmlを以下の内容で作成します。

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
    id="com.example.plugin" version="0.0.1">
    <name>Plugin</name>
    <description>plugin example</description>
    <license>MIT</license>
    <keywords></keywords>
    <platform name="android">
        <config-file target="res/xml/config.xml" parent="/*">
            <feature name="PluginExample" >
                <param name="android-package" value="com.example.plugin.Plugin"/>
                <param name="onload" value="true" />
            </feature>
        </config-file>
        <config-file target="AndroidManifest.xml" parent="/manifest/application">
            <service android:name="com.example.plugin.Service" />
        </config-file>
        <source-file src="src/android/com/example/plugin/Plugin.java" target-dir="src/com/example/plugin" />
        <source-file src="src/android/com/example/plugin/Service.java" target-dir="src/com/example/plugin" />
    </platform>
</plugin>

<plugin>要素のid属性には、プラグインディレクトリの名称と同じ文字列を指定してください。
<platform>要素にはpluginがインストール可能な各プラットフォームが必要な情報を記述する必要があります。この例ではandroidへの情報のみを記載しています。
<config-file>要素ではplatform/android以下のxmlファイルを改変するための情報を記述することが出来ます。
target属性で目的のファイルを指定し、parent属性で内容を書き込む対象となる要素を指定しています。”/*”というのは、任意のトップレベル要素に指定内容を書き込むという意味です。
上記の方法で指定されたXMLファイルの対象要素内に、<config-file>要素の子要素がそのまま書き込まれることになります。

上記例ではconfig.xmlにプラグインインターフェイスの登録設定と、AndroidManifest.xmlへのサービス登録設定が書き込まれています。
この方法で、Permissionの追加やReceiverの登録なども同様に行うことが出来ます。

また、<source-file>要素ではプラグインのディレクトリからplatform/androidのディレクトリへファイルをコピーするための情報を記述することが出来ます。

今回の例ではJavaソースコードのみをコピーしていますが、コピー対象のディレクトリにはsrc/以外を指定することも可能なので、同様にres/やlib/などに配置したいファイルを記述することで、ビルドに組み込むことが可能です。

以上でプラグイン側の実装は終了です。さいごにWebアプリケーションからプラグインのインターフェイスを利用するコードを追加します。

www/js/index.jsのdevicereadyイベントのコールバック部分に以下のコードを追加します。
devicereadyイベント取得後に実行しないと、cordovaオブジェクトが未初期化状態でエラーになります。

cordova.exec(
    function() {},
    function() {
        console.log('startService is failed.');
    },
    "PluginExample", "startService",
    []
);

ビルドと実行

プラグインをplatform/androidに追加します。プラグインの配置は、プラットフォームディレクトリの新規作成時にのみ行われます。

$ cordova platform remove android && cordova platform add android

cordova platform update android でプラグインのアップデートも行われるのではないかと期待したのですが、updateコマンドはAndroidSDKのバージョンアップが行われるだけのようで、プラグインの更新は行われませんでした。プラグインを編集した際は毎回プラットフォームディレクトリを作成し直すことになります。

ビルド

$ cordova build android

あらかじめ実機端末の接続またはエミュレータの起動を行っておいて、インストール

$ adb install -r platforms/android/ant-build/HelloCordova-debug.apk

アプリケーションを起動しlogcatで端末ログを表示すると、実装したServiceによって毎秒ログが出力されているのが確認できると思います。

ネイティブコードのテスト

ざんねんながら、現在はプラグインのテストを構成するきまった方法は無いようです。ただ、pluginディレクトリの中身はかなり自由な構成が可能なので、僕はsrc/android以下にpom.xmlファイルをおいてMaven Androidプロジェクトを構成しています。
最終的にsource-fileタグの設定によりプラットフォームディレクトリ内で所定の構成になれば、プラグインディレクトリはどのような配置でもいいようです。

Yeomanで作ったIonic+AngularJS+PhoneGapなWebアプリのKarma+mocha+chaiなテストをJenkinsで実行するよ!

タイトルの通り、Yeomanで作ったIonic+AngularJSなWebアプリをPhoneGapでネイティブアプリにするプロジェクトを構築し、 テストをmocha+chaiで記述してKarmaで実行できるようにし、Jenkinsで定期ジョブにする手順を記述するエントリです。

登場人物が多すぎて自分でも意味不明になってきたので、整理の意味でブログに書いときます。

用語

まずは上述の用語の乱雑なまとめ

Yeoman

アプリケーションのひな形を作成してくれるツール

grunt

ビルドツール

bower

javascriptライブラリの管理ツール

npm

node.jsライブラリの管理ツール

まだ使い始めたばかりなのでアレですが、Yeoman+grunt+bower+npmでJavaのMavenみたいなことが出来るという 理解でいいのかな。便利っぽいです。

Ionic

AngularJSに依存したモバイルWebアプリのUIライブラリ。 Phonegapでの利用も想定して作られているようなので、使ってみました。 AngularJSはただのWebアプリ用のフレームワークなので、モバイル端末に対応するためには 何らかのUIライブラリが必要です。

AngularJS

有名なJavascript MVCフレームワークです

Karma

テストランナーです

mocha

テストフレームワークです

chai

アサーションライブラリです

Javascriptだとテスト関係のライブラリが機能別に細かく分かれているのでややこしいですね。

PhoneGap

WebアプリをWebView上でで動くネイティブアプリにするライブラリ。 ハードウェア機能もJavascriptで使えるインターフェイスが用意されていてクロスプラットフォームなアプリを作れる

開発環境の構築

概念はややこしいですが、手順がワークフロー化されているので、やることは簡単です。

まずは一連の操作で利用するライブラリをインストールしましょう。rubyとかgemとかnpmとかはすでに入っているかんじで、

# gem install compass
# npm install -g bower grunt-cli yo yeoman-generator generator-ionicjs karma-junit-reporter
# npm install -g phonegap cordova

続いて、実際のプロジェクトを作成していきます。

$ mkdir ionicSample
$ cd ionicSample
$ yo ionicjs

    _             _
   (_)           (_)
    _  ___  _ __  _  ___
   | |/ _ \| '_ \| |/ __|
   | | (_) | | | | | (__
   |_|\___/|_| |_|_|\___|

[?] Would you like to use Sass with Compass (requires Ruby)? (y/N) y
Created a new Cordova project with name "IonicSample" and id "com.example.IonicSample"
[?] Which Cordova plugins would you like to include? (Press <space> to select)
‣⬢ org.apache.cordova.console
 ⬢ org.apache.cordova.device
 ⬡ org.apache.cordova.network-information
 ⬡ org.apache.cordova.splashscreen
 ⬡ org.apache.cordova.battery-status
 ⬡ org.apache.cordova.statusbar
 ⬡ org.apache.cordova.device-motion
[?] Which Cordova plugins would you like to include? org.apache.cordova.console, org.apache.cordova.device
[?] Which starter app template which you like to use? 
  Blank 
  Tabs 
  Side Menu 
‣ Pets Seed 

まず、Compassを使うかどうか、つづいてインポートするphonegapライブラリの選択を行います。 最後にひな形の種類を聞かれます。実際に開発する場合は余計なものが無い方がいいかもしれませんが、 ここではあるていどコンテンツが用意されているPet Seedを選択します。

パッケージビルドするプラットフォームを追加します。ここではandroidを扱います。 あ、AndroidSDKはすでに適当に入ってる感じでおねがいします。

$ cordova platform add android

ビルドします。

$ grunt build

これでもう、./platforms/android/ant-build に動作可能なパッケージができました。マジお手軽。

それでは、実行してみましょう。テスト用の端末をUSB接続するか、エミュレータを立ちあげておいて、

$ cordova run android

実行出来ましたか?実行出来ましたね。

テスト

単体テストを実行します。テストはすでに ./test/spec/ 以下にサンプルがありますので、これを実行します。 karmaといえば、ファイルの変更を検知して実行してくれる監視機能が特長のようですが、 ここでは、最終的にはJenkinsのジョブとして扱いたいので、コマンドライン実行します。

とはいえ、すでにすべての準備は整っていて、

$ grunt karma:continuous

Running "karma:continuous" (karma) task
WARN [karma]: Port 8080 in use
INFO [karma]: Karma v0.12.2 server started at http://localhost:8081/
INFO [launcher]: Starting browser PhantomJS
WARN [watcher]: Pattern "/path/to/ionicSample/test/mock/**/*.js" does not match any file.
INFO [PhantomJS 1.9.7 (Linux)]: Connected on socket jd_w9g872hnwnSfMdscZ with id 60562257
.
PhantomJS 1.9.7 (Linux): Executed 1 of 1 SUCCESS (0.039 secs / 0.001 secs)

=============================== Coverage summary ===============================
Statements   : 81.82% ( 9/11 )
Branches     : 100% ( 0/0 )
Functions    : 66.67% ( 4/6 )
Lines        : 81.82% ( 9/11 )
================================================================================

Done, without errors.


Execution Time (2014-04-01 08:45:17 UTC)
karma:continuous  1.6s  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 100%
Total 1.6s

できました。いたれり尽くせりすぎて怖い。

ちなみに、karmaの監視を開始する場合は grunt test とやってください。

テストのコマンドライン実行はできましたが、jenkinsにジョブ登録してテストの定期実行結果を評価するには テスト結果をファイル出力する必要があります。

JUnit形式Reporterの追加

$ npm install karma-junit-reporter --save-dev

Gruntfile の、karma.options.reporters を修正します

$ vi Gruntfile.js
reporters: ['junit', 'dots', 'coverage'],   #<- 'junit'を追加

再度テスト実行

$ grunt karma:continuous

JUnit形式の結果ファイル test-results.xml ができていると思います。

Jenkinsには、このテスト実行コマンドと結果ファイルを登録しておけば継続的インテグレーションができるようになります。

Geoserver extensionをScalaで作成する

地理情報をオープンデータ形式で配信するサーバーGeoserverのプラグイン “extension” をScalaで作成し、インストールします。

Geoserver extensionを作るのもScalaプログラムを書くのもほとんど初めてです。

とりあえず、公式チュートリアルの一番簡単そうなextension http://docs.geoserver.org/latest/en/developer/programming-guide/ows-services/implementing.html を、Scalaで書きなおしてみようと思います。

Geoserver extensionsのチュートリアルは大体mavenでビルドするように紹介されているので、流れに乗ってmavenプロジェクトを作成します。mavenでScalaプロジェクトを作成しなければなりません。

http://d.hatena.ne.jp/clash_m45/20120614/1339682235

とりあえず上記リンクの通りにやりました。

$ mvn archetype:generate

なにやらプロジェクトのひな形が大量にリストアップされますが、”scala”と打つとscalaプロジェクトのひな形だけがフィルタされます。org.scala-tools.architypes のやつを選択しました。

その後いろいろプロジェクト情報を聞かれますが、

Choose org.scala-tools.archetypes:scala-archetype-simple version: 
1: 1.0
2: 1.1
3: 1.2
4: 1.3
Choose a number: 4: 4
Define value for property 'groupId': : org.geoserver
Define value for property 'artifactId': : helloscala
Define value for property 'version': 1.0-SNAPSHOT: 
Define value for property 'package': org.geoserver: 
Confirm properties configuration:
groupId: org.geoserver
artifactId: helloscala
version: 1.0-SNAPSHOT
package: org.geoserver

こんな感じで入力して作成実行しました。

何かいい感じでプロジェクトが作成されたようなので、プロジェクトのディレクトリに移動しおもむろにビルドしてみます

$ mvn install

うまくビルドできれば問題なくプロジェクト作成されているのではないでしょうか。

さて、このひな形を元にgeoserver extensionを作成します。やることは、公式チュートリアルのpom.xmlを適当に修正することと、javaプログラムをScalaに書き直してパッケージに含めることです。

pom.xmlはチュートリアルの内容を追記すればいいです。

<!-- set parent pom to community pom -->
<parent>
    <groupId>org.geoserver</groupId>
    <artifactId>community</artifactId>
    <version>2.2.0</version>
</parent>

<!-- これらの行はすでに書き込まれていると思います
<groupId>org.geoserver</groupId>
<artifactId>hello</artifactId>
<version>1.0</version>
<name>Hello World Service Module</name>
-->
<packaging>jar</packaging>

<!-- declare depenency on geoserver main -->
<dependencies>
    <dependency>
      <groupId>org.geoserver</groupId>
      <artifactId>main</artifactId>
      <version>2.2.0</version>
    </dependency>
</dependencies>

<repositories>
    <repository>
       <id>opengeo</id>
       <name>opengeo</name>
       <url>http://repo.opengeo.org</url>
    </repository>
</repositories>

src/main/scala/HelloWorld.scala を作成します。

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

class HelloWorld {
  def sayHello(request: HttpServletRequest, response: HttpServletResponse){
    response.getOutputStream().write( "Hello World!!".getBytes() );
  }
}

また、チュートリアルにあるapplicationContext.xmlをsrc/main/resourses/に作成します。 チュートリアルではsrc/main/javaに作成されていますが、これに倣ってsrc/main/scalaに 配置してもパッケージに含まれないので注意してください。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
    <!-- Spring will reference the instance of the HelloWorld class
           by the id name "helloService" -->
    <bean id="helloService" class="HelloWorld">
        </bean>

    <!-- This creates a Service descriptor, which allows the org.geoserver.ows.Dispatcher
           to locate it. -->
        <bean id="helloService-1.0.0" class="org.geoserver.platform.Service">
    <!-- used to reference the service in the URL -->
        <constructor-arg index="0" value="hello"/>

        <!-- our actual service POJO defined previously -->
        <constructor-arg index="1" ref="helloService"/>

        <!-- a version number for this service -->
        <constructor-arg index="2" value="1.0.0"/>

        <!-- a list of functions for this service -->
        <constructor-arg index="3">
            <list>
                <value>sayHello</value>
            </list>
        </constructor-arg>

        </bean>
</beans>

こうなっているはずです

helloscala/
  + pom.xml
  + src/
    + main/
      + scala/
        + HelloWorld.scala
      + resources/
        + applicationContext.xml

ビルドします

$ mvn clean install

target/helloscala-1.0-SNAPSHOT.jar ができているでしょうか。 これをgeoserverのWEB-INF/lib/ディレクトリにコピーします。 また、Scalaで作成されたクラスファイルを実行するためにscala-library.jarが必要なので、 これもどこかから拾ってきて同じディレクトリに配置してください。 以上ができたら、geoserverを(再)起動します。

ows?request=sayHello&service=hello&version=1.0.0

にアクセスして “Hello World!!” と表示されれば成功です。

こんな感じで他のチュートリアルもScalaで書いてみたりしています。
https://github.com/haiiro-shimeji/geoserver-extension-scala

JavaからScalaプログラムの呼び出しが可能なことを利用して、Geoserver extensionをScalaで作成しました。 これで記述性の高いScalaでgeoserver開発が出来るようになるはずです。Scalaは今から勉強します。

PhoneGap+OpenLayersでAndroid用地図アプリを作成する

Androidで地図アプリを作成しようとする場合、真正面から取り組むとWMS取得、描画プログラムを自前で実装することになり非常にめんどくさいです。

ですが、Javascriptで実績を残しているライブラリOpenLayersをPhonegap上で動作させれば、悲しいほど簡単に実現することができます。

前提条件

AndroidSDKをインストールしておきましょう。
最新のPhonegap(記事投稿時点で2.2.0)を利用するためにAPIバージョンは15以上をインストールしましょう。

必要なライブラリを取得します

http://phonegap.com/
http://openlayers.org/

PhonegapのAndroid exampleをコピーします

# phonegapパッケージを解答してできたディレクトリにいるとして、
cp -pri lib/android/example/ ~/cordova-example 

設定ファイルを書き変えます

コピーしてできたディレクトリにある2つのファイルを書き変えます。

local.properties を作成し、

sdk.dir=[AndroidSDK がインストールされているディレクトリの絶対パス]

と書き込みます。

project.properties を編集します。

target=android-15

“15”はインストールされている別のAPIバージョンにしてもいいです。

OpenLayersをパッケージに入れます

OpenLayersを解凍したディレクトリに入っている OpenLayers.jsとthemesディレクトリを、assets/www/ ディレクトリ以下にコピーします。

assets/www/index.html を書き変えます

<!DOCTYPE html>
<html>
    <head>
            <title>OpenLayers Map</title>
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
            <meta name="format-detection" content="telephone=no" />
            <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
            <link rel="stylesheet" href="OpenLayers/theme/default/style.css" type="text/css">
    <link rel="stylesheet" href="OpenLayers/theme/default/google.css" type="text/css">
            <script src="cordova-#########js"></script>
            <script src="OpenLayers/OpenLayers.js"></script>
            <script type="text/javascript">
                document.addEventListener('deviceready', function() {
                    var map = new OpenLayers.Map( {
                        div: "map",
                    } );
                    map.addControl(new OpenLayers.Control.Zoom());
                    map.addControl(new OpenLayers.Control.Navigation({'zoomWheelEnabled': true}));
                    map.addControl(new OpenLayers.Control.KeyboardDefaults());
                    map.addControl(new OpenLayers.Control.TouchNavigation());

                    map.addLayer( new OpenLayers.Layer.OSM( "Simple OSM Map") );

                    //  とりあえず東京を中心にする
                    map.setCenter(
                        new OpenLayers.LonLat( 13###764772, 3###681610 ).transform(
                                new OpenLayers.Projection("EPSG:4326"),
                                map.getProjectionObject() ), 
                        12 
                    );

                 }, false);

            </script>
    </head>
    <body>
        <div id="map" style="position: absolute; left: 0; top: 0; right: 0; bottom: 0">
            <div id="location" style="position: absolute; z-index: 65535;"></div>
        </div>
    </body>
</html>

ビルド、インストールします

事前にインストール対象のデバイスをデバッグ接続しておくか、エミュレータをたちあげておきましょう

$ ant install debug

jQuery Deferred の使い方がよくわからない

jQuery Deferred の使い方がよくわからないので、まったく今更なのですが調べた内容のメモ書きです。

処理を順次実行する (基本的な使い方)

非同期でない普通の処理を順次実行します。この例ではDeferredオブジェクトのありがたみは全くありません。

$.Deferred()
.done( function( arg ) {
    console.log("1:" + arg)
} )
.done( function( arg ) {
    console.log("2: " + arg)
} )
.resolve("hogehoge")

コールバックの処理結果を次のコールバックに与える

ただ単に処理を羅列しても面白くもなんともないので、前の処理結果を次の処理に渡して参照します。

$.Deferred()
.resolve("hogehoge")
.pipe( function( arg ) {
    console.log(arg)
    return "fugafuga"
} )
.done( function( arg ) {
    console.log("1st task returns " + arg)
} )

pipe()に与えたコールバックが返した値は、次のコールバックに渡されます。また、done()はthis返すのに対し、pipe()はthisとは別のPromiseオブジェクトを返します。promiseオブジェクトはresolve(), reject(), promise()などの実行状態を変化させるメソッドが定義されていません。

最初の例ではresolve()をコールバック登録後に読んでいるのに対し上の例では登録前に読んでいます(最後に呼ぶとPromiseオブジェクトに対する呼び出しになってしまうため)が、この順番はあまり関係がありません。resolve()の前に追加されたコールバックでも後に追加されたコールバックでも関係なく登録された順番に実行されていくようになっています。これはDeferredオブジェクトの内部でコールバックを管理している、$.Callbacksオブジェクトの実装によるものです。

あとから処理を追加する

一旦実行状態に入ったDeferred Objectにあとからコールバックを追加しても実行されるので、下のようにバックグラウンドのタスクキューみたいな使い方もできます。

非同期処理をチェーンする

pipe()に与えたコールバックからpromiseオブジェクトを返すと、次のコールバックはその実行を待って実行されます。promiseオブジェクトのresolve()の引数に与えた値は、次のコールバックの引数になります。

$.Deferred()
.resolve("hogehoge")
.pipe( function( arg ) {
    return $.ajax( {
       url: url
    } )
 } )
.done( function( arg ) {
    console.log(arg)
} )
.pipe( function() {
    return $.Deferred( function(d) {
        setTimeout( function() { 
            d.resolve("fugafuga") 
        }, 1000 )
    } ).promise()
} )
.done( function( arg ) {
    console.log(arg)    //fugafuga
} )

$.ajax()のjQueryの非同期処理メソッドはだいたい(?)promiseオブジェクトを返すので、pipe()に与えたコールバックの戻り値として返せばよいです。jQuery Deferredと関連のない非同期処理の場合は、3つめのコールバックのように、あとからresolve()またはreject()される予定のpromiseオブジェクトを返します。

処理が途中で失敗したとき

ここまでは全てのコールバックが成功する前提で適当に組んでいましたが、人生何もかもうまくいくはずはなく当然処理の失敗というのも考えられます。$.ajax()などの非同期メソッドでは、処理に失敗すると戻り値のpromiseオブジェクトのreject()が呼び出されます (下のコードでは例のために自分で作ったpromiseオブジェクトのreject()を読んでいます)。

$.Deferred()
.resolve("hogehoge")
.pipe( function() {
    return $.Deferred( function(d) {
        setTimeout( function() {
            //何らかの理由で失敗してしまった場合、reject()を呼ぶ
            d.reject("fugafuga") 
        }, 1000 )
    } ).promise()
} )
.fail( function( arg ) {
    //reject()が呼び出されたDeferredオブジェクトは、fail()で登録されたコールバックが順次実行される
} )
.done( function( arg ) {
    //done()で登録されたコールバックは実行されない
} )
.pipe( 
    function() {
        //pipe()の第一引数は成功時コールバック。この場合は呼ばれない
    },
    function() {
        //第二引数は失敗時コールバック。こちらが呼ばれる。
        return "error message";
    }
)
.fail( function( errorMessage ) {
    alert( errorMessage )
} )
.always( function() {
    //always()で登録したコールバックは成功失敗いずれの場合にも呼ばれる。
} )

処理が失敗した場合に行う処理はfail()メソッドをつかってコールバック登録します。また、pipe()も第二引数で失敗時コールバックを指定することができます。成功時コールバックと同様に、次の処理に値またはpromiseオブジェクトを引き渡すことができます。また、always()を使うと、同じコールバックをdone()とfail()で同時に登録してくれます。finally処理のように使うことができます。

処理が途中で失敗したがリカバリして成功ルートに戻る

上の例では、処理が途中で失敗した場合は成功ルートを踏み外しエラー処理をひたすら行なっていくしかないですが、実際には取得に失敗した値についてデフォルト値を使うなどでリカバリしてそのまま処理を継続したい場合があります。

$.Deferred()
.resolve()
.pipe( function() {
    return $.ajax( {
        url: "/resource"
    } )
} )
.pipe(
    function( data ) {
        return _parse( data )
    },
    function() {
        //取得に失敗した場合はデフォルト値を使い、正常処理をつづける
        return $.Deferred().resolve( DEFAULT )
    }
)
.done( function( arg ) {
    //ajaxは失敗したが、pipeされたDeferredオブジェクトでresolve()が呼ばれたので成功ルートに戻った
} )
.pipe( 
    function( data ) {
        return $.ajax( {
            url: data
        } )
    }
)

処理をフォークする

$.when() は複数のPromiseオブジェクトを引数に取り、それら全ての実行終了を待ち合わせるPromiseオブジェクトを返します。下の例では、ふたつのajax通信が成功するとdoneのコールバックに処理が移ります。doneのコールバックには非同期処理の結果が、when()に渡したオブジェクトの順番で引数として与えられます。

いずれかの取得が失敗した場合、ただちにfail()に与えられたコールバックに処理が移ります。このとき他の通信が成功していたとしても全ての結果が取得できない点がポイントです。

$.when( 
    $.ajax( {
        url: url1,
        data: data1
    } ),
    $.ajax( {
        url: url2,
        data: data2
    } )
)
.done( function( resultFromAjax1, resultFromAjax2 ) {
        console.log("ajax1 returns " + resultFromAjax1[0])
        console.log("ajax2 returns " + resultFromAjax2[0])
} )
.fail( function( xhr, status, error ) {
        console.log("failed: " + status)
} )

失敗しても他の結果は取得したい場合はこんな感じですかね。。

$.when( 
    $.ajax( {
        url: url1,
        data: data1
    } )
    .pipe(
        function( result, status, xhr ) { 
            return $.Deferred().resolve( result, status, xhr ) 
        },
        function( xhr, status, error ) {
            return $.Deferred().resolve( DEFAULT, status, xhr )
        }
    ),
    $.ajax( {
        url: url2,
        data: data2
    } )
    .pipe(
    function( result, status, xhr ) { 
            return $.Deferred().resolve( result, status, xhr ) 
        },
        function( xhr, status, error ) {
            return $.Deferred().resolve( DEFAULT, status, xhr )
        }
    )
)
.done( function( resultFromAjax1, resultFromAjax2 ) {
        console.log("ajax1 returns " + resultFromAjax1[0])
        console.log("ajax2 returns " + resultFromAjax2[0])
} )
.fail( function( xhr, status, error ) {
        console.log("failed: " + status)
} )

重複コードができてしまうので、Promiseオブジェクト生成の部分は $.map() とかを使ってまとめるとよいです。

非同期処理をいっぱいチェーンする

$.when() は、ある配列について各要素に対し非同期処理を並列実行します。順次実行したい場合は以下のようにします。

var defer = $.Deferred()
            .resolve([])
var piped = defer

$.each( urlArray, function( i, url ) {
    piped = piped.pipe( function(results) {
        return $.ajax( {
           url: url
        } )
        .pipe( function( data, status, xhr ) {
            results.push(data)
        } )
    } )
}

piped.pipe( function( results ) {
    $.each( results, function( i, result ) {
        console.log(result)
    } )
} )

then()とは何だったのか

jquery ver1.8.3 の実装では、pipe()はthen()のエイリアスということになっています。ドキュメントではthen()はむしろdone(), fail(), notify()と互換であるようなことが書かれていますが、then()とdone(), fail(), notify()では戻り値が違うので同じだと思って使うとアレなことになりそうです。

$.Deferred()
.then( function( arg ) {
    return "fugafuga"   //次のコールバックに値を与えられる
} )
.done( function( arg ) {
    console.log(arg)
} )
.resolve("hogehoge")    //Promiseオブジェクトのresolve()呼び出しはerrorになる