Angularシングルページアプリをそのままハイブリッドアプリにした話 / AngularJS+PhoneGap+WebRTC

[2017年アドベントカレンダー「個人開発」](https://qiita.com/advent-calendar/2017/private-development)の1ネタとして、こちらを投稿します。

この記事で紹介するのは、今年2017年の1月頃にリリースした、
「[FlashShare](https://1bs8.app.link/7sjOkwcLeA)」という外国語単語の学習支援アプリを制作した際のノウハウについてです。
もうすぐリリースから1年ですが、先日どうにか世界で累計6万インストールに達しました。
大半がアフリカ(想定外)のAndroidユーザで、広告費は合計¥3,000程、無料の慈善事業アプリです。
この開発・運用の経験から、身を削って美味しい情報をお届けできればと思います。
どこか開発日記や一人反省会のような仕上がりになりましたが、ご容赦ください。

# この記事を通してお伝えしたいこと
- 「Webでシングルページアプリ(以下、SPA)を作れれば、<br>ネイティブ固有スキルがほぼ無くてもアプリをリリースできる」という事実。
    - [Web版FlashShare](https://flash-share.herokuapp.com)をアプリ上からも直接参照してます。<br>(モバイル想定だったので、PC版のレイアウトは手抜きしています。)
- (ほぼ)サーバレスアプリの構成例。
- 使用した技術の内訳と、その選定理由。

# 開発経緯(私的な話)
邪念も多いですが、このあたりの観点でFlashShareの仕様や開発ベースを定めました。

- テーマ(英語)への関心
 - 「*The Walking Dead*、知らない英単語多っ。観ながらメモりたい」的な発想。
 - 「あの友達と単語学習シェアして、倍速で、かつ守備範囲広く学びたい」的な発想。
 - これ以上言語の壁で機会を失わせないために、人々の英語学習を助けたい気持ち。  
- エンジニアリング
 - 当時知りたてのWebRTCによる、P2P実装童貞からの卒業願望。
 - エンジニアとして名刺となる、コンパクトで実用的なアプリ作りの欲求。
 - コードベースの再利用で短期開発に挑戦。
- サービス
 - 集客を考慮して、定常的な露出を確保するためにアプリ化必須。
 - ランニングコストはほぼ**¥0**にしたい。

# 導かれたアプリの仕様と構成方針
## 仕様

- 外国語の単語登録・管理
  - 自動翻訳(テレビ見ながら片手間に単語打って、Enter押したら適当な翻訳結果が残っててほしい)
  - (アプリ自体でなく、登録する単語の)多言語対応
  - 発音確認
- 単語の記憶確認テスト
  - 正答頻度の記録を含む
- その場に居合わせる人との単語シェア

## アプリ構成
以前作ったSPAのコードベースの再利用を念頭に置き、
Webをベースとし、それをiOS/Androidアプリ上からも参照できるようにした、
いわゆる、ハイブリッドアプリを実装することにしました。

具体的には、PhoneGap上でAngular SPAを稼働させる構成です。
(きっとみなさんの期待を裏切る程度にはPhoneGapなりによく動くと信じてます)

**[図1: FlashShareの起動/実行フロー]**
![FlashShare_sequence.jpg](https://qiita-image-store.s3.amazonaws.com/0/51183/8697f6be-cd65-b439-7986-360d46e5b7ad.jpeg)

上記のような具合で、このサービスにおけるサーバの特徴は以下の通りです。

- 基本的な仕事
  - indexページを提供。(アセットのバージョン管理の起点となる。)
  - WebRTC接続確立用エンドポイントを提供。<br>([図2 WebRTC部分のシーケンス図]にて詳述)
- やらせないこと
  - ユーザの単語帳データの保持。(クライアントで完結させます)
  - ユーザ管理や認証処理。<br>(無料前提なので、いっそログイン無しで自由に使えるようにすることで<br>アカウント管理に関する実装/データ保持/データ管理コストを下げ、<br>かつ、AppStoreやGooglePlayの審査材料も減らしました(おまけ)。)

# 採用した技術とその理由
ざっくりですが、以下に採用した技術について記載します。
*※ 慈善事業的なアプリなので、無料で利用できるサービスに対して大きな評価をしています。*

| 技術 | 採用理由 | 対抗馬 |
|-----|-----|-----|
| [Glosbe](https://glosbe.com/)<br>(コミュニティ型多言語辞書API) | 運用体制が不明瞭であるものの、無料で豊富な対応言語であるため採用。| ・Google翻訳API<br>・Microsoft翻訳API |
| [PhoneGap](https://phonegap.com/)|WebViewでのページ遷移は激遅なものの、 SPAにすればアプリ水準になり、またAdobeの信頼感と無料であることを加味し、採用。|・各種プロトタイピングツール<br>・ハイブリッドアプリPF|
| [AngularJS](https://angularjs.org/) |登場から一定経過した枯れた技術であり、かつ、すでにSPAコードベースを制作済みだったため。|各JSフレームワーク|
| [WebRTC](https://ja.wikipedia.org/wiki/WebRTC) |技術検証コスト/実現可能性に不安があったものの、向学のためにも採用。サーバの通信/処理負荷軽減にも寄与。|WebSocket |
| [intro.js](https://introjs.com/)<br>(チュートリアル実装用ライブラリ) |拡張性に限界があるものの、導入が簡単で非商用利用である限り無料であるため採用。|当時、魅力的な対抗馬は無し|
| [Branch.io](https://branch.io/)<br>(ディープリンクPF)|基本無料で便利(urlスキーマ等の設定をすればいい感じにしてくれる)なので採用。|Appsites.com|
| [Heroku](https://www.heroku.com/)|GAEばかり使っていたので経験として。ユーザが少ない限り無料。|Google App Engineその他|

# ノウハウ集
## ポイント1: シングルページ実装ノウハウ
Web制作上常識ですが、上記図1の通り、
アプリ起動時にアセットのバージョン情報を含むindexページのみサーバから直接取得し、
以降、そのバージョンをもとに、ページを構成する必要なアセットをCDNから取得します。
これにより、アプリ自体をリリースし直すことなく、SPAを更新することが可能となります。
(あまり大きな声では言えない)

<details><summary>補足: なぜアセットのバージョン管理が重要か?</summary>
Webでは当然ですが、あるファイルがCDNに乗ると、
その有効期限が切れるまで同じ画像URLからは、同じ画像が返ってきます。
逆に言うと、いくら、サーバに新たなアセットをリリースしても、CDN上に古い画像が残ったままとなり、
思い通りにユーザから見える画像が更新されないのです。</details>

## ポイント2: PhoneGapでのWebViewアプリ実装ノウハウ
*ここでは、説明の便宜上、以下の通り呼び分けます<br>・Nativeコードに含まれるPhoneGapのルートのindexページ = phonegap-indexページ<br>・サーバから提供されるindexページ = web-indexページ*

PhoneGapでWebViewアプリを実装する際、
単純に作ろうとすると、phonegap-indexページにて、
WebViewを開くPhoneGapのAPIを叩き、その上でweb-indexページ(SPA)を展開すると思います。

```
phonegap-indexページ > webview = web-indexページ
```

が、実はこの方法だと、落とし穴があります。
PhoneGapが提供するNative機能へのブリッジAPI(JS)をWebView上から実行することができないのです。

そこで、トリッキーですが、こちらの方法を紹介します。
phonegap-indexページからweb-indexページを表示させる際、
WebView上に展開するのでなく、phonegap-indexページ自体に上書きするのです。
つまりはこういう具合です。

```
phonegap-indexページ = web-indexページ
```

例えば、正常系のみ想定した最小実装だと、以下のような処理を
phonegap-indexページにて呼び出す形になります。

``` sample_spa_on_phonegap_index.js
function openMainPage() {
  var xhr = new XMLHttpRequest();
  var intvl = null, bodyElm = null, isInitialized = null;
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
      var htmlElm = document.getElementsByTagName('html')[0];
      htmlElm.parentNode.removeChild(htmlElm);
      // PhoneGapのルートページ自体を上書き
      document.write(xhr.responseText);
      // 無理やりangularを含むページを後からロードしているので、初期化をしてあげる必要がある。
      // 0.5秒毎に、angular自体がロードずみがチェックし、存在すればbodyへのinjectionを行う。
      intvl = setInterval(function() {
        if (typeof angular !== 'undefined') {
          bodyElm = angular.element(document.body);
          isInitialized = bodyElm.injector();
          if (!isInitialized) {
            angular.bootstrap(document.body, [あなたのAngular APP名]);
          }
          clearInterval(intvl);
        }
      }, 500);
    }
  };
  xhr.open('GET', あなたのweb-indexページのURL);
  xhr.send();
}
```

## ポイント3: 低管理コストのWebRTC実装ノウハウ
以下の図は、WebRTCのシグナリングサーバを自身で提供しない場合の代替案です。
(WebRTC自体についての詳しい説明はここでは割愛します)
コンセプトは、低運用コスト(費用面, 労力面)/そこそこなコネクション確立精度です。

このフローに従うと、ゲーム感覚でユーザ間で数桁の数字を教え合うなどでP2Pコネクションが確立されます。

**[図2: FlashShare-WebRTC実装部の実行フロー]**
![FlashShare-WebRTC_sequence.png](https://qiita-image-store.s3.amazonaws.com/0/51183/9b27d150-30d1-0b04-5245-41944ecc7bcf.png)

## ポイント4: とにかくインストールを狙うノウハウ
*(ここだけ少し趣が異なるので、別の記事に分離するかもしれません)*

世界中の人を幸せにしたい、彼らの人生を豊かにしたい、
もしくは、名刺代わりとなるアプリの実績を高めたい、など様々な理由で
世界中でとにかく沢山インストールしてもらいたいと思う方もいると思います。
そんな時におすすめなのが、以下の方法です。大事な順で。
(リリース以降、各PFのコンソールにアクセスしてないので状況変わっていたらごめんなさい)

- **アプリ内の表示言語を英語とする**
 - 当前ながら、最も世界にその言語を理解する人口が多いだろう言語を押さえます。
 - <details><summary>多言語対応は重労働です。リリース後、調整していくのが効率的です。</summary>
   というのも、多言語対応でインストールを伸ばすには、アプリ内の文字表記だけでなく、実際にはストア掲載物についてもローカライズする必要があります。  
   (経験上、例えば、日本のストアに英語の説明文付きのアプリがあってもそうそうインストールされません)</details>
 - (おまけ)英語にすることで文字数が長くなるため、自然とUIをシンプルに仕上げようとする副次効果がありました。
- **なるべく誰でもインストールできるようなアプリとしてストアに登録する**  
    - 同じ目的を実現しつつ、全年齢対象に近づけるアプローチがないか検討する。
    - 懸念が無い限り、世界各国をリリース対象国とする。
- **途上国のAndroidユーザ獲得に軸を置く**(断言しにくいのですが、戦略の1候補として)
    - 以下の考察に関連して、Google Adwordsを低単価設定にした際のターゲットを全力で取りに行く。
  <details><summary>補足(経験・推測に基づく考察)</summary>
あらためて、「コスパよく、インストール数を最大化する」という目的を前提におきます。  
・広告出稿を考えるとき、ユーザ転換率を世界中で一定と仮定すると、出稿単価が安いほど(数打てるほど)優れた条件となります。例えば、アフリカの途上国など。  
・また、[世界40カ国、主要OS・機種シェア状況【2017年9月】](https://www.auncon.co.jp/corporate/2017/0927.html)によると、iPhoneがこれほど普及しているのはアメリカと日本をはじめとする数カ国のみで、例えばアフリカ諸国ではiPhone普及率は15%以下です。スマートフォン市場規模も、アフリカ全土で6億人と、十分巨大です(人口:12億人, 普及率:50~60%)。
・加えて、企業が収益をあげるために攻めたい国は、個人収入も一定水準を上回るような先進国であるため、途上国では旨味が薄い割に法務リスクなどを負う可能性があるためリリースが避けられ、自然と競争が緩やかになります。  
  以上より、コスパよく、インストール数を最大化するには、狙うべきは「途上国におけるAndroidユーザ」と言えるかもしれません。(実際、"FlashShare"のユーザを見ると同傾向があります。)

- **ストア公認の"教育アプリ認定"的なラベルを獲得する**
  - たしか、Google Playでは、教育アプリは一定条件を満たすと、印をもらえます。(他にもこういったものがあれば積極的に狙ってください)
  - おそらく、それにより教育関連アプリとしての推薦度が高まり、定常的なユーザ流入が活発になります。

- **ストアの注目新着アプリとなることを目指す(個人開発で目指すのは困難なので優先度:低)**
  - 企業レベルのゲーム開発などでは、この獲得は生死を分け得るものだったりします。
  - AppleもGoogleも全部のアプリを見てられないので、目をつけてるデベロッパーから、推薦を受けるようなこともあり、つまりは、なかなか個人開発したものが選ばれることは難しいように思います。

# 運営結果
## 運用費
| 名目 | 利用サービス名 | サービス項目詳細 | 費用 |
|-----|-----|-----|-----|
|サーバ|Heroku|Free Dynos|¥0|
|ストレージ|Heroku|MemCachier|¥0|
|CDN|CloudFront|-|¥350(大体の円換算)|
|ドメイン|Heroku|heroku.appを利用<br>(本来はドメイン取得するべきところ手抜いてます)|¥0|
|翻訳API|Glosbe|-|¥0|
|ディープリンク<br>プラットフォーム|Branch.io|LAUNCH (10000MAU以下)|¥0|
|合計|-|-|**¥350**|
*※ 広告費やGithub, Apple/Google Developer Program管理費を除く、リリースからの総額。*

## 制作期間
おまけで、このアプリを制作した際の所要期間についてもざくっとお伝えします。

|作業内容|開始日|所要期間|何に時間がかかったか|
|---|---|---|---|
|サーバサイド|16' 11/16|2日ほど|アセット管理や構成の整理|
|クライアントサイド(JS)|16' 11/16|2ヶ月ほど|WebRTCの理解と実装。|
|クライアントサイド(アプリ)|16' 12/16|2週間ほど|PhoneGap上でのWebRTC利用<br>外部URLを開くための技術調査|
|チュートリアル導入|17' 1/18|3日ほど|ページ単位のチュートリアル導入|
|デザイン調整|17' 1/20|5日ほど|単純にアーティストじゃないので苦手|
|テスト|17' 1/10|2週間ほど|WebRTCのiOS/Android動作確認・修正|
|SEO対策|17' 1/23|1日|調査|
|Progressive Web App対応|17' 1/25|1日|調査と動作検証|
|コードFIX|17' 1/25|-|-|

# 反省点
- 企画面
  - アプリの命名。同名のサービスが複数存在し、SEO的にも(人気になったら)法務的にも脅かされかねない。
  - 最小の漢の仕様になってしまったので、ゲーム性などを考慮して、学習を促進させる余地がある。
- エンジニアリング面
  - クライアントのエラー原因を特定できるだけのログ機構を初期から用意できなかった。
- 品質管理
  - 画面サイズ別のUI表示結果を容易に確認できるテスト機構を初期から用意できなかった。
  - さらに単体テストを活用し、読みやすく正確なコードになるよう、また高速に改修を図れるよう取り組む余地がある。

# 今後、更なるアップデートをかけるなら
- Branch.ioのライブラリ更新(本当はもう更新してないといけない;;)
- 基本機能の拡充
 - ゲーム性の向上。学習と報酬、もしくは、リーダーボード的な概念の導入。
 - 単語のカテゴリ管理。その単位での確認テストの実装
- チュートリアル改善
 - 利用開始後の初期データ入力をどうにか、自然に捗らせたい。
- サービス拡張
 - 利用者間での単語帳のシェアプラットフォーム構築。


<hr/>
以上が、外国語単語の学習支援アプリ「[FlashShare](https://1bs8.app.link/7sjOkwcLeA)」を制作した際のノウハウでした。

Peingなどを見ていると、新しい技術に触りたい気持ちと、アイディアや時流に目を向けることのバランスを考えさせられますよね。
この実装時に身につけたアプローチの仕方や実装スキルは今の自分のエンジンになっているので
それを信じつつ、もっとアイディアや時流に目を向けたものも作っていければなと思います。

何か、記事中の内容について、もっと掘り下げた説明がほしい点などありましたら、
コメントをよろしくお願いします。

長文、最後までご覧いただきありがとうございました。