React Nativeにおける環境別ビルド設定
こんにちは。graphy事業部の遠藤と申します。
今回はReact Nativeを使ったアプリ開発における、 development/staging/production環境毎のビルド設定について記載していきたいと思います。
環境別ビルドの目的
環境別に分けたいものとして大きく以下のような要素があると思います。
- APIの向き先の変更
- Firebaseなど外部サービスの設定ファイルの選択
- 環境別にアプリを準備
React Nativeで環境別のビルドについて、Googleで調べてみると先人達が作ったライブラリを利用することで、 ビルド設定をある程度簡単に準備することができると思います。
ライブラリを使ったビルド設定
私が調べた限りでは、以下の2つのライブラリを利用する形で環境別ビルド設定が実現できます。
しかし、上記のライブラリを利用して環境別ビルドの設定方法を考えてみたのですが、 最初に上げた要素をすべていい感じに分ける方法が思いつかず、 iOS, Androidプロジェクトそれぞれに対し独自にビルド設定を行っていく形を採用したほうが良いと判断しました。
今回はそのビルド設定について詳しく記載していきます。
iOSのビルド設定
Xcodeプロジェクトを開いて設定をしていきます。 React Nativeプロジェクトは初期化した段階では、Reactなどの必要ライブラリがXcodeプロジェクトを取り込む形で構成されていると思います。 ですがこの後のビルド設定でうまく設定を行うために、 必要ライブラリをcocoapodsで取り込む形に修正しておきます。 初期状態で必要な設定は公式ページのPodfileを移して以下のような感じになると思います。
# Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'sample' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Your 'node_modules' directory is probably in the root of your project,
# but if not, adjust the `:path` accordingly
pod 'React', :path => '../node_modules/react-native', :subspecs => [
'Core',
'CxxBridge', # Include this for RN >= 0.47
'DevSupport', # Include this to enable In-App Devmenu if RN >= 0.43
'RCTText',
'RCTNetwork',
'RCTWebSocket', # Needed for debugging
'RCTAnimation', # Needed for FlatList and animations running on native UI thread
# Add any other subspecs you want to use in your project
]
# Explicitly include Yoga if you are using RN >= 0.42.0
pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
# Third party deps podspec link
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
# Pods for sample
target 'sampleTests' do
inherit! :search_paths
# Pods for testing
end
end
Dev環境用の設定をここから追加していきます。
まずはConfigurationsの追加を行います。
Debug, Releaseをそれぞれ複製します。ここではDev環境用ということでDevDebug, DevReleaseという名前になってます。
しかしこのままだと、DevDebugのビルド設定がデバッグ用になっていない問題があり、デバッグビルドが成功してもすぐにクラッシュしてししまう問題に当たります。 この問題を修正するためにPodfileにデバッグ用のConfigurationに追加設定を行います。
# Podfile
post_install do |installer|
installer.pods_project.build_configurations.each do |config|
if config.name.include?("Debug")
# Set optimization level for project
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0'
# Add DEBUG to custom configurations containing 'Debug'
if !config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'].include? 'DEBUG=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'DEBUG=1'
end
end
end
end
次にProduct>Scheme>Edit Schemeを開きます。
ReactNativeプロジェクト名のスキーマがはじめから存在していると思うので、
そのスキーマをDuplicate Schemeを選択して複製します。
複製したスキーマの名前は語尾にdevとつけてdev環境用だということをここでは示しておきます。
スキーマの設定で、Build Configurationの部分をDebugはDevDebugに、ReleaseはDevReleaseに設定していきます。
あとは追加したスキーマを選択してビルドが可能かどうか確認できれば設定完了です。 CLIでは以下のような形でスキーマを選択してビルドすることができます。
react-native run-ios --configuration DevDebug --scheme sample-dev
development環境の設定をここまで行いました、 stagingとproduction環境についても同様の作業を行うことで設定できます。
ここまでできれば、BuildSettingsでそれぞれのConfigurationに対して個別に設定が行えるので、 マクロを追加するなりアイコンを設定するなりが設定可能となります。 Xcodeでは${CONFIGURATION}の環境変数が取れるので${CONFIGURATION}を条件分岐して、 使用するFirebaseの設定ファイルを選択させることもできます。
Androidのビルド設定
Androidの方では、gradleに便利な機能があるのでbuild.gradleに以下のような設定追加します。 flavorDimensionsを利用することで、それぞれのenvに対してdebugとreleaseビルドの組み合わせを作ることができます。
// build.gradle
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationId "com.sample.dev"
}
stg {
dimension "env"
applicationId "com.sample.stg"
}
prd {
dimension "env"
applicationId "com.sample"
}
}
Androidの最低限のビルド設定はこれくらいで、同じくCLIでビルドすることができます。 Android Studioでももちろん可能です。
react-native run-android --variant devDebug --appId com.sample.dev
JSから環境情報を取得
iOS, Androidのそれぞれのビルド設定が完了しましたが、 このままではJS側で現在のビルド環境を判定することができないので取得できるように追加の実装を行っていきます。
iOS側では、まずUser-DefinedにENVを追加してそれぞれのビルド環境を表す文字列を追加します。
次にInfo.plistにEnvの行を一つ追加します。
JSから呼び出すためにモジュールを追加します。
// BuildConfig.h
#ifndef BuildConfig_h
#define BuildConfig_h
#import <React/RCTBridgeModule.h>
@interface BuildConfig : NSObject <RCTBridgeModule>
@end
#endif /* BuildConfig_h */
// BuildConfig.m
#import "BuildConfig.h"
@implementation BuildConfig
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
- (NSDictionary *)constantsToExport {
NSString *env = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"Env"];
return @{@"ENV": env};
}
@end
iOS側の設定はここまでで、 次はAndroid側にも設定を追加していきます。 build.gradleにbuildConfigFieldをそれぞれ追加します。
// build.gradle
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationId "com.sample.dev"
buildConfigField "String", "ENV", "\"dev\"" // 追加
}
stg {
dimension "env"
applicationId "com.sample.stg"
buildConfigField "String", "ENV", "\"stg\"" // 追加
}
prd {
dimension "env"
applicationId "com.sample"
buildConfigField "String", "ENV", "\"prd\"" // 追加
}
}
BuildConfigModule.javaとBuildConfigPackage.javaを作成します。
// BuildConfigModule.java
package com.sample;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import java.util.Map;
import java.util.HashMap;
public class BuildConfigModule extends ReactContextBaseJavaModule {
public BuildConfigModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "BuildConfig";
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put("ENV", BuildConfig.ENV);
return constants;
}
}
// BuildConfigPackage.java
package com.sample;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class BuildConfigPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModule(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
new BuildConfigModule(reactContext)
);
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
MainApplication.javaにBuildConfigPackageを読み込ませて、Android側の設定は完了です。
// MainApplication.java
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new BuildConfigPackage() // 追加
);
}
...
最後に、JS側からネイティブモジュールを呼び出す実装を追加します。
// build-config.js
import { NativeModules } from "react-native";
export default {
get env() {
return NativeModules.BuildConfig.ENV;
}
};
あとはbuild-config.jsの関数を呼び出せば、 "dev", "stg", "prd"の文字列がそれぞれ返ってくる形になっていると思うのでJS側でビルド環境を判定できると思います。
まとめ
今回はReact Nativeの環境別ビルド設定の詳細について記載しました。 iOS側のビルド設定に関してはやや複雑な感じになってしまいましたが、サーバの向き先変更などのやりたいことはこれで実現できると思います。