React-Native混编学习

React-Native混编学习

本篇主要涉及的是App和RN的混合开发环境搭建,对于基本的RN环境搭建请自行查阅文档

这里需要着重注意的是全局依赖:

  • node v8.1.3(nvm管理)
  • react-native-cli 0.53.3(npm全局包)
  • nrm(npm全局包,主要是为了管理npm源,切换npm源为taobao)
  • gulp(npm全局包,主要是打包zip压缩)

最好的方式是使用react-native init生成一个新的RN项目,参考它的package.json依赖。这里我使用的是"react": "16.2.0" "react-native": "0.53.3"

APP配合

  • 环境搭建
    • 依赖如何引入
  • 接口定义
    • 获取当前用户信息
    • 获取当前环境状态
    • 结束RN activity
    • 路由跳转结构定义(互相跳转的接口)
  • RN热更部分完善(Android和iOS)
    • 资源包地址重定义
    • 热更代码完善
  • RN开发调试
    • 兼容开发环境,方便我们调试(Bundle地址,开发模式启动)
  • 打包脚本修改
    • 依赖如何注入
    • 编译问题解决
  • 部分常量配置
    • RN服务地址常量
    • RN主组件名称
    • Bundle地址
  • RN路由定义
  • 登陆验证问题

android环境搭建

基础部分

  • RNApp嵌入原生
  • 原生跳转RN路由
  • 原生和RN的通信
  • RN资源管理
  • RN开发调试
  • 其他问题
    • 原生加载RN白屏问题
    • 回退问题

RNApp嵌入原生

1. 依赖引入

引入node_modules/react-native/android

  • 主build.gradle 添加

    maven {
        // All of React Native (JS, Android binaries) is installed from npm
        url "$rootDir/../node_modules/react-native/android"
    }
    
  • app/build.gradle 添加 compile "com.facebook.react:react-native:0.53.3"

    后需改为我们的android包管理

2. MyApplication类继承ReactApplication

  • onCreate中添加SoLoader.init(this, false);
  • Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; }代码如下:

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
      @Nullable
      @Override
      protected String getJSBundleFile() { // 定义RN Bundle文件地址
        return super.getJSBundleFile(); // 默认地址为assets/index.android.bundle
      }
    
      @Override
      public boolean getUseDeveloperSupport() { // 定义DEBUG模式
          return BuildConfig.DEBUG;
      }
    
      @Override
      protected List<ReactPackage> getPackages() { // RN包载入
          return Arrays.<ReactPackage>asList(
                  new MainReactPackage(),
                  new CommPackage(); // 载入公共包,见原生和RN通信
          );
      }
    };
    

后需重新定义Bundle文件地址,使之能够热更。

3. RNActivity编写

public class MyReactActivity extends ReactActivity {

  public Bundle getBundle() { // 获取props入参
      return getIntent().getExtras();
  }

  protected @Nullable String getMainComponentName() { // 定义RN组件名称
      return "MyReactNativeApp";
  }

  @Override
  protected ReactActivityDelegate createReactActivityDelegate() { // 通过getLaunchOptions传值
      return new ReactActivityDelegate(this, getMainComponentName()) { 
          @Nullable
          @Override
          protected Bundle getLaunchOptions() {
              return getBundle();
          }
      };
  }

}

至此,通过startActivity就能够正常打开一个RNApp了。

原生跳转RN路由

这里跳转可以有很多方式,不过最根本的是如何通过原生将要跳转的路由传递给RN。
主要分为两大类方式:(见本篇原生和RN通信)

  • 主动传递
  • 被动传递

这里我采用的是主动传递中的Props传递。

在原生开启RNApp的时候,可以传递一个Bundle作为最初的Props,路由及部分参数信息通过这个Bundle带给RN。

Android部分

  • ReactActivityDelegate类

    protected void loadApp(String appKey) {
      if (mReactRootView != null) {
        throw new IllegalStateException("Cannot loadApp while app is already running.");
      }
      mReactRootView = createRootView();
      mReactRootView.startReactApplication(
        getReactNativeHost().getReactInstanceManager(),
        appKey,
        getLaunchOptions()); // 在这里有个getLaunchOptions方法,就是传递Bundle的地方
      getPlainActivity().setContentView(mReactRootView);
    }
    
    protected @Nullable Bundle getLaunchOptions() {
      return null;
    }
    
  • MyReactActivity类
    我们这里通过在MyReactActivity类里重载getLaunchOptions方法,传递Bundle(参考RNApp嵌入原生代码)

