Native Moduleの作り方
決済サービスPAY.JP でモバイルエンジニアをしている tatsuyakit といいます。PAY.JPでは先日 React Native向けプラグイン をリリースしたのですが、本稿ではその中で出会ったNative Moduleを開発する際に気をつけたいポイントについて解説します。
Native Moduleとは
React Native開発において、React NativeのAPIに存在しないネイティブの機能を利用したい場合に、JavaScriptから各プラットフォームのコードにアクセスするための仕組みがNative Modulesです。
https://facebook.github.io/react-native/docs/native-modules-setup
プロジェクト構成とツール
cli
React Nativeのライブラリプロジェクトを作成するツール群はいくつかあり、たとえば以下のようなものがあります。
- brodybits/create-react-native-module
- react-native-community/bob
- frostney/react-native-create-library
- peggyrayzis/react-native-create-bridge
React Nativeの公式ドキュメントには create-react-native-module を使う記載があります。
To get set up with the basic project structure for a native module we will use a third party tool create-react-native-module. https://facebook.github.io/react-native/docs/native-modules-setup
react-native-create-library と react-native-create-bridge はともにGitHubリポジトリのスター数も多いですがmasterブランチの最後のコミットが2018年となっており、最近はあまり活発ではありません。
react-native-community/bobはcreate-react-native-moduleとよく似ていますが、デフォルトでESLintやprettierといったツールやビルド時のワークアラウンドが含まれていて、 react-native-community/react-native-device-info などで採用されています。
プロジェクト構成
実際に create-react-native-module を使ってプロジェクトを作成してみます。
npx create-react-native-module MyLibrary
すると以下の構成のプロジェクトが作成されます。このプロジェクトを雛形として開発を進めていきます。
react-native-my-library/ ├── README.md ├── android │ ├── README.md │ ├── build.gradle │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── reactlibrary │ ├── MyLibraryModule.java │ └── MyLibraryPackage.java ├── index.js ├── ios │ ├── MyLibrary.h │ ├── MyLibrary.m │ ├── MyLibrary.xcodeproj │ │ └── project.pbxproj │ └── MyLibrary.xcworkspace │ └── contents.xcworkspacedata ├── package.json └── react-native-my-library.podspec
この構成は最低限のプロジェクト構成ですので、TypeScriptやESLintなど必要に応じて追加していきます。
作成されたプロジェクトを見てみると、 package.json
、 index.js
の他に各Native Moduleのための構成が android
、 ios
以下にそれぞれ生成されています。また、プロジェクト直下に react-native-my-library.podspec
が生成されているのがわかります。
Autolinking
React Native 0.60より導入されたAutolinkingは従来Native Moduleが含まれるパッケージをプロジェクトに追加する際に必要だった react-native link
に相当するステップをビルド時に自動的に行ってくれます。
iOSのビルドにおいては、このときパッケージに含まれているpodspecを参照するのですが、その際のデフォルトのロケーションがプロジェクトルートとなっているため、 react-native-my-library.podspec
はプロジェクト直下にあるのです。
cli/autolinking.md at master · react-native-community/cli
この設定は react-native.config.js
をプロジェクトに追加することで変更できます。以下のようにpodspecのロケーションを変更します。
const path = require('path'); module.exports = { dependency: { platforms: { ios: { podspecPath: path.join(__dirname, 'ios', 'react-native-my-library.podspec') } } } };
exampleアプリ
次に公式のドキュメントに従い、yarn install
したのちにプロジェクト内にReact Nativeアプリを作成します。
cd react-native-my-library react-native init example
開発するライブラリパッケージをexampleに追加します。
cd example yarn add file:../
npmパッケージを開発する際に npm link
や yarn link
によってシンボリックリンクを作成する手法がありますが、React Nativeではこの方法はうまくいきません。1
開発するディレクトリ
Native Moduleを開発する際は、exampleプロジェクト上で開発するとReact Native本体への依存がネイティブ上で解決できるため便利です。
iOSの場合
iOSの場合は、まずCocoaPodsでexampleプロジェクトで依存するライブラリをインストールします。
cd ios pod install
Xcodeで example/ios/example.xcworkspace
を開きます。
左側のProject Navigatorで Pods
というプロジェクトを開くと、Development Pods
の中に react-native-my-library
を確認できます。
このファイルはどこにあるのかというと、 react-native-my-library/example/node_modules/react-native-my-library/ios/
です。 react-native-my-library/ios
に書いたコードは yarn install
で react-native-my-library/example/node_modules/
に追加され、次に pod install
でAutolinkingによってnode_modules配下のライブラリをPodsに追加していきます。
Development Podsの中ではReact Nativeのクラスが名前解決されるので、ここでNative Moduleを実装していきます。
Androidの場合
AndroidのNative Moduleを開発する際は、Android Studioで example/android/build.gradle
を開きます。Gradleのビルドが完了すると左側のProject Navigatorに react-native-my-library のモジュールが表示されるようになります。
iOS同様、表示されているのは react-native-my-library/example/node_modules/react-native-my-library/android/
です。ここでNative Moduleの開発をしていきます。
コードの変更を反映させる
上記のようなプロジェクト構成の場合、exampleで挙動を確認するためには以下のステップが必要になります。
- iOSまたはAndroidのコードを変更する
example
に移動しyarn add file:../
し直す
このようなステップを何度も繰り返すのは手間です。
そこで react-native-my-library/package.json
に以下のようなスクリプトを追加します。
"dev-sync": "cp -r *podspec *.js android ios example/node_modules/react-native-my-library/",
コードの変更を反映したければ npm run dev-sync
することで対象のコードだけ差し替え、不要なnode_moduleの再取得を回避できます。
これは react-native-community/react-native-share などで使われているテクニックです(オリジナルかはわかりませんでした)。
ローカルパッケージのnode_modulesを取り除く
上記のように作成したプロジェクト構成では問題が発生します。
exampleアプリにローカルの react-native-my-library
を追加しますが、このとき react-native-my-library/node_modules
配下もコピーされ実行時エラーの原因となります。
この問題にはいくつかの解決方法があるので紹介します。
1. postinstallで指定のファイルを取り除く
exampleプロジェクトの postinstall
で、本来node_modules配下にコピーされる必要のない不要な依存を削除する方法です。
これは、react-native-create-library や react-native-create-module で採用されている方法です。ただし、react-native-create-moduleの場合、プロジェクトを生成する際にexampleプロジェクトを同時に生成する場合のみ適用されるようになっています。デフォルトでexampleプロジェクトを生成するオプションはfalseとなっているため、先に説明したような手順で作成したプロジェクトでは有効になりません。
# プロジェクト作成時にexampleプロジェクトも生成する npx create-react-native-module MyLibrary --generate-example
2. metroのBlackListを使う
これは react-native-community/bob が採用している方法です。以下のようにプロジェクトルートのnode_modulesを重複して読み込まないようにしています。
resolver: { blacklistRE: blacklist([ new RegExp(`^${escape(path.join(root, 'node_modules'))}\\/.*$`), ]), extraNodeModules: modules.reduce((acc, name) => { acc[name] = path.join(__dirname, 'node_modules', name); return acc; }, {}), },
3. exampleとプロジェクトを分けない
react-native-community/react-native-webview などではexampleディレクトリに package.json
はなく、プロジェクトルートの package.json
に以下のようにexampleアプリを起動するスクリプトが追加されています。
"scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", "start:android": "react-native run-android --root example/", "start:ios": "react-native run-ios --project-path example/ios --scheme example",
まとめ
React NativeのNative Moduleライブラリの開発手法について説明しました。 現時点の個人的なCLIツールの選定としては、最小限の構成を作りたいのであれば create-react-native-module を、LinterやFormatterなどの初期設定まで任せるのであれば react-native-community/bob を検討するのが良いと思います。
React Nativeは開発者によって多くのライブラリが公開されていますが、特にreact-native-communityには多くのNative Moduleライブラリがあり開発ノウハウが詰まっているので、それらも参考にすると良いと思います。
Author
tatsuyakit