ナカザンドットネット

それって私の感想ですよね

React Nativeの「(not) for you」を伝え続けた2019年を振り返る

2018年の夏に「React Nativeはメリデメ両方デカすぎて、気軽に採用すると事業や組織とのミスマッチを起こしやすいので、マッチしてるかどうか考えてから採用しましょうね」という話をしました。

blog.nkzn.info

このときは雑多に問題提起してしまったので、具体的なモデルケースを想像しづらいものになってしまっていました。

そこに課題意識を持った私は「2019年はRNにマッチしそうな事業(プロダクト)や組織(チーム)の姿を伝え続ける年にしよう」と位置付け、各所でその方針に基づいた情報発信を行いました。

人材調達の難しさに目をつけた第一弾

その第一弾として、ちょっとフライングして2018年末に公開されたのが次の記事です。

codezine.jp

  • 「どうせビジネスサイドは3プラットフォーム出したいっていうじゃん」
  • 「でも本当に3プラットフォームそれぞれの作法を理解してる人をそれぞれ集められるの?」
  • 「UI作法(ライフサイクルとか)だけでもWebに寄せたほうが、採用するにせよ育成するにせよ楽なんじゃない?」

という切り口で導入を書かせてもらいました。これを言語化できたおかげで、良いスタートダッシュが切れたんじゃないかと思っています。

さらにターゲットを絞った第二弾

第一弾の時点では「ふーん、そういう分野もあるかもね(私には関係ないけど)」くらいの認識になりそうだったので、もう少し明確にターゲットを絞って刺しに行ったのが、第二弾のMOBILE CREW NIIGATAでの発表です。

speakerdeck.com

  • 「B2B(2B)だと操作マニュアルとかユーザーサポートが必須だけど、もし楽にUIを揃えられるならサポート部門の負担がちょっと減るでしょ」
  • 「できる人を外から連れてくるような人材流動性はなくて、たまたまいた器用な人を育てたほうが現実的でしょ」
  • 「どうせなら潰しの効くスキルを覚えさせたいでしょ」

という切り口を追加して、「そう! お前ら! お前らに必要なソリューションなんだよ!」感を出してみました。

集大成の第三弾

第一弾で打ち立てた方向性を元に、第二弾で深く言語化することができましたが、技術的な詳細を見ても魅力的であることの紹介が疎かになっていました。

というわけで、技術的な特性の話題も加味した集大成として書いたのが、第三弾でエンジニアHubに寄稿した記事です。公開は2020年になりましたが、2019年の11月ごろに書いてました。

employment.en-japan.com

  • 技術的なイメージが付けやすいように具体的なコードや内部実装の話も重視した
  • 巨大なエコシステムの支援を得られることをイメージしやすくする
  • ビジネス要件としてクロスプラットフォームが求められている現場があることを改めて強調する

この記事で気をつけたのは上記のようなところでした。悲壮感の漂っていた第二弾を裏返して、ある程度ポジティブな味付けにしたつもりです。

三部作の後書き

やっている最中は「結局ずっと同じことの話をしているけれど、読者の方は飽きないだろうか」と思っていました。しかし、こうして振り返ってみると、話題の切り口が違うのでターゲット読者も違っているし、媒体が違うので届くチャンネルも違うし、ということで、かなり広く話題提供ができたんじゃないかと思います。

全部読んでくれている人はエンジニアHubのあたりで「またこの話か」と飽き気味になっていたかもしれません。これは素直にすいませんでした。

この1年で生み出した3部作は、もちろんReact Nativeの大成を願ってのものです。しかしながら、読んだ人にはわかるとおり、「多くの人にReact Nativeを使ってほしい」というものではありません。「こんな状況の事業・組織には、React Nativeは役に立つ可能性がある(for you)」をハッキリと伝えることで、「そういう状況にない事業・組織では役に立たないかもしれない(not for you)」がある程度浮き彫りになるような作りにしたつもりです。

Android SDK + KotlinやiOS SDK + Swiftでアプリをそれぞれ作ったほうが、顧客に対して出せるバリューが大きくなるプロダクトというのは当然ありますし、それを実現するためのチームが無理せず構築できるならば、React Nativeのようなクロスプラットフォーム技術はnot for youであるはずです。無理にクロスプラットフォーム技術などという巨大なフレームワークを採用して苦しみを抱える必要はないでしょう。

