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开发调试
这里只说我的调试方式,更详细的请查阅调试文档。
- 在
AndroidManifest.xml中添加<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />启用调试模式。 - 通过
npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start。 - 真机安装APP进入RNApp界面,摇动手机可以打开开发者列表,通过设置Dev Host可以连接RN服务。Reload可以重载RNApp。
 - 通过在Android Studio输出中检索React可以查看RN的输出(包括console)。
 
这里的调试基于HOST Bundle获取方式上,也就是说你的Bundle获取地址应当是默认的
super.getJSBundleFile()。
其他问题
原生加载RN白屏问题
白屏是因为加载Bundle文件过慢导致的,这个网上有很多的解释了。这里我的解决办法是在App开启动画的Activity里执行了一次加载。private void preLoadReactNative() { // 1.创建ReactRootView ReactRootView rootView = new ReactRootView(this); rootView.startReactApplication( ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(), "MyReactNativeApp", null); }返回按键问题
这里的返回键分为两种,一种是手机硬件后退按键,另一种是导航栏后退按键。手机后退这里无需做处理,导航栏后退如下处理: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开发调试
这里只说我的调试方式,更详细的请查阅调试文档。
- 通过
initWithBundleURL方式连接本地RN服务 - 通过
npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start。 - command + R可以在RN界面Reload RN
 - 通过在Xcode输出中检索React可以查看RN的输出(包括console)。
 
遗留问题
- android项目依赖maven 
{ url "$rootDir/../node_modules/react-native/android" } - iOS项目依赖怎么合并到打包脚本中
 - iOS编译问题如何在流程中解决
 - 登陆验证问题
 
资源地址
服务器172.16.192.249/mobile_node/魏晓峰/RN