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类
import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; /** * Created by jhjr on 18-4-24. */ 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; } /** * 在rn代码里面是需要这个名字来调用该类的方法 * @return */ @Override public String getName() { return MODULE_NAME; } /** * 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(null, 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<>(); Random rand = new Random(); int i = rand.nextInt(100); Object o = new Object(); o = (Object)(new Integer(i)); params.put("test",o); return params; } @ReactMethod public void startActivityFromJS(String path, ReadableMap params){ try{ Activity currentActivity = getCurrentActivity(); if(null!=currentActivity){ Map intentParams = ((ReadableNativeMap) params).toHashMap(); // 路由跳转 } }catch(Exception e){ throw new JSApplicationIllegalArgumentException( "不能打开Activity : "+e.getMessage()); } } @ReactMethod public void activityFinish() { try{ Activity currentActivity = getCurrentActivity(); currentActivity.finish(); }catch(Exception e){ throw new JSApplicationIllegalArgumentException( "不能打开Activity : "+e.getMessage()); } } /** * Promise * @param promise */ @ReactMethod public void getUserInfo(Promise promise) { WritableMap map = Arguments.createMap(); map.putString("name", "Android"); promise.resolve(map); } }
最后在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编译问题如何在流程中解决
- 登陆验证问题