ただ、世の中でそういったチームを構築できる企業というのは、そんなに多いのだろうか、という疑問を、第一弾を書きながら思ってしまったのです。

「採用は人事や経営の仕事だからITエンジニアの自分が扱うべきスコープではない」と言ってしまうのは簡単ですが、採用が上手くいっているわけでもないのにユニコーン企業の技術選択やチームビルディングだけをカーゴカルト的に真似しようとしたところで、苦しむのは自分たちなのです。

様々な事情があるとは思いますが、「(まだ)お金が潤沢にない」「(まだ)十分な数のメンバーがいない」という状況において、React Nativeはfor youなツールになりうると私は思っています。また、そういった状況になくても、純粋にReact Nativeの技術特性が事業にとって好ましいと判断されれば、時雨堂さんDiscord iOSNature Remoのように、腕力のある器用な人々が扱う前提で採用されることもあります。

自分が関わっている事業と組織の課題と向き合ってみて、そのロードマップの途上にReact Nativeが役立ちそうな場所があれば、採用してみることを検討してみてください。そのときに私の書いた記事たちがお役に立てたなら、検討の結果がfor meでもnot for meでも、それはとても喜ばしいことなのです。

余談:他のX-Platを扱う方々へお願い

私はReact Nativeが手に馴染んだことによって見えた景色(観点)があったので、それを言語化する目的で記事を書いています。React Nativeが他のX-Platより優れていることを示すために書いた記事は(おそらく)ありません。(極めてfor meではあるものの、優れているとは思っていません)

私はFlutterやXamarinやIonic(Capacitor)をほとんど使ったことがありません。使ったことがないもののことを偉そうに解説することはできないので、私はこれらについて、今回の三部作のような語り口で解説することができません。

しかしながら、私の書いた三部作は、多くの部分でX-Platツールに共通の観点を含んでいると思っています。

私は技術選択が大好きなので、Flutterは、Xamarinは、Ionic(Capacitor)は、その特性によってどんな事業(プロダクト)と組織(チーム)にマッチするのかを誰かが記事にしてくれたら、喜んで読みます。結果的に僕の記事と似てしまっても全然構わない(むしろdiffが際立つ)ので、読みたいのです。

というわけで、誰か記事を書いてください。私からのお願いでした。

Uber Eatsの障害についての事実をReact Nativeの観点から確認する

本日、Uber Eatsで大規模障害がありました。React Native絡みのようなので、今わかっている範囲の事実だけメモしておこうと思います。

公式アナウンス

ユーザーの声

Twitterを眺めている限りでは、店舗側に配布されているアプリ(レストランアプリ)が利用できなくなり、受注の確認ができなくなっていたようです。

React Nativeっぽい

Uber EatsチームがReact Nativeを採用していることは、次の公式エンジニアブログでも言及されています。

eng.uber.com

本件で報告が上がっていた赤い画面は、React Nativeのデバッグモードで console.error() や最上位まで到達してしまった例外の内容を表示するための、RedBoxと呼ばれる開発補助のための機能です。レストランアプリもReact Native製と見て問題ないでしょう。

2つの事実を確認する

それでは、React Nativeの観点で、今回報告されている画面から読み取れる内容について解説します。主に次の2件について言及します。

  • RedBoxが表示されている
  • Textコンポーネントについてのエラーが出ている

RedBoxが表示されている

まずは、赤い画面(RedBox)が出ている件についてです。繰り返しになりますが、

本件で報告が上がっていた赤い画面は、React Nativeのデバッグモードで console.error() や最上位まで到達してしまった例外の内容を表示するための、RedBoxと呼ばれる開発補助のための機能

です。

React Nativeでアプリ開発をしたことがある方なら日常的に目にしているものですが、そうでない方はほぼ見たことないと思います。React Nativeは多くの有名アプリで使われているにもかかわらず、です。

というのも、RedBox(とconsole.warnを司るYellowBox)はリリース用にビルドした際に無効化され、ストアに公開された時点で表示されなくなっているために、ユーザーとしてアプリを触る場合には目にすることがないからです。

次の公式ドキュメントでも言及されています。

facebook.github.io

RedBoxes and YellowBoxes are automatically disabled in release (production) builds.

今回の障害では、Uber EatsのレストランアプリにRedBoxが表示されていました。

誤ってなのか、もともとそういう運用なのかはわかりませんが、今回障害のあったレストランアプリはデバッグビルドされたものだったことが見て取れます。

