React Native与原生(Android、iOS)混编,三

2019-11-09  本文已影响0人  干吧_7973

在做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();
}

封装完之后就可以愉快的跳转页面了,不需要改动代码了

源码:https://github.com/1280103995/RN-Android-iOS

上一篇下一篇

猜你喜欢

热点阅读