Cordova、Xamarin、NativeScript、React Native、Electron、Flutter
幸運なのか不幸なのか、キャリア初期からクロスプラットフォーム開発に携わることが多く、初期のCordovaから現在のReact Nativeまで、SmartTVからAndroid、iOS、MacOS、Windows(そして亡きWindows Phone… ああ、私の愛しいLumia 720は母親専用機になってしまった…)と、あらゆるプラットフォームを経験してきました。全てを極めたとは言えませんが、それなりに経験と知識は積んできたつもりです。記憶が薄れてしまう前に、近年のクロスプラットフォーム開発の歴史を振り返りつつ、感想をまとめてみようと思います。
今回は、思い出話として、様々なソリューションを振り返りながら簡単に紹介します。中には、私の記憶違いや、すでに時代遅れになっているものもあるかもしれません。その際は、ご指摘いただけると幸いです。
Cordovaは、かなり初期のクロスプラットフォームソリューションで、さらにその前のPhoneGapから派生したものです。私がクロスプラットフォーム開発に携わり始めた頃(2012年頃)は、最も人気のあるソリューションでした。なぜなら、Cordovaはシンプルで使いやすく、当時の競合はXamarinとTitaniumという、どちらも有料のソリューションだったからです。
Cordovaのアーキテクチャは非常にシンプルで、シンプルなActivity(説明を簡単にするために、以降のアーキテクチャはAndroidを例に説明しますが、決してiOSに詳しくないわけではありません!)の上にCordovaWebViewコンポーネントが搭載されています。これは、Cordova APIを追加して改造されたWebViewで、ネイティブ部分との通信を可能にします。アプリは基本的にモバイルWebで、ネイティブと通信する機能が追加されているだけです。
このシンプルで分かりやすい設計が、Cordovaのメリットをもたらしました。まず、習得しやすいことです。フロントエンドエンジニア(あるいはWebエンジニア… 当時はまだ「フロントエンド」という言葉を知らない人も多かったですね)であれば、モバイルWebの作り方さえ知っていれば、簡単にクロスプラットフォームアプリを作ることができました。魅力的ですよね?
しかし、物事はそううまくはいきません。Cordovaのこのシンプルすぎるアーキテクチャは、諸刃の剣でもありました。良い面としては、Webなので、Webでできることは何でもできるということです。しかし悪い面としては、結局のところただのWebなので、どんなに頑張ってもWebの域を出ることができないということです。私がどんなに頑張っても、キムタクにはなれないのと同じです。
当時のモバイルデバイスのハードウェア性能はまだそれほど高くなく(まだ1GB RAMの時代でした)、モバイルWebのパフォーマンスはネイティブビューと比べて大きく劣っていました。特にUIのレスポンスに関しては、AndroidのWeb UIとネイティブUIでは大きな差があり、一般ユーザーでも見分けがつきました(ただし、同じハードウェア条件であれば、iOSデバイスではそれほど顕著な差はありませんでした。これは、2つのプラットフォームのレンダリング構造の違いによるもので、詳細は専門家にご確認ください)。
もちろん、Webエンジニアたちも、このUXの差を何とかしようと努力し、多くの工夫(ほとんどはGPUアクセラレーションに関するもの)を凝らして、AndroidでのUI操作体験の向上に取り組みました。その中でも、最も優れた仕事をしたのがIonicです。
当時はスマートフォンが急速に普及し始めた時代で、モバイルWeb UIフレームワークが雨後の筍のように登場しました。jQuery Mobile、Sencha Touch、Kendo UI、Bootstrapなど、多くのフレームワークが登場しましたが、Ionicはその中でも後発組でした。Ionicが当時、他のフレームワークを出し抜いて人気を集めた最大の理由は、「質感」でした。Ionicチームは、それほど多くのUIコンポーネントを提供していたわけではありませんでした(むしろ、他のフレームワークと比べると少なかったと言えます)。しかし、彼らがリリースするコンポーネントはどれも非常にスムーズに動作し、非常に細かいUXのディテールにもこだわり、改善を続けていました。さらに、ng1との緊密な統合や、Cordova WebView向けに特別に調整された点なども、Cordovaアプリの開発に最適な選択肢となりました。
しかし、残念ながら、IonicチームやWeb開発者がどんなに努力しても、アーキテクチャ上の限界を突破することはできませんでした。これが、次世代のクロスプラットフォームソリューションの開発を促すことになったのです。
(余談ですが、Cordovaのアーキテクチャと当時のハードウェア環境によって生じたUXのボトルネックと、Cordovaが当時最も普及していたクロスプラットフォームソリューションであったことから、「クロスプラットフォーム開発=パフォーマンスが悪い、ユーザー体験が悪い」というイメージが定着してしまいました。そのため、その後のクロスプラットフォームソリューションは、普及する際に「以前Cordovaを使ったことがあるんだけど、パフォーマンスが… 」という意見に遭遇することが多く、新世代のソリューションがCordovaとどう違うのかを説明するのに苦労することになりました…)
Microsoftに買収される前から、Xamarinは知る人ぞ知るクロスプラットフォームソリューションでした。しかし、オープンソースのCordovaとは異なり、Xamarinは当時有料でした。これが、Cordovaほどの人気を得られなかったもう一つの理由です。また、Xamarinが普及しなかったもう一つの理由は、開発言語にC#を使用していたことです。C#が悪いと言っているのではありません。実際、私もC#からプログラミングを始めましたし、この言語に慣れ親しんでいますし、好きな言語です。ただ、.NET Coreが登場するまで(Oracle様がお怒りになるまでは)、C#のコミュニティは比較的閉鎖的で、オープンソースのリソースも少なかったのです(当時、爆発的に普及していたJavascriptと比べると)。これが、Xamarinの生来の弱点となっていました。
XamarinのアーキテクチャはCordovaよりも複雑で、Android & Javaのバインディングや、クロスプラットフォームの.NETランタイムであるMonoなど、多くのことを実現していました(実際にはXamarinよりも先にMonoが存在していましたが、開発元は同じ会社で、当時Monoの最大の用途はXamarinだったようです…)。Xamarinは、大きく分けてXamarin.Android、Xamarin.iOS、Xamarin.Mac(後から登場)、Xamarin.Formsの4つの部分に分かれています。MicrosoftがXamarinを買収してからは、ドキュメント作成のノウハウを遺憾なく発揮しているので(Facebookも見習ってほしいものです)、ここではこのアーキテクチャの長所と短所について簡単に触れるだけにします。詳細は公式ドキュメントをご覧ください。
Xamarinアーキテクチャのメリットは、パフォーマンスのオーバーヘッドが少ないことです。CordovaがWebレンダリングによる大きなオーバーヘッドを負担しなければならないのに対し、Xamarinは言語間インターフェースのオーバーヘッドだけで済むため、相対的に非常に軽微です。これが、UXの面でもXamarinに優位性をもたらしています。しかし、このアーキテクチャにももちろん欠点があります。最大の問題は、新しいネイティブAPIを使用するには、公式のラッパーを待たなければならないことです。ただし、Xamarinは現在オープンソース化されているので(ご指摘ありがとうございます!)、待ちきれない場合は自分で作ることも可能です。もう一つは、サイズが大きいことです。ただし、これから登場するのはもっとサイズが大きいものばかりなので、実際にはそれほど問題ではありません。
すみません、私はその人をよく知りません。ただ、彼はJS to Native Bindingを使ったソリューションに料金を請求していたことを覚えています。しかし、私はそれを聞いたところ、バグだらけだと聞いていたので、遠ざけました!ある意味、彼はパイオニアと言えるでしょう。
NativeScriptから始めると、第二世代のクロスプラットフォームソリューションと言えるでしょう。NativeScriptはTelerik(Kendo UIを作った会社)が開発し、その後オープンソースになりました。彼らの考え方はTitaniumと似ていて、jsとネイティブ間のバインディングを目指していました。そして、彼らはリフレクションという方法を採用しました。ご存知の通り、v8にはJavascriptランタイムをいじくり回すためのAPIがたくさん用意されています。例えば、Node.jsはcommenjsを実現するためにこの方法を使っています。NativeScriptも同様のことを行い、Android/iOSのAPIをNativeScriptランタイムにマッピングすることで、開発者はJavascriptから直接これらのネイティブAPIを呼び出すことができるようにしました。この方法により、NativeScriptは非常に強力な拡張性を持ちました。なぜなら、「あらゆる」ネイティブアプリで使用できるAPI(サードパーティのライブラリを含む)を使用することができ、特別な作業は必要ないからです。プラットフォームのAPIに変更があった場合でも、Xamarinのようにチームがアップデートするのを待つことなく、すべての変更をすぐに使用することができます(リフレクションの仕組みがアップデートで壊れていない限り)。すごいと思いませんか?
(一点注意が必要なのは、NativeScriptはパフォーマンス上の理由からJavaのリフレクションを直接使って動的にAPIを検索しているわけではなく、apk内にメタデータを事前に生成して検索とマッピングを行っています)
NativeScriptはViewの面でも優れており、ネイティブコンポーネントに加えて、スタイリングに使えるCSSのサブセットも提供しており、UI開発をネイティブアプリよりも便利にしています(余談ですが、NativeScriptがReactNativeのYogaをCSSのコンパイルに使っていないのは少し残念です。両者がリソースを統一できれば、もっと多くのことができるのに…)。NativeScriptは当初、UIにAngularを使用していましたが、その後、基本的なjs/ts、Angular、Vueの3つの開発方法に分離されました(そう、これが私がWeexを調べようと思わなかった理由です)。そして、3つの方法すべてでデータバインディングのメリットを享受できます。素晴らしいと思いませんか?
NativeScriptのもう一つの大きな利点は、TypeScriptを第一級言語として扱っていることです(本体もTypeScriptで書かれています)。しかし、考えてみれば当然のことです。リフレクションの仕組みを使っている以上、TypeScriptのサポートがなければ、APIのリファクタリングだけで死んでしまいます(TypeScriptに触れたことのない方は、メモ帳でJavaを書くことを想像してみてくださいXD)。
しかし、このようなアーキテクチャはNativeScriptにいくつかの欠点ももたらしました。まず、開発者はある程度のネイティブの知識を持っている必要があるということです。NativeScriptランタイムはWebkitではなくv8/JavaScriptCoreをベースにしているため、WebAPIは一切使用できません(誰かが別に実装してくれれば別ですが)。例えば、一般的なXmlHttpRequestやfetchはWebkitレイヤーの実装なので、NativeScriptではネイティブプラットフォームの実装を使ってラップする必要があります(余談ですが、実装を探しているときに偶然、Vj(ReactNativeのコア開発者)がNativeScriptにいくつかのissueを上げていたのを見つけました。両者で実装に対する考え方が少し違うようです)。これは、開発者がネイティブ関連の知識に依存することを強いることになります。しかし、この点では、NativeScriptは比較的簡単で、サードパーティのライブラリをjavascriptにマッピングすることができ、.d.tsを書くだけで済みます。ReactNativeのように毎回Bridgingを自分で書く必要はありません。
結論としては、NativeScriptは非常に優れたクロスプラットフォームソリューションだと思います。リフレクションを使うことで拡張性を最大限に高め、UIも最新のAngular/Vueと統合することで、開発効率を大幅に向上させています。彼らが有名にならなかった主な理由は、登場のタイミングが少し悪かったことです。NativeScriptがリリースされてから数ヶ月後、まだ成熟していない段階で、ReactNativeがReactコミュニティの人気を引っ提げて彗星のように現れたのです。もし、彼らがもう少し早く登場していれば、今の勢力図は違っていたかもしれません…。
数の力は偉大なり、これはまさにReactNativeを象徴する言葉ですXD。2018年にクロスプラットフォーム開発について語るとき、Cordova時代に留まっている少数の人(いや、少数ではないかもしれません)を除けば、関連情報を追っている人なら誰でもReactNativeについて言及するでしょう。ちょうど私も、ReactNativeをプロダクション環境に導入した第一陣の人間です(当時は0.0.3バージョン、最初の公開バージョンでした。当時の私の勇気を考えてみてくださいXD)。以下では、このReactNativeが一体どれほどすごいのかを簡単に紹介しましょう。
まず、ReactNativeはアーキテクチャの考え方としてはNativeScriptと似ていて、「Javascriptでネイティブを制御する」ことを目指しています。ただし、その方法が異なります。ReactNativeはNativeScriptのようにマッピングを行うのではなく、jsとネイティブ間の通信を行うRCTBridgeを構築しています。ネイティブAPIやネイティブコンポーネントを使用するには、ネイティブ層でブリッジを作成し、jsにエクスポートする必要があります(NodeのC++アドオンを書くような感じです)。この方法の利点は、パッケージ化されたファイルが小さくなり、アーキテクチャの実装がシンプルになることです。欠点は、すべてのネイティブモジュールをブリッジする必要があることです…。正直なところ、この部分に関してはNativeScriptのやり方のほうが優れていると思いますが、ReactNativeがそうしているからには何か理由があるはずです。今のところ、公式の議論でこのことについて言及しているものを見つけることができませんでしたが、いつかissueを上げて聞いてみようと思いますXD。
ReactNativeが今日のような勢いを持つようになったのは、Reactエコシステム全体の成長と、Facebookがすでに自社のアプリで製品実装を行っていることが大きく貢献しています。これは多くの人にとって安心材料となり、AirbnbやUberなどの大手企業もこぞって採用したことで、さらに拍車がかかりました。しかし、ReactNativeはまだ安定した状態にあるとは言えず、2週間ごとにアップデートがあり、破壊的な変更も頻繁に行われています。これは開発時に注意すべき点です。
ReactNativeを開発する際に注意すべきもう一つの点は、ネイティブプラットフォームに関する知識が必要になるということです。Cordova以降の主流のクロスプラットフォームソリューションはすべて、ある程度のプラットフォームネイティブに関する知識を必要とするようになっています。NativeScriptに比べれば、ReactNativeはネイティブの知識がなくても使えるアプリを作ることができますが、拡張性が大きく制限されてしまいます(プラグインを自分で書くことができず、サードパーティや公式のものに頼ることになります)。また、ネイティブのレンダリングや動作の仕組を理解していないと、パフォーマンスを悪化させてしまう可能性があります(RNのパフォーマンスが悪いという声の多くは、APIの背後にある仕組みを理解せずに、おかしなことをしているからです…)。
React Nativeが出た当初の謳い文句は「learn once, write everywhere」で、開発チームは理想的な「write once, run everywhere」は目指さず、プラットフォームごとに異なる特性を保つようにしました。この中心思想に基づく設計により、React Nativeは他のプラットフォームにも簡単に拡張できるようになりました。例えば、サードパーティ開発者によって実現されたreact-native-macos、react-native-appletv(後に公式に統合)、Microsoftが公式に開発したreact-native-windows(元々はuwpという名前でしたが、uwpが徐々に微妙になってきたのでwindowsに改名されました…実際にはまだuwpです)、Samsungも公式にreact-native-tizenを開発するなど、ほぼすべてのプラットフォームでReact Nativeを見かけるようになりました(ただし、成熟度はまた別の話です)。
全体的に見て、RNは非常に信頼性の高いクロスプラットフォームソリューションと言えるでしょう。まだ完全に安定しているわけではありませんが、優れたコアアーキテクチャ、高い拡張性、そして巨大なコミュニティサポートがあり、現時点では大きな問題は見当たりません。クロスプラットフォーム開発に興味があるなら、React Nativeを学ぶ価値は大いにあるでしょう。
(ん?なんでデスクトップアプリが出てくるかって?だってタイトルは「クロスプラットフォーム」って書いてるし、AndroidとiOSに限定してないし、文句あるなら訴えてみろよ、バーカ。)
Electronは、元々はAtom-Shellという名前で、GitHubがオープンソースエディタAtomをリリースした際に一緒にリリースされた副産物でした。しかし、その後、この副産物の影響力はAtom editor自体をはるかに上回り、独立したプロジェクトとしてElectronという名前に変更されました。Electronの本質は非常にシンプルで、ChromiumとNode.jsの組み合わせです。両者はipc通信を介してやり取りをしています(実際にはNode-Webkitという、もっと前からあるプロジェクトも同様のモデルを採用していますが、使ったことがないのでここでは紹介しません)。
Electronについては、正直なところ、何を書けばいいのか迷ってしまいます。なぜなら、すでに成熟しすぎているからです。2018年現在、ある程度のJavaScriptスキルを持つエンジニアにとって、Electronアプリの開発は朝飯前です。フロントエンドは完全なWeb環境でありながら、互換性が高く、常に最新のChromiumを使用しているため、さまざまな先進的な構文やWebAPIを使用できます。バックエンドはみんな大好きNode.jsで、バージョンもかなり新しく、構文やモジュールのサポート不足を心配する必要はありません。Electronはプラットフォームとの連携性不足を心配する必要もありません。豊富なNode.jsエコシステムのおかげで、必要な機能のほとんどはモジュールで見つけることができます。また、自動更新やパッケージ化、リリースなど、ほとんどのアプリに必要な機能は、Electronとそのエコシステムがすでに用意してくれています。公式サイトのキャッチコピーにもあるように、Electronは面倒な作業をすべて処理してくれるので、開発者はコア機能の開発に集中することができます。まさに「it just works!」です。
Electronの欠点を強いて挙げるとすれば…それは「重い」ことでしょう。これはクロスプラットフォームソリューションにつきものの宿命なのかもしれません(苦笑)。完全なChromiumを搭載する必要があるため、最小限のHellowWordでも圧縮して数十MBのサイズになってしまいます。ただし、これはデスクトップソフトウェアにとってはそれほど問題ではありません。問題は、メモリ使用量と起動時間の2点です。
フロントエンドにメモリを大量消費することで有名なChromiumを使用しているため、Electronはメモリ使用量で常に批判の的となってきました。起動時に100MB以上のメモリを消費するのは当たり前で、アプリケーションの複雑さによっては、その数字はさらに増加します。これは完全にChromiumに責任があるので、Electronチームとしてはどうしようもなく、Chromiumがこの問題を改善してくれることを待つしかありません。起動時間に関しては、MicrosoftのVSCodeが良い例です。彼らは見事なlazy requireテクニックを駆使することで、多機能なアプリでありながら、Atomと比較して優れた起動時間を達成しています。しかし、それでもChromiumの起動時間がボトルネックになるという限界があります。そのため、VSCodeの起動速度はSublimeTextに劣っています。ただし、ほとんどの場合、これはそれほど重要な問題ではありません。HDDでは約1秒程度の差しかなく、SSDではほとんど差を感じないからです。
余談ですが、最近Proton Nativeというものが登場しました。名前からして、Electronに対抗するものであることは明らかです。そのアイデアは、Chromiumを軽量なlibuiに置き換え、RNのバインディング方式で操作するというものです。これは非常に興味深いアイデアであり、アプリ全体の軽量化に大きく貢献する可能性がありますが、まだ開発初期段階であるため、様子見をお勧めします。
結論としては、もしあなたがデスクトップ向けのクロスプラットフォームアプリを開発しようと考えているなら、Electronは間違いなく最有力候補と言えるでしょう。全体的な成熟度、コミュニティの人気の高さ、利用可能なリソース、そして有名なアプリ(あなたのPCにもElectronベースのアプリが入っている可能性は非常に高いです。もし今入っていなくても、いずれ入るでしょうXD)など、あらゆる面で安心して利用できます。CEF、NW.js、react-native-windows、react-native-macosなど、他の選択肢もそれぞれにメリットがありますが、総合的に判断すると、Electronが最適な選択肢と言えるでしょう。
この記事を書いている時点では、Flutterはまだベータ版であり、私もそれほど詳しくありません。しかし、これは本当に面白いものであり、新時代の展望と言えるものなので、ここで簡単に紹介したいと思います。
FlutterはGoogleが開発したクロスプラットフォームソリューションで、Dartをメイン言語として使用し、Android、iOS、Fuchsiaで動作します。実はこのプロジェクトは、かなり前から(2~3年前から)水面下で進められていました。当初はSky projectという名前で、発表当時は非常に地味で、知る人はほとんどいませんでした。しかし、ここ半年ほどでGoogleが本格的にプロモーションを開始したことで、注目を集めるようになりました。
Flutterが新世代の産物と言われる所以は、これまでのソリューションとは一線を画す特徴を持っているからです。
まず、JavaScriptを捨てたこと。近年話題になったクロスプラットフォームソリューションは、JavaScriptを中心に据え、その豊富なエコシステムを活用しようとしてきました。しかしFlutterは、あえて当時まだメジャーとは言えなかったDartを採用したのです。FlutterがDartを選んだ理由は、すでに多くの記事で詳しく解説されているので、ここでは割愛します。
JavaScriptを捨てたことは、メリットとデメリットがあります。デメリットは、世界的に見てもトップクラスの人気を誇り、活気のあるエコシステムの恩恵を受けられないこと。開発初期段階では、車輪の再発明を強いられる場面も多いでしょう。
一方、Dartを採用したメリットは計り知れません。言語仕様の詳細はさておき、DartはJITコンパイルとAOTコンパイルの両方に対応している点が重要です。開発中はJITコンパイルによって快適な開発体験を実現しつつ、リリース時にはAOTコンパイルによってパフォーマンスを大幅に向上させることができます。
DartのAOTコンパイル対応は、ReactNativeやNativeScriptが抱えるパフォーマンスのボトルネック、つまり起動時間の遅さや、ネイティブプラットフォームとの通信時に発生するruntime JS bridgeによるコンテキストスイッチのオーバーヘッドを、Flutterは根本から解決できることを意味します。
Flutterのもう一つの大きな特徴は、独自のレンダリングエンジンSkiaを搭載していることです。SkiaはGoogleが買収しオープンソース化したレンダリングエンジンで、Chrome、FireFox、Androidなど、様々な場面で利用されています。Flutterでは、すべてのUIコンポーネントがSkiaによって描画されるため、プラットフォームのUIコンポーネントに依存しません。つまりFlutterは、真の意味でのUIのクロスプラットフォームを実現しているのです。
素晴らしいと思いませんか?しかし、ここにもメリットとデメリットがあります。メリットは、ReactNativeのように異なるプラットフォームのUIコンポーネントを抽象化する必要がなく、FlutterのUIコンポーネントはすべてのプラットフォームで全く同じ仕様で動作し、ネイティブレンダリングによる高いパフォーマンスを実現できることです。
デメリットは、長年培われてきたプラットフォームのコンポーネントを捨て、自分たちで一から作り直さなければならないことです。
現状、Flutterの思想は非常に明確かつ大胆です。何かを犠牲にしなければならないとき、Flutterは既存の豊富なエコシステムよりも、アーキテクチャの美しさや高パフォーマンスを優先します。これは非常に野心的で、ある意味危険な賭けと言えるでしょう。
ちょうど画像の下のコメントにもあるように、「良いもの」が必ずしも「売れるもの」とは限りません。IT業界では、その例は枚挙にいとまがありません。FlutterはいつまでもGoogleの支援を受け続けられるわけではありません。このような大胆な改革を進める中で、いかに一般の開発者を惹きつけ、エコシステムを活性化していくかが、Flutterの未来を左右する最大の課題となるでしょう。
結論として、Fuchsiaが正式リリースされていない現段階では、単なるクロスプラットフォームUIソリューションとしてのFlutterは、まだ発展途上と言えます。アーキテクチャレベルでの高パフォーマンスと快適な開発体験は大きな魅力ですが、エコシステムの未成熟さや、Google Adwords以外のキラーアプリが存在しない点は課題として残ります。
Flutterの未来はどうなるのか? それは、私たちも見守っていくしかありません。
まさか最後まで読んでくれた人はいないですよね?(笑) 書いているうちに思いました。なぜ私はこれを分割して投稿しなかったのか? なぜ私は自分と読者をこんなにも苦しめるのか? なぜ私はこんなにも長くて誰も興味のない記事を書かなければならないのか? なぜ誰も私を愛してくれないのか? なぜ私はこの世に生まれてきたのか…?
いろいろ書きましたが、まだ終わりではありません。次回はもう少し軽い話題、クロスプラットフォーム開発全般に関する考察などを書いてみようと思います。