前回Expo Modulesの素振りをしたので、アプリに組み込んでいきます。
上記を参照すると、推奨方法として、前回参照したページのガイドが記載されていますが、あれだとモジュールが主でアプリが従のような形だったのでナゾ・・・。
で、別の方法として、モノレポにするか、npmへ公開する、とあります。
想定している使い方としては、製品としてのアプリ開発でプロプライエタリなライブラリを導入するケースなので、npmへ公開はナシです。
組み込み必要なライブラリが複数になることも考えて、モノレポにしようと思います。
モノレポ化する
構造改革
まずはapps
とpackages
ディレクトリを作成し、今あるアプリのコードをapps/labapp
へ移動します。
(.git
と.gitignore
とyarn.lock
はルートに残します)
ExpoLab % mkdir -p apps/labapp
ExpoLab % mkdir packages
ExpoLab % rm -rf node_modules/
ExpoLab % mv App.tsx assets babel.config.js *.json apps/labapp
移動したpackage.json
のname
を変更しておきます。
"name": "labapp"
次にルートに新しくpackage.json
を作成します。
{
"name": "expolab",
"version": "1.0.0",
"workspaces": ["apps/*", "packages/*"],
"private": true
}
この状態でyarn
を実行し、依存をインストールします。
ここまでを一旦コミットしておきましょう。
モノレポ下でのExpoLabアプリ実行
次にモノレポ環境に合わせてカスタムmetro.config
を作成します。
apps/expolab
へ移動して、まずは存在しないmetro.config.js
を生成します。
labapp % yarn expo customize metro.config.js
生成されたファイルの中身はこうなっていました。
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
module.exports = getDefaultConfig(__dirname);
サンプル通りnohoist
なしのモノレポなので、依存パッケージはworkspaceルートのnode_modules
に入ります。Metro Bundlerがこれを参照するよう設定を変更します。
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
// Find the project and workspace directories
const projectRoot = __dirname;
// This can be replaced with `find-yarn-workspace-root`
const workspaceRoot = path.resolve(projectRoot, '../..');
const config = getDefaultConfig(projectRoot);
// 1. Watch all files within the monorepo
config.watchFolders = [workspaceRoot];
// 2. Let Metro know where to resolve packages and in what order
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;
module.exports = config;
次にエントリーポイントを変更します。
Managedなアプリではデフォルトでindex.js
は存在せず、package.json
のmain
に"node_modules/expo/AppEntry.js"
と設定されています。Expoが汎用エントリーポイントを用意してくれてるんですね。
このままだとモノレポでは動作しないようなので、index.js
を作成します。
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
package.json
のmain
も"index.js"
に変更します。
これでアプリは準備できたので、確認してみます。
yarn start
a
でAndroidエミュレータ、i
でiOSシミュレータが起動するので、アプリを確認できれば完了です。
モジュールをモノレポへ
前回作ったmy-module
をモノレポへ移動します。
.git
とnode_modules
は削除しておきます。
my-module % rm -rf .git node_modules
my-module % cd ..
expo % mv my-module ExpoLab/packages
my-module/yarn.lock
の中身を全てコピーして、ExpoLab/yarn.lock
の末尾に追加します。
yarn
実行して依存インストールが済んだら、my-module/yarn.lock
は削除しておきます。
my-module/example
も不要なので削除してOKです。
アプリからモノレポ内のネイティブモジュールを利用する
labapp/package.json
にmy-module
を追加します。
"dependencies": {
"my-module": "*",
"expo": "~47.0.12",
追加したら、yarn
しておきます。
次に、labapp/App.tsx
にmy-module
を利用するコードを追加します。
import * as MyModule from 'my-module'; // <-- 追加
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
<Text>{MyModule.hello()}</Text>{/* <-- 追加*/}
<StatusBar style="auto" />
</View>
);
}
では、これで動かしてみましょう。
labapp % yarn start
:
› Press ? │ show all commands
Logs for your project will appear below. Press Ctrl+C to exit.
iOSシミュレータを起動してみます。
ERROR Error: Cannot find native module 'MyModule'
ERROR Invariant Violation: Failed to call into JavaScript module method AppRegistry.runApplication(). Module has not been registered as callable. Registered callable JavaScript modules (n = 11): Systrace, JSTimers, HeapCapture, SamplingProfiler, RCTLog, RCTDeviceEventEmitter, RCTNativeAppEventEmitter, GlobalPerformanceLogger, JSDevSupportModule, HMRClient, RCTEventEmitter.
A frequent cause of the error is that the application entry file path is incorrect. This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.
「MyModuleというネイティブモジュールがない」と怒られました。
これは、デフォルトの実行環境であるExpo Go(のネイティブ部分)に、MyModule
が存在しないため発生しているエラーです。
ならばMyModule
を含んだExpo Goがあれば解決・・・?
それがDevelopment Buildです。
Development Buildを試す
Expoアカウント作成
Development Build作成のためにEASを利用します。
ので、Expoアカウントが必要となりますので、ない場合は作ります。
# GitHubアカウント連携とかできて欲しい・・・
EASのセットアップ
eas-cli
をプロジェクトにインストールします。
ガイドではグローバルへのインストールが推奨されていますが、複数プロジェクトを扱う場合にバージョンによる挙動の問題に遭遇したくないのでローカルへインストールします。
labapp % yarn add -D eas-cli
インストールできたらログインし、
labapp % yarn eas login
EAS Buildのセットアップを行います。
labapp % yarn eas build:configure
対象プラットフォーム、EASプロジェクトを作成するかどうか、を聞かれますが、すべてデフォルトでOKです。
expo-dev-clientを設定
expo-dev-client
をインストールします。
labapp % yarn expo install expo-dev-client
App.tsx
に以下を追加します。
import 'expo-dev-client';
Development Build作成
Androidからいきます。
labapp % yarn eas build -e development -p android
アプリケーションIDを聞かれますがデフォルトでもOKです。
しばらく待つと・・・ビルドが失敗しました。
✖ Build failed
🤖 Android build failed:
Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.
上記では分かりにくいですが、"Run gradlew"
の部分はリンクになっていて、開くとEASのビルドログの該当箇所が確認できます。
どうやら、Kotlinのバージョンミスマッチのようです。
そういえば、最初にcreate-expo-appした際に、あえて1つ前のSDKにしてたんでした。
ネイティブモジュール側と合わない状態になっているようです。
これが原因ですね。
では、予告通り(?)、SDKバージョンを上げましょう。
こちらを参考に、アップグレードします。
SDK48を入れて・・・
labapp % yarn add expo@^48.0.0
SDK48に合わせた更新をします。
labapp % yarn expo install --fix
・・・これだけです。
簡単すぎますね。
素のReactNativeでバージョンアップする場合、こちらのページで利用中のバージョンと更新先のバージョンを選択して表示される差分を取り込みます。
https://react-native-community.github.io/upgrade-helper/
手動です。辛いです。
利用してるライブラリが動かなくなることもあります。
(これはexpoでも利用するライブラリによっては発生するでしょうけど)
動作確認含めて、結構な時間が掛かります。
バージョンアップできたところで、EASビルドをリトライします。
・・・今度は無事にビルドが完了し、QRコードが表示されます。
🤖 Open this link on your Android devices (or scan the QR code) to install the app:
https://expo.dev/accounts/sakai-y/projects/ExpoLab/builds/aff9798f-823b-4ba5-a7f5-295acfxxxxxx
? Install and run the Android build on an emulator? › (Y/n)
最後に「エミュレータで実行する?」って聞かれているので実行します。
EASからビルドしたアプリがダウンロードされ、どのエミュレータを使うのか聞いてきます。
至れり尽くせりですね。
インストールが完了し、アプリが起動するとnpx expo start --dev-client
しろと表示されています。
package.json
のstart
コマンドを書き換えましょう。
"scripts": {
"start": "expo start --dev-client",
expo start
します。
labapp % yarn start
a
キーでエミュレータで実行中アプリにロードされます。
無事にlabappからMyModuleを利用したコードが実行できています。
この状態でアプリのコード(ReactNative部分)を変更すると即座に反映されます。
Expo Goの時と変わらない開発体験で、かつ、カスタムネイティブコードを含んだアプリの開発ができるということです。
なんと素晴らしい。
iOSシミュレータで実行する
iOSシミュレータでも実行してみます。
まずはeas.json
にシミュレータ用のビルド設定を追加します。
"build": {
"development-simulator": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "m-medium",
"simulator": true
}
},
EASビルドします。
labapp % yarn eas build -e development-simulator -p ios
しばらく待つと完了し、Androidの時と同じようにシミュレータ起動するか聞いてきます。
✔ Build finished
🍎 iOS app:
https://expo.dev/artifacts/eas/6v2Spif98ubTZLxxxxxxxx.tar.gz
? Install and run the iOS build on a simulator? › (Y/n)
シミュレータにインストール完了して起動が確認できたら、先ほどと同様にyarn start
してi
キーでシミュレータへロードします。
まとめ
ちょっと長くなりましたが、Managed workflowのアプリでカスタムネイティブコードを利用するために、モノレポ構成にした上で、Expo Modulesで作ったネイティブモジュールを組み込むところまでできました。
次は、プロプライエタリなライブラリをExpo Moduleとして作成して、アプリから利用してみたいと思います。
つづく