Textコンポーネントについてのエラーが出ている

RedBoxは console.error() なので、そのとき発生したエラーのメッセージとスタックトレースが表示されています。メッセージの内容を見てみましょう。

Invariant Violation: Text strings must be rendered within a <Text> component.

これはブラウザ開発に慣れ親しんだ方がReact Nativeを触り始めたときによくやるエラーで、簡単に再現することができます。↓の "Tap to play" を押すと、実行結果を見ることができます。

今回起きていたのは、JSX内で <Text> コンポーネント以外の場所に文字列を書くと発生するエラーです。ブラウザなら <div> の直下にテキストを書いても怒られませんが、React Nativeで <View> の直下にテキストを書くと怒られるのです。((最終的にAndroidのTextViewやiOSのUIViewに落とし込まないと表示できないと考えると、妥当な仕様です。))

これは静的解析では見つけづらいエラーなので、ちょっと同情はします。しかし、言ってしまえば凡ミスの類いなので、特に今回の障害でReact Nativeのヤバさが露見したとか、そういうことではないように思います。

追記

使ったことなかったけど、eslint-plugin-react-nativeの react-native/no-raw-text ルールで検出できそう。

github.com

感想と邪推

というわけで、Twitterで報告されていた内容から読み取れる範囲の事実について解説しました。ここからは私個人の意見や妄想の話で、事実に基づく話ではないものが混ざっています。眉に唾をつけて読んでください。

デバッグビルドの話はアプリの運用ルールの話なので、良いとも悪いとも断言できない部分もあるのですが、普通はやらないです。というのも、デバッグ中というのは、APKファイルの中にJSファイルを内包するのではなく、パソコン側で立ち上げた開発サーバーからJSファイルを逐次にダウンロードして画面に描画するため、配布に向いていないはずだからです。

……もしかして、開発サーバーを公開サーバーの中で起動しておいて、配布したデバッグビルドのアプリからアクセスさせている……? そうすれば、Hot Reloadの要領で、最新の実装がリアルタイムでエンドユーザーの手元に届くから……? そんなことある……? 簡易Code Pushとして技術的には実現可能かもしれないけど、本当にやるか……?

妄想はできるのですが、憶測を見てきたかのように語るのもよくないので、口を噤むことにしましょう。

追記:Textコンポーネントのエラーが起きるパターン

ちょっとだけ複雑なパターンもあるようです。

例えば、こんなコンポーネントがあったとします。

type Props = {
  message: string;
};
const Component: React.FC<Props> = ({ message }) => (
  <View>
    {message && <Text>message</Text>}
  </View>
);

これを <Component message=""> のように呼び出した場合、JSX部分の条件式が評価されて次のようになります。

<View>
  ""
</View>

こうなるとViewの直下にテキストがきてしまうので、アウトです。空文字はfalthyなので、短絡評価の結果として左オペランドだけが残った形ですね。Twitterを見ていると、このケースがあるある案件のようです。

とはいえ、想像の域を出ないので、実際にこういう実装をした結果として起きたエラーだというわけではありません。

おわりに

今回の件は、React Native関連の障害としては初めてと言っていいレベルの大規模なものになったように思います。しかしながら、React Nativeのせいというにはちょっと運用のほうが前衛的すぎるんじゃないかなという感想を持ちました。

こんな事例を挙げて、訳知り顔で「これだからReact Nativeみたいなツールは信用できない」みたいなことを言う人が出てくるのも業腹なので、そうではないよ、と言うために筆を取った次第です。

それにしても、本当にどういう運用をしているのか純粋に(割とポジティブな感情で)興味が出てきました。いつかReactConfとかApp.js Confあたりで、今回の障害についてUber Eatsのエンジニアから振り返りセッションがあったりすると面白いのになあ、と思っています。

Application Loader亡き後の私たちはどうやってipaをアップロードするのか

10/25追記: Application Loaderの後継となる公式アプリとして、TransporterがMac App Storeに公開されました。

Transporter

Transporter

  • Apple
  • 開発ツール
  • 無料
apps.apple.com

これで安泰ですね。


Xcode 11がMac App Storeからもダウンロードできるようになりました。既にアップデートした方もいるかと思います。

