React Native Tech Blog

supported by maricuru (旧maricuru tech blogです)

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のライブラリプロジェクトを作成するツール群はいくつかあり、たとえば以下のようなものがあります。

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.jsonindex.js の他に各Native Moduleのための構成が androidios 以下にそれぞれ生成されています。また、プロジェクト直下に 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 linkyarn 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 を確認できます。

f:id:tatsuyakit:20200219162600p:plain

このファイルはどこにあるのかというと、 react-native-my-library/example/node_modules/react-native-my-library/ios/ です。 react-native-my-library/ios に書いたコードは yarn installreact-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 のモジュールが表示されるようになります。

f:id:tatsuyakit:20200219162606p:plain

iOS同様、表示されているのは react-native-my-library/example/node_modules/react-native-my-library/android/ です。ここでNative Moduleの開発をしていきます。

コードの変更を反映させる

上記のようなプロジェクト構成の場合、exampleで挙動を確認するためには以下のステップが必要になります。

  1. iOSまたはAndroidのコードを変更する
  2. 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


  1. 以前からissueとして存在し、またcreate-react-native-moduleではPRが作られているので将来的には解決するかもしれません。