RN部分
RN这里采用了react-native-router-flux作为路由管理插件。这里需要注意的是版本问题,测试发现"react-native-router-flux": "^4.0.0-beta.25""react-navigation": "^1.0.0-beta.22"可用。

相关代码如下:

import React from 'react';
import { Router, Scene, Actions } from 'react-native-router-flux';
import { getUserInfo, finishActivity } from './communication'

import PageOne from './modules/PageOne'
import PageTwo from './modules/PageTwo'
import PageThree from './modules/PageThree'
import PageFour from './modules/PageFour'

export default class App extends React.Component {
  constructor(props) {
    super(props);
    console.log("RN启动");
  }

  componentDidMount(){
    const rnKey = this.props.rnKey || "PageOne";
    Actions.reset(rnKey, this.props);
  }

  // 导航栏回退方法
  onBack () {
    let popRouter = Actions.pop();
    !popRouter && finishActivity();
  }

  render() {
    return (
      <Router>
        <Scene key="root" hideNavBar={true}>
          <Scene key="PageOne" back={true} hideNavBar={false} component={PageOne} title="PageOne" onBack={() => this.onBack()}/>
          <Scene key="PageTwo" back={true} hideNavBar={false} component={PageTwo} title="PageTwo" onBack={() => this.onBack()}/>

          {/* 用户信息获取,用户已登陆 */}
          <Scene key="PageThree" back={true} hideNavBar={false} component={PageThree} title="PageThree" onBack={() => this.onBack()}/>
          <Scene key="PageFour" back={true} hideNavBar={false} component={PageFour} title="PageFour" onBack={() => this.onBack()}/>
        </Scene>
      </Router>
    )
  }
}

原生和RN的通信

上面说道通信分为两种,主动传递和被动传递。这里的主动/被动是以原生为参照。具体的可以参考和原生端通信,这里只贴关键部分的代码。如下:

Android部分

  • Package类

    package xxx;
    
    import com.facebook.react.ReactPackage;
    import com.facebook.react.bridge.NativeModule;
    import com.facebook.react.bridge.ReactApplicationContext;
    import com.facebook.react.uimanager.ViewManager;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class CommPackage implements ReactPackage {
        public CommModule mModule;
    
        /**
        * 创建Native Module
        * @param reactContext
        * @return
        */
        @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            List<NativeModule> modules = new ArrayList<>();
            mModule = new CommModule(reactContext);
            modules.add(mModule);
            return modules;
        }
    
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    }
    
  • module类

    public class CommModule extends ReactContextBaseJavaModule {
    
        private ReactApplicationContext mContext;
        public static final String MODULE_NAME = "commModule";
        public static final String EVENT_NAME = "nativeCallRn";
        public static final String EVENT_NAME1 = "getPatchImgs";
        /**
        * 构造方法必须实现
        * @param reactContext
        */
        public CommModule(ReactApplicationContext reactContext) {
            super(reactContext);
            this.mContext = reactContext;
        }
    
        /** 主动传递
        * Native调用RN
        * @param msg
        */
        public void nativeCallRn(String msg) {
            mContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                    .emit(EVENT_NAME,msg);
        }
    
        /**
        * Callback 方式
        * rn调用Native,并获取返回值
        * @param msg
        * @param callback
        */
        @ReactMethod
        public void rnCallNativeFromCallback(String msg, Callback callback) {
    
            // 1.处理业务逻辑...
            String result = "处理结果:" + msg;
            // 2.回调RN,即将处理结果返回给RN
            callback.invoke(result);
        }
    
        /**
        * Promise
        * @param msg
        * @param promise
        */
        @ReactMethod
        public void rnCallNativeFromPromise(String msg, Promise promise) {
    
            Log.e("---","adasdasda");
            // 1.处理业务逻辑...
            String result = "处理结果:" + msg;
            // 2.回调RN,即将处理结果返回给RN
            promise.resolve(result);
        }
    
        /**
        * 向RN传递常量
        */
        @Nullable
        @Override
        public Map<String, Object> getConstants() {
            Map<String,Object> params = new HashMap<>();
            params.put("Constant","我是常量,传递给RN");
            return params;
        }
    
        @ReactMethod
        public void startActivityFromJS(String path, ReadableMap params){
            try{
                Activity currentActivity = getCurrentActivity();
    
                if(null!=currentActivity){
                    Map intentParams = ((ReadableNativeMap) params).toHashMap();
    
                    <!--startActivity-->
                }
            }catch(Exception e){
                throw new JSApplicationIllegalArgumentException(
                        "不能打开Activity : "+e.getMessage());
            }
        }
    
        @ReactMethod
        public void dataToJS(Callback successBack, Callback errorBack){
            try{
                Activity currentActivity = getCurrentActivity();
                String result = currentActivity.getIntent().getStringExtra("data");
    
                successBack.invoke(result);
            }catch (Exception e){
                errorBack.invoke(e.getMessage());
            }
        }
    }
    

