Monaca/CordovaでCodePushを使ってみる

# これはなに？

[Monaca Advent Calendar 2018](https://adventar.org/calendars/3073) 16日目の記事です．

Monaca / Apache Cordova（以下，単に「Cordova」）でCodePushを扱う機会があったので，概要と手順をまとめてみました．

# 環境

* ローカルPC
    * Mac OS X 10.13.6
    * Node.js 10.12.0
    * npm 6.4.1
    * cordova-code-push-plugin 1.11.15
    * appcenter-cli 1.1.5
    * Cordova 7.1.0
    * cordova-android 6.4.0
    * cordova-ios 4.5.4
* Monaca Cloud
    * Cordova 7.1.0
    * cordova-android 6.4.0
    * cordova-ios 4.5.4

# CodePushの概要

CodePushは，Microsoft社が開発している，OTAアップデートをサポートするためのプラットフォーム．
開発プラットフォームとしては，CordovaとReact Nativeをサポートしている．

ここで，「OTA（Over The Air）アップデート」とは，App Store，Google Play Storeなどの公式ストアを介さず，配信サーバから直接ユーザのアプリをアップデートする技術を指す．
CordovaやReact Nativeなどのプラットフォームでは，ガワのHTML/CSS/JavaScriptの部分[^1]について，（大きく機能を変更しない場合に限り，）例外的に公式ストアを介さない形での配信が認められている．

> According to section 3.3.2 of Apple’s developer agreement, as long as you are using the CodePush service to release bug fixes and improvements/features that maintain the app’s original/presented purpose (i.e. don’t CodePush a calculator into a first-person shooter), then you will be fine, and your users will be happy. In order to provide a tangible example, our team published a (pretty cheesy!) CodePush-ified game to the Google Play Store and Apple App Store, and had no problems getting it through the review process.

https://microsoft.github.io/code-push/faq/index.html

> 3.3.2 Except as set forth in the next paragraph, an Application may not download or install
executable code. Interpreted code may only be used in an Application if all scripts, code and
interpreters are packaged in the Application and not downloaded. The only exceptions to the
foregoing are scripts and code downloaded and run by Apple's built-in WebKit framework or
JavascriptCore, provided that such scripts and code do not change the primary purpose of the
Application by providing features or functionality that are inconsistent with the intended and
advertised purpose of the Application as submitted to the App Store.

https://developer.apple.com/programs/information/Apple_Developer_Program_Information_8_12_15.pdf (Apple Developer Program Information)

OTAアップデートにより，軽微な変更やバグ修正，一時的な文言追加など，アプリの運用に役立つ機能をすばやく実現できるようになる．

CodePushの特徴として，

* 無料での利用が可能
* 自前で配信サーバを用意する必要はなく，CLIを介してCodePush側の配信サーバと通信し，アップデート操作を行う

がある．

https://microsoft.github.io/code-push/
https://appcenter.ms/

以下，Android/iOSアプリを例として，Monacaにて作成したアプリのOTAアップデートを試してみる．

# 注意点

* CodePushでは（おそらく他のOTAアップデート技術でも），Cordova Pluginの追加/削除/設定変更はできない．これらを行いたいときは，公式ストアを介したリリースを行う必要がある．
* CodePushは，以前は独立したサービスとして運営していたが，最近Visual Studio App Centerに統合され，現在はその機能の一部として提供されている．古い記事を参照する際は注意．
* CordovaでCodePushを利用するためには，専用のCordova Plugiunをプロジェクトにインストールする必要があるが，Monacaでサードパーティ製のCordova Pluginを利用したい場合には，Proプラン以上の契約が必要となる（逆に，Cordovaのみで開発を進めるのであれば無料でできる．その場合はこの記事を適宜読み替えるとよい）． https://ja.monaca.io/pricing-detail.html
* Monacaの契約プランによっては，Monaca In-Appアップデータープラグインを利用でき，こちらを使ってCodePushとほぼ同様の要件が実現できる．こちらを使う際には，配信サーバを自前で用意する必要があることに注意．[^2] https://docs.monaca.io/ja/reference/power_plugins/in-app_updater/

# Visual Studio App Centerアカウント作成とアプリの登録

1. http://appcenter.ms
から，Visual Studio App Centerのアカウントを作成する（Github，Microsoft，Facebook，Googleアカウントが連携できる）．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/885d3eb2-4a10-f725-6615-0b9cc3590dc0.png)