さて、世の中にはXcode本体を使わずにApp Store Connectにipaファイルをアップロードする方法を何とかして見つけないといけない、ちょっと辛い状況を抱えた人々が存在します。Xcodeでのアップロードが上手くいかなかったり、サードパーティの開発ツールからipaファイルを直接与えられてしまったりと、事情は様々ですが、そういった人たちはこれまで、Application Loaderを使うのが一般的でした。

qiita.com

しかし、残念なことに、Xcode 11にはApplication Loaderが同梱されないことが、Appleからアナウンスされました。

developer.apple.com

私たちはどうすればよいのでしょうか。

幸いにも、コマンドラインからデプロイする方法が従来から用意されています。

blog.kishikawakatsumi.com

altool というコマンドを使えばいいようです。直接叩いた場合には動作しませんでしたが、 xcrun altool という形であれば実行することができました。

$ xcrun altool
Copyright (c) 2009-2019, Apple Inc. Version 4.00.1181

Usage: altool --validate-app -f <file> -t <platform> -u <username> {[-p <password>] | --apiKey <api_key> --apiIssuer <issuer_id>}
       altool --upload-app -f <file> -t <platform> -u <username> {[-p <password>] | --apiKey <api_key> --apiIssuer <issuer_id>}
       altool --notarize-app -f <file> --primary-bundle-id <bundle_id> -u <username> {[-p <password>] | --apiKey <api_key> --apiIssuer <issuer_id>} [--asc-provider <provider_shortname>]
       altool --notarization-info <uuid> -u <username> {[-p <password>] | --apiKey <api_key> --apiIssuer <issuer_id>}
       altool --notarization-history <page> -u <username> {[-p <password>] | --apiKey <api_key> --apiIssuer <issuer_id>} [--asc-provider <provider_shortname>]
       altool --list-apps -u <username> {[-p <password>] | --apiKey <api_key> --apiIssuer <issuer_id>}
       altool --store-password-in-keychain-item <name_for_keychain_item> -u <username> -p <password>

従来、Application Loaderで実施していたアップロード作業に相当する機能を持っているのは、次のコマンドです。

$ xcrun altool --upload-app -f <ファイル名> -t ios -u <username> -p <password>
$ xcrun altool --upload-app -f <ファイル名> -t ios -u <username> --apiKey <api_key> --apiIssuer <issuer_id>

試しに、パスワードを使うほうを動かしてみましたが、問題なくApp Store Connectにipaファイルをアップロードすることができました。

ひとまず、これで生きていくことができそうです。 fastlane deliver を使えばいいような気がするのですが、まあ公式ツールを直接使うに越したことはないでしょう。

iOS 13 プッシュ通知ヘッダー必須問題(解決)とNSDataのdescriptionの話

Amazon SNSがapns-push-typeヘッダーに対応する旨がアナウンスされました。ひとまず解決です。

aws.amazon.com


いろいろ触ってみたんですが、なんだか状況がよく見えないので会社ブログやQiitaに書くわけにもいかず、ひとまず個人ブログで供養しますみたいなやつです。

Motivation

iOS 13(とwatchOS 6)以降のデバイスに対してプッシュ通知を送る場合に、追加のヘッダーが必須になるという情報が、ドキュメントに追記されていました(2019年8月20日現在)。

Header field Description
apns-push-type (Required when delivering notifications to devices running iOS 13 and later, or watchOS 6 and later. Ignored on earlier system versions.) The type of the notification. The value of this header is alert or background. Specify alert when the delivery of your notification displays an alert, plays a sound, or badges your app's icon. Specify background for silent notifications that do not interact with the user.

The value of this header must accurately reflect the contents of your notification's payload. If there is a mismatch, or if the header is missing on required systems, APNs may delay the delivery of the notification or drop it altogether.

For information about configuring notification alerts, see Generating a Remote Notification. For more information about sending background notifications, see Pushing Background Updates to Your App.

Sending Notification Requests to APNs | Apple Developer Documentation

要約すると

  • apns-push-type ヘッダーに alertbackground を指定してください
    • alert を指定するのは、ユーザーに通知の存在を見せたい場合です
    • background を指定するのは、ユーザーに何も気づかせない(バックグラウンドプロセスを起動するのみに留める)場合です

これまで受け取り側(iOSデバイス側)で振り分けていたものを、サーバー側から制御する意図があるようです。

apns-push-typeを付与しないとどうなるのか

APNsに対して行ったプッシュ通知依頼のPOSTリクエストに、 400 InvalidPushType というエラーレスポンスが返るそうです(Table 5 Response error stringsより)。