最后在MyApplication中注入,查看上面嵌入RNApp部分的【载入公共包,见原生和RN通信】注释。

RN部分
RN部分正常引用调用就好,代码如下:

import { NativeModules } from 'react-native';

const { commModule } = NativeModules;

export function startActivity (appPath, params = {}) {
  if(appPath) {
    commModule.startActivityFromJS(appPath, params);
  }
}

export function finishActivity () {
  commModule.activityFinish();
}

export function getUserInfo() {
  return commModule.getUserInfo()
}

export function getEnv() {}

// test
export function rnCallNativeFromCallback(msg, callback) {
  commModule.rnCallNativeFromCallback(msg, callback);
}

export function rnCallNativeFromPromise(msg) {
  return commModule.rnCallNativeFromPromise(msg);
}

export function getConstans() {
  console.log(commModule.test)
}

RN资源管理

测试发现如果将打包的Bundle文件和资源文件放到统一目录下,就可以正常引用资源。
参考命令react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/assets

有需要做热更的可以将资源放到SD卡中,只需要指定以下Bundle文件路径,将资源和Bundle文件放到一起就好。

RN开发调试

这里只说我的调试方式,更详细的请查阅调试文档

  1. AndroidManifest.xml中添加<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />启用调试模式。
  2. 通过npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start
  3. 真机安装APP进入RNApp界面,摇动手机可以打开开发者列表,通过设置Dev Host可以连接RN服务。Reload可以重载RNApp。
  4. 通过在Android Studio输出中检索React可以查看RN的输出(包括console)。

这里的调试基于HOST Bundle获取方式上,也就是说你的Bundle获取地址应当是默认的super.getJSBundleFile()

其他问题

  1. 原生加载RN白屏问题
    白屏是因为加载Bundle文件过慢导致的,这个网上有很多的解释了。这里我的解决办法是在App开启动画的Activity里执行了一次加载。

    private void preLoadReactNative() {
        // 1.创建ReactRootView
        ReactRootView rootView = new ReactRootView(this);
        rootView.startReactApplication(
                ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(),
                "MyReactNativeApp",
                null);
    }
    
  2. 返回按键问题
    这里的返回键分为两种,一种是手机硬件后退按键,另一种是导航栏后退按键。手机后退这里无需做处理,导航栏后退如下处理:

    • RN

      onBack () {
        let popRouter = Actions.pop();
        !popRouter && finishActivity(); // 判断是否为RN首页,返回原生上个页面。
      }
      
    • 原生见本篇finishActivity方法。

部分代码参考ReactNativeApp项目

iOS环境搭建

基于Android环境搭建,iOS部署大同小异,这里仅介绍不同的部分。

依赖安装

修改Podfile,添加如下内容:(环境配置参考集成到现有原生应用)

# 'node_modules'目录一般位于根目录中
# 但是如果你的结构不同,那你就要根据实际路径修改下面的`:path`
pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'CxxBridge', # 如果RN版本 >= 0.45则加入此行
    'DevSupport', # 如果RN版本 >= 0.43,则需要加入此行才能开启开发者菜单
    # 这里注意一下!!!添加这些解决react-navigation的native module not be null的问题
    'ART',
    'RCTActionSheet',
    'RCTGeolocation',
    'RCTImage',
    'RCTNetwork',
    'RCTPushNotification',
    'RCTSettings',
    'RCTText',
    'RCTVibration',
    'RCTWebSocket', # 这个模块是用于调试功能的
    'RCTLinkingIOS',
]
# 如果你的RN版本 >= 0.42.0,则加入下面这行
pod "yoga", :path => "../node_modules/react-native/ReactCommon/yoga"

# 如果RN版本 >= 0.45则加入下面三个第三方编译依赖
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'GLog', :podspec => '../node_modules/react-native/third-party-podspecs/GLog.podspec' // 这里修改glog为GLog
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

end

然后运行pod install安装依赖

这里是离线的包,怎么去合并到打包流程?(可以通过npm install去解决,不过比较麻烦)

RNApp嵌入原生

和安卓一样,需要写一个类似于activity的壳子供给RN。

ReactView.h

#import <Foundation/Foundation.h>

@interface ReactView : UIViewController

@end

ReactView.m

#import "ReactView.h"
#import <React/RCTRootView.h>
#import <React/RCTBridgeModule.h>
#import "commModule.h" # 这里是RN和原生交互所需要的一些方法,可先去掉

