Cordova PluginのJavaScript部分の実装

# イントロダクション

Cordova Pluginを実装する場合のJavaScript部分の記述方法について、まとめました。

Cordovaプラグインについての記事

- [基本事項](https://qiita.com/KNaito/items/4a49ba098115085d74fb)
- [JavaScript部分の実装](https://qiita.com/KNaito/items/af7b61f83888c95f0c7b) <- _この記事_
- [Native部分の実装iOS] ... TODO
- [Native部分の実装Android] ... TODO
- [フックスクリプト](https://qiita.com/KNaito/items/65587f5d51974e8b4adf)
- [プラグインのテスト] ... TODO

# plugin.xmlへの記述法

Pluginとして組み込みたいjsファイルを指定して、 `js-module`タグで記述します。
例えば、`cordova-plugin-camera`であれば次のようになっています。

```xml:plugin.xml
    <js-module src="www/CameraConstants.js" name="Camera">
        <clobbers target="Camera" />
    </js-module>

    <js-module src="www/CameraPopoverOptions.js" name="CameraPopoverOptions">
        <clobbers target="CameraPopoverOptions" />
    </js-module>

    <js-module src="www/Camera.js" name="camera">
        <clobbers target="navigator.camera" />
    </js-module>
```

ここでは、3つのjsファイルを組み込んでいます。このように、一つのプラグインで複数のjsファイルを読み込むことも出来ますが、通常は、組み込むのjsファイルは1つだけあれば十分です。

- src属性で、組み込むJSファイルへのパスを記述します。

- name属性は、そのjs-moduleのIDとして利用されます。ただし、アプリの開発者がjs-moduleのIDを直接参照することはないので、特に意識する必要はありません。js-moduleのIDは、`「プラグインのID」.「name属性で指定した文字列」`となります。例えば、上記のカメラプラグインの例であれば、3つのjs-moduleについて、IDはそれぞれ
 
 - `cordova-plugin-camera.Camera`
 - `cordova-plugin-camera.CameraPopoverOptions`
 - `cordova-plugin-camera.camera`
 
 となります。prepare後に生成される `www/cordova-plugins.js`の中で、上記が定義され、cordova.jsから読み込まれます。
 
- js-module内で設定しているclobbersは重要です。srcで読み込まれたJSモジュール(後述)は、このclobbersのtargetで設定したオブジェクトとして参照出来るようになります。これについては、次節で詳しく説明します。

## js-moduleとclobbersの関係

js-moduleのsrcタグで指定するモジュール(jsファイル)は、Common-JS形式のモジュールと似ていて、次のような形式になります。

```js
module.exports = [オブジェクト]
```

例えば、上記のカメラプラグインの3番目のjs-moduleを見てみると、次のような形式になっています。

```js
var cameraExport = {};
cameraExport.getPicture = function (successCallback, errorCallback, options) {
...
}
module.exports = cameraExport;
```

そして、module.exportsしたオブジェクトは、clobbersのtargetで設定したオブジェクトとして参照出来るようになるため、このサンプルでは、`navigator.camera`が、`cameraExport`となり、結局、次のように呼び出せます。

```js
navigator.camera.getPicture( ... );
```

## clobber以外のオプション

js-moduleタグの中に記述するのは、clobber以外に、mergesとrunsがあります。

## merges

clobbersとほぼ同様で、次のように`target`属性と共に記述して使います。

```html
    <js-module src="www/CameraConstants.js" name="Camera">
        <merges target="window.cordova" />
    </js-module>
```

この例では、window.cordovaオブジェクトに組み込まれて参照出来るようになります。clobbersとの違いは、すでに存在しているwindow.cordovaオブジェクトに、このモジュールで定義している`getPicture`などのプロパティを追加するところです。

## runs

属性を指定せずに次のように記述します。

```html
    <js-module src="www/CameraConstants.js" name="Camera">
        <runs />
    </js-module>
```

runsを指定した場合、モジュールのプロパティを参照するためのオブジェクトが用意されません。cordova.requireを使って、自前で参照することになります。


# モジュールの書き方

## ネイティブコードの呼び出し

ネイティブコードの呼び出しは、`cordova.exec`を使います。次のような形式です。

```
    cordova.exec(success, failure, '【プラグイン（のフィーチャー）名】', '【メソッド名】', [【引数】]);
```

プラグイン（のフィーチャー）名は、plugin.xmlの`config-file`タグの中の`feature`タグの`name`属性でつけた値です。例えば、Androidで次のようになっていたとすると、

```
    <config-file target="res/xml/config.xml" parent="/*">
      <feature name="HelloWorldPlugin">
        <param name="android-package" value="com.example.plugin.HelloWorldPlugin"/>
      </feature>
    </config-file>
```

`HelloWorldPlugin`というのが、プラグイン（のフィーチャー）名になります。

メソッド名は、呼び出すメソッド名です。Androidの場合、ネイティブ側のプラグインクラスの`execute`メソッドの第一引数である`action`変数に、メソッド名が引き渡されます。コードで書くと、次のようになります。

```
public class HelloWorldPlugin extends CordovaPlugin {
  ...
    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
       // このactionに、メソッド名が格納される。
       // argsに、引数が格納される。
       ...
    }
}
```

iOSの場合は、Androidと同様、plugin.xmlの`config-file`タグの中の`feature`タグの`name`属性でつけた値です。例えば、

```
        <config-file target="config.xml" parent="/*">
            <feature name="HelloWorldPlugin">
                <param name="ios-package" value="CDVHelloWorldPlugin"/>
            </feature>
        </config-file>
```

`HelloWorldPlugin`というのが、プラグイン（のフィーチャー）名になります。

メソッド名は、iOSの場合、ネイティブ側のメソッドそのものになります。上記の例では、CDVHelloWorldPluginクラスのメソッドが直接呼ばれるので、例えば

```
    cordova.exec(success, failure, '【プラグイン（のフィーチャー）名】', 'myfunc', [【引数】]);
```

で呼び出すと、

```
@implementation CDVHelloWorldPlugin

...
- (void) myfunc:(CDVInvokedUrlCommand*) command
{
  // command.argumentsに、引数が格納される
  ...
}
```

として定義されたmyfuncメソッドが直接呼ばれます。このとき、myfuncメソッド自体の引数はかならず、`CDVInvokedUrlCommand *`になります。jsから渡された引数は、`command.arguments`で取得します。


## argsCheck

 TODO

## channelとpub/sub
 
 channelを使うことで、pub/subを実現することが出来ます。例えば、cordovaが利用可能になった時に何か実行させたい場合、次のように実装します。

```js
var channel = require('cordova/channel');
channel.onCordovaReady.subscribe(function() {
 // 起動時の処理
});
```

`onCordovaReady`イベントが発火した時に実行する処理を、`subscribe`メソッドで定義しています。

このように定義すると、`onCordovaReady`イベントが発火したときに、定義した処理が自動的に実行されます。

後述するように、onCordovaReadyイベントはスティッキーイベントであるため、`subscribe`を定義する以前にonCordovaReadyイベントがすでに発火済みだった場合、`subscribe`するとすぐにその処理が実行されます。

channelで定義されているイベントには、他にも次のようなものがあります。

- onDOMContentLoaded
- onNativeReady
- onCordovaReady
- onPluginsReady
- onDeviceReady
- onResume
- onPause

`onNativeReady`, `onPluginsReady`が発火した後、`onCordovaReady`がイベントが発火します。
また、`onDOMContentLoaded`、`onCordovaReady`が共に発火すると、`onDeviceReady`イベントが発火します。(他にも、`waitForInitializationで登録されたイベントがあると、それらすべてが発火後に、`onDeviceReady`イベントが発火します）

少し紛らわしいですが、'onCordovaReady`イベントと、`onDeviceReady`イベントは別のイベントであることに注意してください。

なお、`onDOMContentLoaded`イベントと、最後に`Ready`が付いているイベントは、スティッキーイベントであり、一度そのイベントが発火すると、以後は発火したままの状態となります。(発火されたままのイベントに対しては、イベント発火後にsubscribeするとただちに実行されます。スティッキーでないイベントは、発火後にsubscribeしても実行されません）

詳しくは、https://github.com/apache/cordova-js/blob/c75e8059114255d1dbae1ede398e6626708ee9f3/src/common/channel.js を参照してください。

### channelイベントを自前で作成する

cordovaでデフォルトで定義されている上記のイベント以外に、自前で新規にイベントを作成することも出来ます。
例えば、cordova-plugin-mediaプラグインでは、次のように`onMediaPluginReady`イベントを作成し、利用しています。(このコードは、プラットフォームがandroid, amazon-fireos, windowsphoneときのみ読み込まれますので、iOSでは実行されません）

```js
    var channel = require('cordova/channel');

    channel.createSticky('onMediaPluginReady');
    channel.waitForInitialization('onMediaPluginReady');

    channel.onCordovaReady.subscribe(function() {
        exec(onMessageFromNative, undefined, 'Media', 'messageChannel', []);
        channel.initializationComplete('onMediaPluginReady');
});
```

`createSticky`メソッドで、スティッキーなイベント(一度発火したら、発火したままの状態になる)が作られます。(スティッキーでないイベントを作る場合は、`create`メソッドを使います)

`waitForInitialization`で、onMediaPluginReadyイベントを、初期化待ちイベントとして追加します。初期化待ちイベントがすべて発火しないと、cordovaのdevicereadyイベントは発火しません。すなわち、このように実装することで、deviceready時にonMediaPluginReadyイベントはすでに発火済みであることを保証することが出来ます。

最後の行は、`onCordovaReady`イベントが発火後に、ネイティブコードの呼び出し

```js
    exec(onMessageFromNative, undefined, 'Media', 'messageChannel', []);
```

が行われ（非同期なので、呼び出しのみ。Nativeからの呼び出しを受け付けるonMessageFromNativeメソッドを登録している）、その後、

```js
    channel.initializationComplete('onMediaPluginReady');
});
```

により、onMediaPluginReadyイベントを発火しています。
（ここではこのようにしましたが、これだとexecの処理が完了する`onMediaPluginReady` イベントが発火してしまうので、外部にメッセージを送っておくりっぱなし（レスポンスを気にしない）ような処理はいいのですが、execの処理の結果を使ったこともしておきたいのであれば、`onMessageFromNative`関数の内部で`onMediaPluginReady`イベントを発火してください）


# モジュールの組み込まれ方と開発作業

プラットフォームごとに違いますが、基本的な構造は同じなので、iOS (cordova-ios 4.5.4)の場合を例にとって説明します。

プラグインのjs-moduleで設定したコードは、`plugin add`もしくは`platform add`を実行した時点で、`platforms/ios/platform_www/plugins/プラグイン名/www`の下にコピーされます。このファイルは、`cordova prepare`を実行しても変更されないので、開発時にはこのファイルを編集すると良いでしょう。

```
platforms
 ┗ ios
    ┗ platform_www
       ┗ plugins
          ┗ プラグイン名
             ┗ www
                ┗ モジュールのファイル名.js
```

なお、このファイルは厳密にはjs-moduleのsrcで指定しているファイルそのものではなく、一行目と最終行にコードが追加され、次のようになっています。

```js
cordova.define("[モジュールのID]", function(require, exports, module) {

 // js-moduleのsrcで指定したファイルの中身

});
```

そのため、開発時にこのコードを編集し、完成後にプラグイン側にフィードバックする場合(js-moduleで設定したファイルとして保存する場合)、一行目と最終行目を除いておく必要があるので注意です。

### cordova prepareの実行

`cordova prepare`を実行すると、`platforms/ios/platform_www/plugins/プラグイン名/www`配下のコードは、`platforms/ios/www/plugins/プラグイン名/www`配下にコピーされます。実際にアプリに組み込まれるのはこちらのファイルです。

```
platforms
 ┗ ios
    ┗ www
       ┗ plugins
          ┗ プラグイン名
             ┗ www
                ┗ モジュールのファイル名.js
```

こちらは、`cordova prepare`するたびに上書きされてしまうので、デバッグ用やテスト用のコードなど、ちょっとだけ確認したいをここに記述しても良いかもしれません。