Web制作・アプリ開発をコストパフォーマンスで考えるなら

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という名前になってます。 Add Configuration

しかしこのままだと、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を開きます。 Edit Scheme

ReactNativeプロジェクト名のスキーマがはじめから存在していると思うので、 そのスキーマをDuplicate Schemeを選択して複製します。 Duplicate Scheme

複製したスキーマの名前は語尾にdevとつけてdev環境用だということをここでは示しておきます。 スキーマの設定で、Build Configurationの部分をDebugはDevDebugに、ReleaseはDevReleaseに設定していきます。 Setting Scheme

あとは追加したスキーマを選択してビルドが可能かどうか確認できれば設定完了です。 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-DefinedENVを追加してそれぞれのビルド環境を表す文字列を追加します。 Add User-Defined

次にInfo.plistにEnvの行を一つ追加します。 Add 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側のビルド設定に関してはやや複雑な感じになってしまいましたが、サーバの向き先変更などのやりたいことはこれで実現できると思います。