ReactとCordovaで、Web/モバイルのハイブリットアプリを作った話
ReactとCordovaを使って、ブラウザ向けのWebUI + Androidで動くスマホアプリ を提供するサービスを、一人で作ってみた話です。
サマリー
- 作ったもの
- 最大の課題:作業量
- 一人で作りきるために意識したこと
- 取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する
- 取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する
- 取り組み3: ユニットテストを書く
- まとめ
作ったもの
自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんにFX自動取引を開始できる、システムトレードフレームワークです。
- アルゴリズムの作成、バックテスト、リアル口座での自動取引まで、これ一つで可能。
- 取引アルゴリズムはRubyで記述。メール送信や取引タイミングのPush通知も、APIを呼び出すだけで実現できます。
- 取引の状況は、スマホアプリ/Web UIでいつでもどこでも確認が可能。アルゴリズムの管理もできるので、相場が急に動いても安心です。
- クラウドプラットフォームのHerokuに対応。運用も低コスト。
- オープンソース。ソースコードはGitHubで公開しています。
スクリーンショットをいくつか。
Web UI:
スマホアプリ:
コード規模
UI側のコード規模は以下の通り。テストケースを含めた合計で、31000行くらいです。
WebUI | アプリ | 合計 | |
---|---|---|---|
ソースコード | 15222 | 4454 | 19676 |
テストケース | 9676 | 1574 | 11250 |
また、これとは別に、サーバーのコード(ruby)が、ソースコード/テストケースあわせて、27000行くらいあります。
構成
- UIは、いわゆるシングルページアプリケーションで、ECMAScript2015 で書いています。
- Babelでコンパイルして、Webpackで統合する形。
- マテリアルデザインを採用していて、Reactで動くUIライブラリの Material UI を利用しています。
前置きは以上。
最大の課題:作業量
開発にあたっての最大の課題は、なんといっても作業量でした。
初期の検討段階で、「Webアプリ or モバイルアプリのどちらかに注力できないか?」と考えましたが、
Push通知やモバイルでのシステム管理に対応した、スマホ時代のFXシステムトレードフレームワークにしたかった。
- 同様の機能を提供するソフトはすでにあるのですが、モバイルでの取引状況の確認やアルゴリズムの管理に難があったのが、開発のきっかけにもなっています。
ということで、両方必要という結論に至りました。そうと決まれば、後は、如何にして作りきるかです。
一人で作りきるために意識したこと
以下の2点を、特に意識しました。
- コードの共通化/再利用
- Web UI/スマホアプリのコードを可能な限り共通化して、開発コストを削減する。
- ユニットテスト
- ユニットテストで個々のモジュールの動作を保障。
- 機能追加や変更を、低コストで素早く行えるようにする。
そして、↑のための具体的な取り組みとして、以下を行いました。
取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する
スマホアプリをCordovaを使ったハイブリットアプリにして、Web UI/スマホアプリのコードを共通化しました。
- Cordovaを使うことで、JavaScript + HTML + CSSでスマホアプリを作ることができるので、同様の構成で作成するWeb UIのコードを再利用できるようになります。
- React Native という選択肢もありましたが、UI側の開発に着手した時点ではAndroidには対応していなかったため、見送りました。
- Cordovaは、Webでの情報が豊富で、実績も多くあった点がプラスでした。
- また、Push通知や課金決済関連のプラグインがあったことも大きいです。
- 採用に当たって懸念だった、UIのパフォーマンスについては、かんたんなプロトタイプを作って検証しました。
- やはり、ネイティブアプリよりはもっさりしていますが、ゲームのようなパフォーマンスがアプリの価値に直結する性質のソフトウェアではないので、問題ないレベルと判断。
- それよりも、リリースまでの開発工数を抑えること、また、リリース後の機能強化や改善をスピード感を持って行えること、を重視しました。 unageanu.hatenablog.com
「UIの反応速度とか使い勝手とか、実際どんなもんなの?」と気になる方は、ダウンロードしてお試し頂ければと思います。(まいど、ありがとうございます! / 30日の無料トライアル期間内に、定期利用を解除すれば請求は発生しないので大丈夫です)
なお、Crosswalkの効果は絶大でした。体感速度が劇的に向上したうえ、怪しい動きをしていた箇所も治ったり。 apkのサイズ/起動時間は増えますが、採用する価値はあります。
取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する
MVVMのレイヤードアーキテクチャを採用して、Modelレイヤのコードを共通化。
ViewやViewModelも、変更の度合いに応じて最適なレイヤーでカスタマイズすることで、共有できるコードを最大化しました。
- Model/View/ViewModel + 通信などのInfrastructureで構成。
- Model(UIに依存しない、アプリ共通のコアドメイン)は、Web UI/スマホアプリでそのまま共有。
- スタイルの変更だけで済む場合は、CSSのレベルでカスタマイズ。
- DOM構造を変える必要がある場合は、View(Reactコンポーネント)のコードを差し替えて対応。
- Viewで管理するデータや機能がそもそも異なる場合は、ViewModel(Modelをラップして、Viewに依存するデータや操作を提供するクラス群)のレベルで、カスタマイズして再利用。
このほか、Infrastructureの通信部分やGoogle Analyticsでの利用状況解析も、WebUI/スマホアプリでカスタマイズして利用しています。
- 通信部分は接続先REST APIを差し替える必要があるため(Web UIは自ホストに、スマホアプリはUIで設定したサーバーに接続)、URLResolverとして機能を切り出しカスタマイズしています。
- 利用状況解析は、スマホアプリではJava APIを使うようカスタマイズしたクラスを用意して差し替え。
以下は、利用状況解析のコードの抜粋です。 インターフェイスを統一しつつ、実装を差し替える形で、別のクラスを用意しました。
WebUI版:
export default class GoogleAnalytics { // 略 // Google Analytics のJavaScript版APIを呼び出す sendEvent( action, label="", value={} ) { this.run((ga) => ga('send', 'event', this.category, action, label, value)); } sendTiming( category, timingVar, time, label ) { this.run((ga) => { ga('timing', category, timingVar, time, label); }); } // 略 }
スマホアプリ版:
export default class GoogleAnalytics { // 略 // Cordova google-analytics-plugin のAPIを呼び出す // https://github.com/danwilson/google-analytics-plugin sendEvent( action, label="", value={} ) { this.run((ga) => { ga.trackEvent(this.category, action, label, value, () => {}, (e) => console.log(e)); }); } sendTiming( category, timingVar, time, label ) { this.run((ga) => { ga.trackTiming(category, time, timingVar, label, () => {}, (e) => console.log(e.message)); }); } // 略 }
これら、コンポーネントの差し替えは、DI(Dependency Injection)コンテナを利用して、行うようにしました。 DIコンテナを使うことで、コンポーネントの外側で、環境ごとにどのクラスを使うのか宣言的に指定できるようになります。
WebUI版のコンポーネント定義:
binder.bind("googleAnalytics").to("utils.GoogleAnalytics") .withProperties({ category: "web-ui", version: "1.0" }).onInitialize("initialize");
// スマホアプリ用のクラスを使うように変更 binder.bind("googleAnalytics").to("app.utils.GoogleAnalytics") .withProperties({ category: "app", // category, versionもカスタマイズ。 version: "1.0.6" }).onInitialize("initialize");
利用側のコードは以下のような感じ。Inject
としてマークしておくと、コンポーネント定義で宣言されたコンポーネントが注入されるので、利用側はAPIを呼び出すだけです。
export default class RmtService extends AbstractService { constructor() { this.urlResolver = ContainerJS.Inject; this.xhrManager = ContainerJS.Inject; this.googleAnalytics = ContainerJS.Inject; // コンポーネント定義で宣言されたコンポーネントが注入される } // 略 putAgentSetting(settings) { this.googleAnalytics.sendEvent( "put rmt agent setting" ); // GAにイベントを送信 return this.xhrManager.xhr(this.serviceUrl("agents"), "PUT", settings); } }
取り組み3: ユニットテストを書く
MVVMアーキテクチャを採用したことで、View以外の部分はDOMに依存しなくなるため、容易にユニットテストができるようになります。 ユニットテストを意識的に用意したことで、一人でも何とか作りきることができたかな、と感じます。
- 一人で開発したとこもあり、サーバー側の開発で1か月くらいrubyのコードばかり書いていると、自分が書いたコードでも忘れてしまって、変なバグを埋め込んだりします。テストコードがあることでデグレードが検知され、バグに気付くことが多くありました。
- また、コードがどう動くか、についても、テストケースをみると思い出せるということもありました。
ユニットテストで確認された"動く"コードを少しずつ積み上げていく、という作り方は、心理的にも良かったと思います。 一人なので、開発期間はどうしても長くなりますし、やってもやってもタスクがなくならない、終わりが見えない感じになりますが、"動く"コードが積み上がっていくことで、毎日少しずつでもコードを書いていれば、いつか完成すると思えるようになります。
なお、初版リリースの段階ではViewのテストは省略しました。
- 初版リリースでは、UIの精緻化などでDOMの構造を変更する可能性が高く、テストを作成しても、効果が変更コストに見合わないと考えました。
- また、Viewの機能は「ViewModelの状態に応じて仮想DOMを生成する」だけで、テストすべきロジックが少ない && ViewModelのテストで、アプリケーションがこういう状態の時はこうなる、〇〇操作を実行するとこういう状態になる、といった機能は確認できている、というのも理由としてあります。
- 同じ理由で、E2Eテストも行っていません。このあたりは、今後、サービスが利用されるようになって、メンテナンスの比重が大きくなってからの課題と考えています。
まとめ
- Cordovaを使ってちゃんと作れば、一人でもサービスを作れる。
- プロトタイプや最初のMVPは、Cordovaで素早くコストをかけずに作る、そして、手ごたえがあったらネイティブで作り直す、という選択肢は割とアリなのではと思います。
- もちろん、パフォーマンスが重要なアプリの場合は、不相応な場合もありますが。
- ユニットテスト重要。テスタビリティは最初から考慮して作ろう。
- Crosswalk速い。使おう。