2. （必要あれば）`Add organization`からOrganizationを作成する．
Organizationは，他アカウントとプロジェクトを共有するために必要．逆に，作成したいプロジェクトが個人プロジェクトであれば必要ない．

    例では，`foo-org`というOrganizationを作成している．
    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/c0399acc-3f64-5235-83f3-9395e779a71f.png)

3. `Add new app`からAppを作成する．
Appは，CodePushにおける配信の単位に相当する．
Appごとに，配信対象のOSと開発プラットフォームを設定する．

    今回はCordovaを用いたハイブリッドアプリが対象なので，Appも1つでもいいような気がするが，Android/iOSでは，実際にアップデートの際に配信されるコンテンツが異なるため，別のAppを作成した方がよいと思われる．

    > Using the same app for Android and iOS may cause installation exceptions because the CodePush update package produced for iOS will have different content from the update produced for Android.

    https://docs.microsoft.com/en-us/appcenter/distribution/codepush/cli

    例では，foo-orgの管理下に，`awesome-app-ios`（iOS向け），`awesome-app-android`（Android向け）の2つのAppを作成している．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/dcb2cca6-f8d2-f0c6-f7e4-ef55b6efb8f7.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/c416f525-5425-3d4f-230d-5160b90daf63.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/7ddda9e6-7eeb-9fa6-5b3a-d42d75b3ad29.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/d7d5aa05-6492-46bd-2566-e6c6a4650f3a.png)

# appcenter-cliの導入と設定

基本的に下記記事に従う．
https://docs.microsoft.com/en-us/appcenter/distribution/codepush/cli

1. Node.jsとNPMをインストールする．
詳細は省略．

2. appcenter-cliをインストールする．

    ```
    $ npm install -g appcenter-cli
    ```

3. Visual Studio App Centerアカウントでログインする．

    ```
    $ appcenter login
    Opening your browser...
    ? Visit https://appcenter.ms/cli-login?hostname=P51195 and enter the code:
    ? Access code from browser:
    ```

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/4b2951a8-571f-dc7f-9ce9-48ec55fae5db.png)

    前項まででVisual Studio App Centerにログインしていれば，Webブラウザが開き，上記のように認証トークンが表示される．これをコピーしてターミナルに貼り付けてEnter．

    ```
    Logged in as <username>
    ```

    と出力されればOK．

    ここで，確認のため，

    ```
    $ appcenter apps list
    ```

    と入力して，

    ```
      foo-org/awesome-app-android
      foo-org/awesome-app-ios
    ```

    のように，`<Organizationの名前（Organizationを作成した場合） or アカウントの名前（Organizationを作成しなかった場合）>/<Appの名前>`がすべて表示されることをチェックする．

4. `Deployment`を作成する．
Deploymentは，`Name`（配信環境の名前）と`Key`（Nameに対応する秘密鍵．Deployment作成時に生成される）からなり，例えば，開発環境と本番環境で配信されるコンテンツを分けたいときに利用する．

    例では，`foo-org/awesome-app-ios`，`foo-org/awesome-app-android`のそれぞれに，`Staging`と`Production`という2つのDeploymentを作成する．

    ```
    $ appcenter codepush deployment add -a foo-org/awesome-app-ios Staging
    $ appcenter codepush deployment add -a foo-org/awesome-app-ios Production
    $ appcenter codepush deployment add -a foo-org/awesome-app-android Staging
    $ appcenter codepush deployment add -a foo-org/awesome-app-android Production
    ```

    下記のようにして，現在のDeploymentのNameとKeyを確認できる．

    ```
    $ appcenter codepush deployment list -a foo-org/awesome-app-ios --displayKeys
    ┌────────────┬───────────────────────────────────────┐
    │ Name       │ Key                                   │
    ├────────────┼───────────────────────────────────────┤
    │ Production │ XXXX                                  │
    ├────────────┼───────────────────────────────────────┤
    │ Staging    │ XXXX                                  │
    └────────────┴───────────────────────────────────────┘
    $ appcenter codepush deployment list -a foo-org/awesome-app-android --displayKeys
    ┌────────────┬───────────────────────────────────────┐
    │ Name       │ Key                                   │
    ├────────────┼───────────────────────────────────────┤
    │ Production │ XXXX                                  │
    ├────────────┼───────────────────────────────────────┤
    │ Staging    │ XXXX                                  │
    └────────────┴───────────────────────────────────────┘
    ```

    ここで得たDeployment Keyは，Cordova Pluginの組み込みの際に用いるので控えておく．

    これでアカウントまわりの設定はOK．