Amazon SNSとか大丈夫?

同一ユーザーの複数デバイス宛や、あるセグメントのユーザーなど、ある程度のまとまったプッシュ通知を、Android/iOSの両方に送りたい場合などに、Amazon SNSのMobile Pushといった、プッシュ通知のラッパー的なサービスを使うことがあると思います。

これらのサービスは、 apns-push-type ヘッダーを付与してくれるのでしょうか。また、付与しなかった場合のエラーハンドリングはどうすればよいでしょうか。

最悪のケースとしてiOS 13が正式リリースされた直後から、ユーザーにプッシュ通知が届かなくなる恐れもあると思われたため、調べてみることにしました。

apns-push-type問題の結論

2019年8月現在のSANDBOX環境に限っていえば、Amazon SNS由来のプッシュ通知はiOS 13デバイスに届きました。

エラーが出ることを確認してAmazonに直談判するつもりだったので、ちょっと拍子抜けです。

Amazonがこっそり apns-push-type を付与する改修を済ませていたのか、AppleがまだAPNsサーバーにドキュメントの内容を適用していないのか……後者だったら怖すぎる……

deviceTokenが正しく取れない罠を踏んだ

ここからは余談です。

Cordova iOS製のアプリをiOS 13デバイスに入れて検証していたのですが、序盤にAPNsから 400 BadDeviceToken のエラーをもらってしまいました。

Cordovaでプッシュ通知を扱う場合はphonegap-plugin-pushを使います。デバイス登録を行った際の deviceToken を、16進数文字列に変換してから渡してくれるので、ちょっと便利です。

さて、 BadDeviceToken というからには deviceToken にミスがあったのだろうとログを仕込んでみると、次の文字列が出てきました。

"{length=32,bytes=0x3dd89c70dabd0be290cc2f8e215132e0...75a03b3cbf9bbe0a}"

これをBase64エンコードしたものを使ってAPNsにリクエストした結果として、 BadDeviceToken が出ていたようです。さもありなん。

さて、 bytes の中身は16進数っぽいですが、謎の省略がされています。正規表現で抜き出すということもできなさそうです。Objective-C側の挙動が変わったようで、JavaScript側からはどうにもできなさそうです。

実装上の意図されていた挙動

phonegap-plugin-pushのコードを読んでみると、NSData型の deviceToken から description で文字列を取り出した後、加工することで16進数文字列を作っているようでした。

NSString *token = [[[[deviceToken description] stringByReplacingOccurrencesOfString:@"<"withString:@""]
                   stringByReplacingOccurrencesOfString:@">" withString:@""]
                   stringByReplacingOccurrencesOfString: @" " withString: @""];

https://github.com/phonegap/phonegap-plugin-push/blob/110e3c2/src/ios/PushPlugin.m#L365

上記の処理では、[deviceToken description] の結果が次のような文字列になることを期待しています。

<124686a5 556a72ca d808f572 00c323b9 3eff9285 92445590 3225757d b83997ba>

ほぼバイナリの16進数表記ですね。これを加工することで、NSString *token には 124686a5556a72cad808f57200c323b93eff9285924455903225757db83997ba という文字列が格納されていたわけです。

これがなぜ {length=32,bytes=...} になってしまったのでしょうか。

NSData#descriptionの挙動が変わった?

Appleのフォーラムを調べてみると、似たような話題が見つかりました。

プッシュ通知や deviceToken がどうのこうのというよりは、 description の挙動そのものが変わってしまったような印象も受けますが、どうなんでしょう………

phonegap-plugin-pushにPR投げました

ひとまず目の前の問題として、phonegap-plugin-pushを何とかしないといけないので、iOS 13以降では description を使わずに deviceToken の16進数文字列を生成できるようにしたPRを作成しました。

github.com

フォーラムでFBの実装がいいよ!という書き込みがあったので、ほぼコピペです。

Objective-CでOSSにPRを投げる実績を解除したぞ!!!🎉 (まだマージされたわけではない)

マージされました

イエーイ✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️

Ionicでもプッシュ通知するならphonegap-plugin-push使えと言われてるので、かなり広い範囲の人々を救済できたのではないだろうか……

まとめ

iOS 13でプッシュ通知が動かなくなりそうな罠を複数見つけてしまって、気が気でないですね……

どうか穏便に正式リリースの日を迎えたい……

Expo for Webを支えるモジュールたち

