React Native与原生(Android、iOS)混编,三
在做RN混编项目的时候或者面试的时候经常会遇到一些问题,总结起来有以下几种:
1、过多的注册RN组件( AppRegistry.registerComponent() );
2、从原生跳转指定的RN页面及传值问题;
3、路由处理:原生 -> React Native -> 原生 -> React Native,多次操作后的进栈出栈问题。
一、解决问题1需要使用 React-Navigation 这个库,然后创建一个 RootScreen.js 作为RN页入口,且这个页面不显示UI元素,每次进入RN页面都需要经过的页面进行转发。
1、先注册RootScreen页面:
Root: {screen: RootScreen}
2、设置RootScreen页面为初始化页面
const AppNavigator = createStackNavigator(
StackNavigator,
{
initialRouteName: "Root", // 默认显示界面
mode: 'card',
headerMode: 'screen',
defaultNavigationOptions:{
gesturesEnabled: true
},
transitionConfig: () => ({
//push动画(右进右出)
screenInterpolator: StackViewStyleInterpolator.forHorizontal,
})
}
);
export const AppContainer = createAppContainer(AppNavigator);
3、在RootScreen页面进行跳转,可以看到这里使用了重置路由的方法StackActions.reset()
,防止返回时能回到这个RootScreen页面。通过this.props.navigation.dispatch()
跳转页面,然后在打开的页面中按照正常取值的方法this.props.navigation.getParam()
取出对应的值即可。
其中两个参数:(1)RouteInfo.routeName
;(2)RouteInfo.routeParams
在后面会说到。
export default class RootScreen extends Component {
constructor(props) {
super(props);
const RouteName = RouteInfo.routeName;
const RouteParams = RouteInfo.routeParams;
this._push(RouteName, RouteParams);
}
/**
* 通过重置路由方式实现初始化不同的页面
* @param routeName 在StackNavigator中注册的页面
* @param params 如 {user_id: 21, money: 100}
* @private
*/
_push = (routeName: string, params?: NavigationParams) => {
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({routeName, params})]
});
this.props.navigation.dispatch(resetAction)
};
render() {
return null;
}
}
二、从原生进入RN页面传值,通过源码看到Android是以Bundle传进去,就是这个initialProperties
/** {@see #startReactApplication(ReactInstanceManager, String, android.os.Bundle, String)} */
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle initialProperties) {
startReactApplication(reactInstanceManager, moduleName, initialProperties, null);
}
iOS的源码也有一个initialProperties,且是个字典对象。
/**
* - Designated initializer -
*/
- (instancetype)initWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initialProperties:(nullable NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;
好了,知道了关键点就好做了。先给Android创建一个RNRouteInfo类,用来存放要进入的RN页面,以及要传进去的参数。其中:
1、NativeRouteInfo这个就是给RN的属性名称,RN根据这个属性取出传进去的值;
2、routeName是要打开的页面,这个值就在React-Navigation中注册的页面,比如前面Root: {screen: RootScreen}
中的Root;
3、routeParams就是要给routeName页面的参数。
到这里就可以解释前面问题一中第3点提到的两个参数(1)RouteInfo.routeName;(2)RouteInfo.routeParams
public class RNRouteInfo {
public static final String NATIVE_ROUTE_INFO = "NativeRouteInfo";
private String routeName;
private ArrayMap routeParams;
public String getRouteName() {
return routeName;
}
public void setRouteName(String routeName) {
this.routeName = routeName;
}
public ArrayMap getRouteParams() {
return routeParams;
}
public void setRouteParams(ArrayMap routeParams) {
this.routeParams = routeParams;
}
public Bundle getBundle(){
Bundle bundle = new Bundle();
//把对象转成json字符串传给RN
bundle.putString(RNRouteInfo.NATIVE_ROUTE_INFO, new Gson().toJson(this));
return bundle;
}
}
所以要打开某个页面,就像这样就行了
RNRouteInfo route = new RNRouteInfo();
route.setRouteName("TestOne");
ArrayMap<String, Object> map = new ArrayMap<>();
map.put("initTitle", "从Android首页过来");
route.setRouteParams(map);
startActivity(RNActivity.class, route.getBundle());
而iOS这边也是需要创建一个类RNRouteInfo.m,可以看到这边也定义了三个相同的属性名称
#import "RNRouteInfo.h"
@implementation RNRouteInfo
- (void)setRouteName:(NSString*)name {
routeName = name;
}
- (void)setRouteParams:(NSDictionary*)params {
routeParams = params;
}
- (NSDictionary *)toNSDictionary{
NSDictionary *dic;
if (routeParams == nil) {
dic = @{@"NativeRouteInfo":@{
@"routeName":routeName
}
};
}else{
dic = @{@"NativeRouteInfo":@{
@"routeName":routeName,
@"routeParams": routeParams
}
};
}
return dic;
}
@end
使用起来也很简单
RNViewController *vc = [[RNViewController alloc] init];
//初始化RN路由信息
RNRouteInfo *info = [[RNRouteInfo alloc] init];
//设置要进入的RN页面
[info setRouteName:@"TestOne"];
//设置要传入的参数
NSDictionary * params = @{@"initTitle": @"从iOS首页过来"};
[info setRouteParams:params];
vc.rnRouteInfo = info.toNSDictionary;
AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[app.nav pushViewController:vc animated:YES];
接下来就是重点了,RN页面接受传过来的值。RN要取到从原生传过来的值只能在AppRegistry.registerComponent()
中注册的组件中拿到,而在这里是注册了一个App
的组件,所以取值是this.props.NativeRouteInfo
。这里的构造函数中不只判断NativeRouteInfo
属性是否定义,还多了一层判断,这是因为iOS传进来的值可以是json对象,而Android传进来的只能是基本数据类型,所以这里要转成json对象。
到这里,再回去看RootScreen.js,整个模块的封装就穿起来。
export default class App extends Component {
constructor(props) {
super(props);
if(this.props.NativeRouteInfo){
if (typeof this.props.NativeRouteInfo === 'object'){//ios
global.RouteInfo = this.props.NativeRouteInfo
}else {//android
global.RouteInfo = JSON.parse(this.props.NativeRouteInfo);
}
}
}
render() {
return (
<AppContainer/>
);
}
}
三、路由问题
app中习惯了右进右出的转场效果,所以在Android中定义入栈动画
overridePendingTransition(R.anim.slide_in_right, 0);
而iOS中使用UINavigationController pushViewController
来进行。
而出栈动画需要原生定义都定义CommonModule
,且实现以下两个方法:
1、定义Android的出栈方法finish()
@ReactMethod
public void finish(){
if (getCurrentActivity() != null){
getCurrentActivity().finish();
getCurrentActivity().overridePendingTransition(0, R.anim.slide_out_right);
}
}
2、定义iOS的出栈方法finish()
RCT_EXPORT_METHOD(finish){
dispatch_async(dispatch_get_main_queue(), ^{
AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[app.nav popViewControllerAnimated:YES];
});
}
RN的的出栈方法是:先定义一个基类BaseScreen
,所有页面都应该继承这个基类来实现业务需求。
export default class BaseScreen extends React.Component {
constructor(props) {
super(props);
this._didFocusSubscription = props.navigation.addListener('didFocus', payload =>
BackHandler.addEventListener('hardwareBackPress', this._onBackButtonPressAndroid),
);
}
componentDidMount() {
this._willBlurSubscription = this.props.navigation.addListener('willBlur', payload =>
BackHandler.removeEventListener('hardwareBackPress', this._onBackButtonPressAndroid),
);
}
componentWillUnmount() {
this._didFocusSubscription && this._didFocusSubscription.remove();
this._willBlurSubscription && this._willBlurSubscription.remove();
}
_onBackButtonPressAndroid = () => {
this.navLeftClick();
return true;//拦截返回按钮默认事件
};
renderNavLeftView = () => {
return (
<TouchableOpacity activeOpacity={1} onPress={this.navLeftClick}>
<Text>返回</Text>
</TouchableOpacity>
);
};
navLeftClick = () => {
if (!this.props.navigation.goBack()) {
CommonModule.finish();
} else {
this.props.navigation.goBack();
}
};
......
通过判断this.props.navigation.goBack()
是否能返回,如果不能,表示RN的路由栈已经到底了,此时应该关闭当前页面(Activity、Controller)
,否则正常执行RN的出栈方法goBack()
if (!this.props.navigation.goBack()) {
CommonModule.finish();
} else {
this.props.navigation.goBack();
}
封装完之后就可以愉快的跳转页面了,不需要改动代码了