# Cordova Pluginの組み込み

CodePushをCordova上から利用するために，Pluginが用意されている．
https://github.com/Microsoft/cordova-plugin-code-push

これを，下記の手順にしたがってCordovaプロジェクトに組み込む．
https://github.com/Microsoft/cordova-plugin-code-push#getting-started
https://github.com/Microsoft/cordova-plugin-code-push#plugin-usage

1. Monacaを用いてテンプレートからCordovaプロジェクトを作成する．

    例では，`CodePush sample`という名前の空プロジェクトを作成している．
    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/c9e588bb-08d6-080c-b501-6f6a697c89c2.png)

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/4a7c2f6d-5066-1b83-f92a-f6de7a34ef8a.png)

2. Localkitを利用して，作成したプロジェクトをローカルPCにインポートする．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/e1bce3d8-89b8-74db-8fa9-0ffc8b0507e6.png)

3. ダウンロードしたばかりのプロジェクトはplatformを含んでいないので，これらを追加しておく．

    ```
    $ cd /path/to/your/cordova/project
    $ cordova platform add android
    $ cordova platform add ios
    ```

4. CordovaアプリをCodePushに対応させるためのPlugin（`cordova-plugin-code-push`）をインストールする．

    ```
    $ cordova plugin add cordova-plugin-code-push@latest
    ```

    一点，Monacaでは，Monaca Cloud IDEや，Monaca Localkitの`Remote Build`画面からもCordovaのサードパーティPluginをインストールすることをサポートしているのだが，この方法だと，なぜか別のPluginがインストールされてしまうらしく，うまく行かなかったので注意[^3]．

5. https://docs.microsoft.com/en-us/appcenter/distribution/codepush/cordova#getting-started にしたがって，`config.xml`にPluginの設定をしていく．

    例では，`foo-org/awesome-app-ios`，`foo-org/awesome-app-android`のそれぞれの`Staging`環境のDeployment Keyを入力することとする．

    ```config.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <widget xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" id="com.example.helloworld" version="1.0.0">
      <name>Minimum Template</name>
      <description/>
      <author/>
      <content src="index.html"/>
      <access origin="*"/> <!-- CodePushのサーバと通信するために必要．テンプレートのままなら変更の必要なし -->
      ...
      <meta http-equiv="Content-Security-Policy" content="default-src https://codepush.azurewebsites.net 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *" /> <!-- CSPへの対応に必要　-->
      ...
      <platform name="ios">
        ...
        <!-- CodePushのDeploymentとアプリを紐付けるために必要．valueには，配信したい環境（今回はfoo-org/awesome-app-iosのStaging）のDeployment keyを入力する -->
        <preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
      </platform>
      <platform name="android">
        ...
        <!-- CodePushのDeploymentとアプリを紐付けるために必要．valueには，配信したい環境（今回はfoo-org/awesome-app-androidのStaging）のDeployment keyを入力する -->
        <preference name="CodePushDeploymentKey" value="YOUR-IOS-DEPLOYMENT-KEY" />
      </platform>
    ...
    </widget>
    ```

    ここまででPluginの設定は完了．

4. 実際に配信されたアップデートを取得して更新するためのロジックを記述する．

    ここで設定できるのは，

    i. どのタイミングでアップデートを確認してインストールするか
    ii. アップデートの際にそれをどのようにアプリに適用するか

    例では，一番単純な，「Cordovaのresumeイベントにフックしてアップデートを確認し，次のアプリ起動時にアップデートの内容を自動的に適用する」方法を取ることとする．

    ```www/index.html
    <!DOCTYPE HTML>
    <html>
    <head>
      ...
      <script>
        document.addEventListener("resume", function () {
          console.log("resume event fired");
          codePush.sync();
        });
      </script>
    </head>
    <body>
      <br />
      This is a template for Monaca app.
    </body>
    </html>
    ```

    アップデートをすぐに適用したり，アップデート前にダイアログを出すようにするなど，もう少し凝ったアップデートをすることもできる．
    その場合は， https://github.com/Microsoft/cordova-plugin-code-push#api-reference を参照するとよい．