React Nativeの抽象化フレームワーク、Expoのv33が先日リリースされました。

blog.expo.io

今回の目玉の一つは、何と言っても Expo for Web です。React Native for Webを取り込んだことで、Expoで作ったアプリがよほどネイティブな機能を使ってない限り、ブラウザでも動かせるかもしれない感じになってきました。

どうして歯切れが悪いのかというと、当然Expoにはブラウザでの再現が困難な機能も多く含まれているからです。React Native for Webのサンプル集にあるような機能であれば高確率で動くので、安全な範囲で導入したい方は expo パッケージの機能に手を出さないようにしておけば、かなりの部分は動くはずです。

安全でない範囲で導入したい方への朗報としては、sdk-33ブランチ(執筆時点でf5f35c30)を見ていただけるとわかるのですが、次のようなモジュールのWeb版(.web.ts/tsx)が実装されています。

  • import { Video, Audio } from 'expo-av';
  • import { GLView } from 'expo-gl';
  • import { SQLite } from 'expo-sqlite';
  • import { Camera } from 'expo-camera';
  • import { Accelerometer, Gyroscope } from 'expo-sensors';

どれもインパクトがあるやつですね。だいたいEvan Baconの仕業です。何なんだあの情熱はどこから来るんだ。

ちゃんと実装を見てみると、そんなに魔法は使っていないパターンが多く、普通に 最近のWeb APIってすげーな 、という感じになります。

魔法枠としては、実はfeatureブランチですがブラウザ上でBarcodeScannerを動かすコードもEvan Baconの手によって実験されています*1。何なんだあの情熱はどこから来るんだ。

こんな感じで、単にReact Native for Webを取り込んだだけではなく、従来からあったモジュールに関しても(Evan Baconがほぼ一人で黙々と作業して)Web版がどんどん実装されています。正直、彼の情熱が尽きた時点でメンテナンスされなくなる恐怖があるので、あんまりまだ使おうと思えないのですが、我こそは人柱になろうという奇特な方がいらっしゃれば、ぜひお願いします。

本当はプロダクションビルドしたビルド済みフォルダの話がしたかったのですが、長くなってきたので次回にします。

*1:まあjsQR使ってるだけといえばだけですが

モバイルアプリ開発フレームワークのレイアウトの計算、描画方式の違い

レイアウト計算のエンジンの派閥みたいなのがたまに気になるので、いま認識している範囲で雑に書いておきます。

「俺の愛する○○が入ってないじゃないか」は、僕が知らないかたまたま思いつかなかっただけで、特に意図的に排除したものはありません。

公式のレイアウト計算+公式の描画

Androidでいえば、Androidを支える技術<I>で解説されているような、プラットフォームネイティブなレイアウト計算(measure)と描画(draw)が行われている方式です。

  • Android SDK
  • iOS SDK
  • Xamarin Native

公式のSDKがここに入っているのは当然のこととして、Xamarin Native*1も、普通にLayout XMLやStoryboardでUIを組んだりするので、こちらに入ります。

独自のレイアウト計算+公式の描画

レイアウト計算は独自の方式で実施しつつ、描画はネイティブ側におまかせする方式です。

  • Yoga (React Native / Litho / ComponentKit)
  • Xamarin.Forms

このジャンルだと、広く使われていそうなのはFacebookのYogaでしょうか。Flexboxを中心とした、Webの方法論でUIを構築するためのレイアウトエンジンです。

今回調べてみて初めて知ったのですが、Xamarin.Formsも独自のレイアウトエンジンで動いてるっぽいですね。↓の記事を流し読んだだけなので、もしかしたら違うのかもしれませんが。

xamarinhelp.com

独自のレイアウト計算+独自の描画

レイアウト計算も描画も自前で頑張ってる方式です。

  • Flutter Engine
  • Unity
  • Jetpack Compose?

公式と言えば公式のJetpack Composeを独自と呼ぶのもアレですが、Canvasにゴリゴリ書いてるみたいです。レイアウト計算について明言しているところは見つかりませんでしたが、ひとまずここに置いてみました。

FlutterやUnityは、独自方式をとっているおかげで、プラットフォーム間やバージョン間での表示差異が出づらいのがいいところです。

まとめ

クロスプラットフォーム方面は、言語やUIキットの違いが取り沙汰されることが多い感じですが、こうして中を見るとレイアウト計算や描画の方式にも違った特徴があります。