@interface ReactView()<UINavigationBarDelegate>
@end
@implementation ReactView

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    # 这里的http和localhost要给一下权限
    NSString * strUrl = @"http://localhost:8081/index.bundle?platform=ios&dev=true";    
    NSURL * jsCodeLocation = [NSURL URLWithString:strUrl];

    # 除http之外,和安卓一样,也可以通过bundle
    RCTRootView * rootView = [[RCTRootView alloc] 
                                initWithBundleURL:jsCodeLocation
                                moduleName:@"MyReactNativeApp"
                                initialProperties:nil
                                launchOptions:nil];
    self.view = rootView;
}

#pragma mark - 导航条处理,取消RN的导航栏
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [navigationController setNavigationBarHidden:YES animated:YES];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

Bundle地址重定义

NSString *cachePath = @"XXX";
cachePath = [cachePath stringByAppendingPathComponent:@"index.ios.bundle"];
NSURL *jsCodeLocation = [NSURL URLWithString:cachePath];

中间遇到了几个编译问题导致build失败。问题与解决方法如下:

  • React Native iOS: Could not build module ‘yoga’: ‘algorithm’ file not found

  • RCTReconnectingWebSocket.h文件的#import <fishhook/fishhook.h> 显示error: ‘fishhook/fishhook.h’ file not found

    "scripts": {
        "postinstall": "sed -i '' 's#<fishhook/fishhook.h>#\"fishhook.h\"#g' ./node_modules/react-native/Libraries/WebSocket/RCTReconnectingWebSocket.m"
    }
    
  • resolveRCTValueAnimatedNode
    sed -i '' 's/#import <RCTAnimation\\/RCTValueAnimatedNode.h>/#import \"RCTValueAnimatedNode.h\"/' ./node_modules/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h

这样配个正常的iOS跳转地址,就可以正常跳转到RN了。这里的RN界面最好用一件简单的hello world测试下。

编译问题怎么合到打包代码中?

原生和RN的通信

通信一样是原生提供一下方法,可供给RN调用,直接上代码。

commModule.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface commModule : NSObject <RCTBridgeModule>

@end

commModule.m

#import "commModule.h"
#import <React/RCTConvert.h>

@implementation commModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(rnCallNativeFromCallback:(NSString *)msg callback:(RCTResponseSenderBlock)callback)
{
    NSString *result = [@"处理结果:" stringByAppendingString:msg];
    callback(@[[NSNull null], result]);
}

RCT_REMAP_METHOD(rnCallNativeFromPromise, msg:(NSString *)msg
                findEventsWithResolver:(RCTPromiseResolveBlock)resolve
                rejecter:(RCTPromiseRejectBlock)reject)
{
    NSString *result = [@"处理结果:" stringByAppendingString:msg];
    resolve(result);
}

RCT_REMAP_METHOD(getUserInfo, msg:(NSDictionary *)msg
                resolver:(RCTPromiseResolveBlock)resolve
                rejecter:(RCTPromiseRejectBlock)reject)
{

    resolve(@{@"name": @"iOS"});
}

RCT_EXPORT_METHOD(startActivityFromJS:(NSString *)path params:(NSDictionary *)params)
{
    # 路由跳转,注意UI主线程
}

RCT_EXPORT_METHOD(activityFinish)
{
    # 结束RN finish,注意UI主线程
}

- (NSDictionary *)getConstants
{
    int n = arc4random_uniform(100);
    return @{ @"test": @"test" };
}

@end

这样就可以在RN端调用了,调用方法和Android一样。

RN资源管理

测试发现如果将打包的Bundle文件和资源文件放到统一目录下,就可以正常引用资源。
参考命令react-native bundle --platform ios --dev false --entry-file index.ios.js --bundle-output ReactNative/ios/index.ios.bundle --assets-dest ReactNative/ios

有需要做热更的可以指定以下Bundle文件路径,将资源和Bundle文件放到一起就好。

RN开发调试

这里只说我的调试方式,更详细的请查阅调试文档

  1. 通过initWithBundleURL方式连接本地RN服务
  2. 通过npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start
  3. command + R可以在RN界面Reload RN
  4. 通过在Xcode输出中检索React可以查看RN的输出(包括console)。

遗留问题

  • android项目依赖maven { url "$rootDir/../node_modules/react-native/android" }
  • iOS项目依赖怎么合并到打包脚本中
  • iOS编译问题如何在流程中解决
  • 登陆验证问题

资源地址

服务器172.16.192.249/mobile_node/魏晓峰/RN