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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268