言語やパラダイムが似ているように見えても、プラットフォーム差異やバージョン差異の起こりやすさ、ネイティブUIの取り込みやすさなどで違いがあるので、意識しておくとよさそうです。

追記

エンジンがモバイル向けとして作られているわけではないのであえて書きませんでしたが、CordovaのレンダリングエンジンをWebViewであるとした場合は、「独自のレイアウト計算+独自の描画」枠です。

*1:Xamarin.Formsを使わない開発方式をこう呼ぶと聞いたことがあるので、この名称を使わせてください

React Native for Windowsを斜め読みした感想

jp.techcrunch.com

はい、なんか出てきました。「react-native-windowsなら前からあったじゃん」と思ったのですが、どうやら大幅にリライトしたみたいなので、本家とどのくらい違うのか、簡単に流し読みしてみました。雑に読んだだけなので、たぶん勘違いを多分に含んでます。眉に唾をつけて読んでください。ちゃんと知りたい人はコード読んでください。

三行で

  • VC++使った結果、言語をブリッジするレイヤーがひとつ減ったのは面白い
  • フォルダ構成は大きく変わったけど、たぶん妥当
  • 開発者が使うときのAPIだけ本家と共通なら、内部実装の方針は多少本家と違ってても大丈夫なはず(ほんとか?)

前提

React Native製のアプリは、ざっくり分けると、次の3つのレイヤーで構成されます。

  • 開発者が作成したJavaScript製Reactアプリケーション
  • JavaScriptライブラリとしてのReact Native('react-native'モジュール)
  • 「JavaScript実行環境+ネイティブブリッジ」によるミドルウェアとしてのReact Native

「ミドルウェアとしてのReact Native」がWebViewのような役割を果たしつつ、DOM APIの代わりとなるReact Native Rendererにアクセスできる「JavaScriptライブラリとしてのReact Native」を私たちが利用することで、アプリを組み上げていく形です。

「ミドルウェアとしてのReact Native」には、C++ライブラリであるJavaScriptCoreが含まれており、この中でJavaScriptが動いています。さらにC++とObjective-C/Javaをつなぐコードも用意されているので、全体としては次のような構成になっています。

JavaScript
↕️ (JavaScriptCore)
C++
↕️ (Objective-C++ / Android NDK)
Objective-C / Java

このへんを軸に話をしていきます。

今までのreact-native-windows

まずは従来のreact-native-windows*1がどうなっていたのかを確認しておきましょう。現在のmasterである 4798c610 では、currentフォルダに旧実装が格納されています。

github.com

f:id:Nkzn:20190507111541p:plain
currentフォルダ

割とReact Native本家に近い構成になっています。ミドルウェアのiOS実装である React フォルダや、Android実装である ReactAndroid フォルダの代わりに、 ReactWindows フォルダが用意されている形です。中身は次のような感じ。

f:id:Nkzn:20190507111803p:plain
ReactWindowsフォルダ

ChakraBridge フォルダがあるので、JavaScriptCoreの代わりにChakraを使ってるんですかね(未確認)。ChakraBridgeの中はC++ですが、他のフォルダはだいたいC#で書いてあるようです。

f:id:Nkzn:20190507112142p:plain
ReactNative.Sharedフォルダ

ここまで来ると、だいぶC#プロジェクトっぽい見た目になってますね。

ざっくりとは次のような構成になっていると思われます。

JavaScript
↕️ (Chakra?)
C++
↕️ (Visual C++?)
Visual C#

ぶっちゃけWindows文化圏の開発環境に詳しくないので間違っていそうな気もする・・・

新しいreact-native-windows

それでは今回発表された、新しいReact Native for Windowsのほうを見てみましょう。 4798c610 のvnextフォルダに新しい実装が格納されています。

github.com

f:id:Nkzn:20190507112837p:plain:h640
vnextフォルダ

ごそっと増えました。ざっくり見た感じでは、次の点が変わっています。

  • ReactWindowsフォルダの中身をトップレベルに移した
  • ReactWindowsフォルダにまとめられていたネイティブ実装をUWP実装とWindows Desktop実装に分けた
  • C#製のネイティブ実装を(おそらくすべて)C++で再実装した

たぶん他にも色々あると思うのですが、私のWindows力が足りない……

C++版をmasterにマージした際のコメントに細かいコミットコメントがまとめられてるっぽいので、興味がある方はどうぞ。