5. アプリをビルドしてみる．

    例ではデバッグビルドでAndroidアプリを作成（MonacaデバッガーとCodePushを一緒に使っているとたまにデバッガーごと強制終了してしまうので，確実な挙動を見たいときはリリースビルドを使った方がよい）している．未検証だが，iOSアプリもほぼ同様かと思われる．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/6ab2007d-7b62-7432-5a56-5653441317cc.png)

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/1b799bfd-6841-9082-9d59-1d11e2ac0ffb.png)

6. ビルドしたアプリを起動してみる．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/b6d63eed-e285-8e40-efad-29595080cb9c.png)

    テンプレートはいじっていないのでこんな感じ．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/f9b2979a-d6de-df9a-7bf6-0210707d5a90.png)

    デバッガーのログはこんな感じになる．

# CodePushによるOTAアップデート

ここから，実際にアップデートの配信と適用を行ってみる．

注意点として，アップデートの配信にはappcenter-cliを用いるため，ローカルPC等のappcenter-cliが使える環境に，作成したCordovaプロジェクトを用意する必要がある．
つまり，事前にLocalkitやMonaca CLIを用いて，プロジェクトをローカルPCにダウンロードする必要がある．

1. 前項までで作成した`CodePush Sample`プロジェクトについて，アップデートの内容を確認するために，アプリの内容をちょっと書き換える．

    ```www/index.html
    <!DOCTYPE HTML>
    <html>
    <head>
      ...
    </head>
    <body>
      <br />
      A new update was released. <!-- 文言変更 -->
    </body>
    </html>
    ```

4. https://docs.microsoft.com/en-us/appcenter/distribution/codepush/cordova#releasing-updates こちらの手順に従って配信を行う．

    ```
    $ cd /path/to/your/cordova/project
    $ appcenter codepush release-cordova -a foo-org/awesome-app-android
    ```

    ```
    Successfully released an update containing the "/path/to/your/cordova/project/platforms/android/assets/www" directory to the "Staging" deployment of the "awesome-app-android" app.
    ```

    のような出力がされればOK．
    今回はStaging環境のDeployment Keyを指定したので，Staging環境にデプロイされているはず．

5. アプリを起動する（ここでresumeイベントが発火し，アップデートが端末にダウンロードされる．ダウンロードまでに少し時間がかかるので，再起動前に少し放置するとよい）．

6. アプリを再起動する（このタイミングでダウンロードされたアップデートが適用されるはず）．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/a070ae08-7c7d-a053-f342-94b0d25497dd.png)

    変更が反映された．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/72ece2c2-fdf8-5802-4b68-1b36503cbfcb.png)

    https://appcenter.ms/orgs/foo-org/apps/awesome-app-android/distribute/code-push/Staging
    Visual Studio App Center側のコンソールにも配信の履歴が残っている．

    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/46811/27c11db9-583c-7a83-1806-ce9ed95cf940.png)
    
    ログはこんな感じ．

# まとめ

OTAアップデートは，ネイティブ実装のアプリには実現できない，非常に強力な機能だと思います．
アプリの修正以外にも，テスト用のアプリをテスターにのみ配信したり，UIのA/Bテストをしたり，使い道もいろいろあるかと思います．

一度試してみてはいかがでしょうか？

# 参考

https://speakerdeck.com/myb/codepushtoreact-nativedejin-ji-otaririsu-liao-jie
https://www.webprofessional.jp/push-code-updates-to-apps-instantly-with-codepush/

[^1]: Cordovaの場合はWebView，ReactNativeの場合はJavaScriptCore．
[^2]: おすすめはCodePush．一度In-Appアップデータープラグインを試してみたが，ドキュメントに理解しづらい点が多く，かつ，（有料ということもあってか）検索した限り実際の適用事例も多くなかったように思う．
[^3]: 最初この方法を試したがうまく行かず1週間悩んだ．`package.json`を見て違いを理解はしたが…