この変更で、「ミドルウェアとしてのReact Native」は次のような構成になったのかなと想像しています。

JavaScript
↕️ (Chakra?)
Visual C++

これもWindows素人の想像なので、違うっぽかったらご指摘ください!

「ミドルウェアとしてのReact Native」の大変革については、後述の「感想 > 全面C++リライト」で感想を述べます。

「JavaScriptライブラリとしてのReact Native」はまだポーティングを進めている最中という感じでした。このへんはおいおい実装されていくと思います。ミドルウェアに合わせてJavaScript側の実装も最適化*2されていますが、コンポーネント名とPropsの型は本家に準拠してるっぽいので、普通に使っている分にはそんなに問題にはならないでしょう(たぶん)。JavaScriptCoreとChakraの差異とかのほうがよっぽどやばそう。

感想

というわけで本題です。疲れたので簡潔にいきます。

全面C++リライト

React Nativeは「ミドルウェアとしてのReact Native」が必ず持っている C++で書かれた処理そのプラットフォームで本来使われているGUI開発言語 に繋げないといけない性質上、「他言語とのブリッジ」という複雑な機構を持たざるをえませんでした。C++とJavaを繋ぐためにAndroid NDKのJNI機構が使われていますし、C++とObjecitve-Cを繋ぐためにObjective-C++が使われています。

さて、Java/KotlinやObjective-C/Swiftを使わずに画面を作ることはできるでしょうか。Android NDKでUIを書くことは可能ですが、あまり一般的ではありません。Objective-C++からUIKitを触るのは……できたっけ?(うろ覚え)もできます*3が、煩雑になりやすいため、やはり一般的ではないようです。

では、Windowsアプリケーションの開発ではどうかというと、Visual C++で画面を開発するのは、そこそこ一般的だったと記憶しています。POSレジのアプリがVC++ランタイムで動いているのを見たことがありますし。古い記憶なので今では廃れつつあるのかもしれませんが。そのへんよくしらない。

Windows向けの実装として考えた場合に、ブリッジを1段階廃して、最初からC++で書くというのは、パフォーマンスの面では良い選択であるように思えました。

C#でネイティブモジュールを作っていた人達からすると「何してくれてんのォ!?」という感じかもしれない……どうなんだろう……C++からC#を呼び出すブリッジコードみたいなのを足せば既存のモジュールも動くのかな……そこだけ心配……

フォルダ構成の変更

本家とこんなにフォルダ構成変えちゃって大丈夫なの?というのは思いますが、これはこれでありなのかなーと思えてきました。

本家は「AndroidとiOSの両方に対応する」という制約があるので、iOS向け実装とAndroid向け実装を別々に管理する必要がありましたし、各プラットフォーム向けのファイルがトップレベルに漏れ出すのは可能な限り避けられていました。しかし、react-native-windowsはWindowsのことだけ考えていればいいので、トップレベルにWindows向けの都合が漏れ出しても、そんなにまずくはありません。それどころか、UWP版とWinDesktop版の実装を分けるにあたっては、このくらいのフォルダ構成にしておいたほうが見通しが良くなるくらいかもしれないです。

本家との挙動の違いについて

本家側の修正に追従しようとしたときにしんどそうじゃない?という疑問は湧いてきますよね。

まあそもそも、本家側もiOSとAndroidで挙動が同じなのかという話がありまして、コントリビューターたちが頑張って似たような挙動になるようにしているだけで、別に挙動を同じにする仕組みがあるわけではないのです。

なので、Windows版の挙動も頑張って合わせていく感じになるんだろうなあと思います。

ひとまず「JavaScriptライブラリとしてのReact Native」を私たちが触るときのAPIさえ本家と同じ型なら、割り切って使うことはできそうです。本家のAPIについてはOpen Source Roadmapで語られたとおり、React Native 1.0を目指してStableなAPIにしていくそうなので、react-native-windowsもそこに追従してくれれば、多少内部実装が違っても使う側としては「これはこれで」になるのかなと。

まとめ

Windows素人の雑な感想でした。

たぶんこれからOfficeやSkypeなどで実用しながらブラッシュアップされていくと思うので、見守っていきたいと思います。

*1:たぶんWindows版のSkypeとかOfficeがこれを使ってる

*2:Android向けワークアラウンドを消したり

*3:https://twitter.com/omochimetaru/status/1125698058337964032