小记React Native与原生通信(iOS端)

2,107 阅读8分钟

emmm…… 先说个题外话,时隔一年,再遇RN,较之以前唯一不同的一点就是遇到的坑终于有人先踩了😂本文会通过原生与RN页面相互跳转、方法间的相互调用、以及H5页面调用原生页面进而调用RN页面等方面来阐述原生与RN间的通信。不要疑惑为啥子会有这种撒娇三连的操作,我也只能摊手道:存在即合理(无奈╮(╯▽╰)╭.gif)。

一、原生与RN通信

先做点准备工作叭~ 通过react-native init创建一个RN的新项目,此后将会得到一个内部带有iosandroid目录的文件夹。把这两个目录下的文件换成自己的项目。位置如下图所示。

修改podfile文件,将RN需要的库引入到自己的项目中。

pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector"
  pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
  pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
  pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
  pod 'React', :path => '../node_modules/react-native/'
  pod 'React-Core', :path => '../node_modules/react-native/'
  pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
  pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
  pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
  pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
  pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
  pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
  pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
  pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
  pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
  pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
  pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
  pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'

  pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
  pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
  pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
  pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
  pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon"
  pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
  pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga'

  pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
  pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
  pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

1、 原生跳RN页面

RCTRootView是一个可以将RN视图封装到原生组件中并且提供联通原生和被托管端接口的UIView容器。properties属性用于在React中将信息从父组件传递给子组件。 RCTRootView在初始化函数之时,通过类型为NSDictionaryinitialProperties可以将任意属性传递给RN应用。这一字典参数会在RN内部被转化为可供组件调用的JSON对象。

1) 创建RN的桥接管理类(单例)实现RCTBridgeDelegate协议

// .h文件
#import <React/RCTBridgeModule.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTBridgeDelegate.h>


@interface XXXRCTManager : NSObject<RCTBridgeDelegate>
+ (instancetype)shareInstance;

// 全局唯一的bridge
@property (nonatomic, readonly, strong) RCTBridge *bridge;
@end
//.m文件
static XXXRCTManager *_instance = nil;
+ (instancetype)shareInstance{
    if (_instance == nil) {
        _instance = [[self alloc] init];
    }
    return _instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    if (_instance == nil) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _instance = [super allocWithZone:zone];
        });
    }
    return _instance;
}

-(instancetype)init{
    if (self = [super init]) {
        _bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
    }
    return self;
}

实现sourceURLForBridge方法。调试模式下,读取index文件资源,打包则读取jsbundle中的资源。

#pragma mark - RCTBridgeDelegate
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
# if DEBUG
        return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"
                                                              fallbackResource:nil];
# else
    return [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"jsbundle"];
#endif
}

2) 创建容纳RN页面的控制器

//.h
@interface XXXReactHomeViewController : UIViewController
@property(nonatomic,strong)NSString *rnPath; // 传递给RN的数据 页面名称

@end

在.m文件中初始化RCTRootView,并将其添加到控制器页面上

 NSDictionary *props = @{@"path" : self.rnPath};
 RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:[SZLRCTManager shareInstance].bridge                                                     moduleName:@"RN中AppRegistry注册的名字"                                              initialProperties:props];

如此一来,iOS页面就能跳转到RN项目的首页了。轻松加愉快啊。

2、 RN页面跳原生页面及调用原生方法

RCTBridgeModule是定义好的protocol,实现该协议的类,会自动注册到iOS代码中对应的Bridge中。Object-C Bridge上层负责与Object-C通信,下层负责和JavaScript Bridge通信,而JavaScript Bridge负责和JavaScript通信,如此就能实现RN与iOS原生的相互调用。 需要注意的是:所有实现RCTBridgeModule的类都必须包括这条宏:RCT_EXPORT_MODULE()。它的作用是自动注册一个Module,当原生的桥加载之时,这个Module可以在JavaScript Bridge中调用。 先来看一下它的定义:

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

由此可以看出RCT_EXPORT_MODULE接受字符串作为其Module的名称,如果不设置名称的话默认就使用类名作为Module的名称。

1)新建类实现RCTBridgeModule协议

// .h
@interface xxxModule : NSObject<RCTBridgeModule>
@end
//.m
RCT_EXPORT_METHOD(goBack){
    //    用通知的方式返回原生页面
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"configBack" object:nil];
    });   
}
  1. 在XXXReactHomeViewController即承载RN页面的控制器中,接收通知,并实现从RN返回到原生页面的方法
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(navagateBack) name:@"configBack" object:nil];
- (void)navagateBack{
    [self.navigationController popViewControllerAnimated:YES];
}

3)在RN的界面中,通过NativeModules引入原生的module类,并调用返回原生界面的方法。

import {
  NativeModules,
} from 'react-native';
 onPressBack={() => {
        NativeModules.xxxModule.goBack();
        }}

以上骚操作已经可以满足RN跳转到原生界面的需求了。 however,在实际项目中,这还远远不够。比如说me正在进行的项目,需要将登录获取到的token传递给RN界面,一旦失效,则立即唤起原生的登录页面。

咳咳,好累ヽ( ̄▽ ̄)و坐直了。

…………………………………………假装我是分割线……………………………………

3、将原生参数传递给RN

将原生的参数传递给RN,或是让RN实现原生的某些操作可以通过RCT_EXPORT_METHOD实现。它是用来定义被JavaScript调用的方法的宏。RCT_EXTERN_METHOD调用了宏RCT_EXTERN_REMAP_METHOD。下面是该宏的定义:

#define RCT_EXTERN_REMAP_METHOD(js_name, method) \
  + (NSArray<NSString *> *)RCT_CONCAT(__rct_export__, \
    RCT_CONCAT(js_name, RCT_CONCAT(__LINE__, __COUNTER__))) { \
    return @[@#js_name, @#method]; \
  }

由此可以看出,它的作用是在RCT_EXPORT_MODULE定义的Module下面,定义一个可以被JavaScript调用的方法。 RCT_EXPORT_MODULE的使用,需要写入方法名,参数以及完整的实现。

  1. 原生定义方法
// 获取token
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getToken)
{
    NSString *token = [[NSUserDefaults standardUserDefaults]objectForKey:@"token"];
    return token;
}
// 退出登录
RCT_EXPORT_METHOD(signOut){
    dispatch_async(dispatch_get_main_queue(), ^{
        AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
        LoginViewController *loginVC = [[LoginViewController alloc]init];
        UINavigationController *nav = [[UINavigationController alloc]initWithRootViewController:loginVC];
        appDelegate.window.rootViewController = nav;
    });
    
}
  1. RN方调用
import { NativeModules } from 'react-native';
// 拿token
  requestObj.headers.Authorization = NativeModules.config.getToken();
  // 调用原生的退出登录方法
   NativeModules.XXXModule.signOut();

4、 多入口跳转到RN不同的页面

项目中有这样一个需求,要从不同的原生页面进入到不同的RN页面。此时,单纯通过导航跳转就无法解决该问题了。

在初始化RCTRootView之时,通过initWithBridge:(RCTBridge *)bridge方法将要展示的页面路径通过属性传递给RN。RN方接收到信息,再根据传入的路径决定要跳转到哪个页面。 1) 原生端传入数据 创建RCTRootView的代码在上文中已给出。在需要跳转的类中,传递字段。

 XXXReactHomeViewController *reactVC = [[XXXReactHomeViewController alloc]init];
            reactVC.rnPath = @"SugarStack";
            [self.navigationController pushViewController:reactVC animated:YES];
  1. RN端接收属性并跳转页面 在本项目中,采用的是react-navigation导航栏控制器。 飞机票👻:react-navigation
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';

每个栈中都存放不同页面。如:

const SugarStack = createStackNavigator({
  SugarFriend,
  SugarFriendDetail,
  RosterSearch,
});

将栈放入到导航中去,一次只显示一个屏幕。通过从原生接收的参数path来判断要显示哪个屏幕。

const App = function (props) {
  const AppNavigator = createSwitchNavigator(
    {
      AppStack,
      SugarStack,
    },
    {
      initialRouteName: props.path || 'AppStack',
    },
  );
  const Navigation = createAppContainer(AppNavigator);
  return (
    <Provider store={store}>
      <StatusBar translucent backgroundColor="#00000000" barStyle="dark-content" showHideTransition="Slide" />

      <Navigation />
    </Provider>
  );
};

5、 H5页面调用原生页面进而调用RN页面(吐血三连)

这波骚操作源于项目本身就是一个H5与原生混合的app,其中有一个酱紫的功能。H5页显示一条消息提醒用户有待办事项,而用户点击进行处理的操作是需要跳转到RN页面的。如果按照前文中带参跳转也只能跳转到RN栈的第一个页面。因此需要使用到deep-link方案。深度链接是一项可以让一个App通过一个URL地址打开,之后导航至特定页面或者资源,或者展示特定UI的技术 传送门👻:Deep linking

1)RN配置导航容器,使其能够从传入应用程序的 URI 中提取路径。

const SimpleApp = createAppContainer(createStackNavigator({...}));
const prefix = 'mychat://';
const MainApp = () => <SimpleApp uriPrefix={prefix} />;

2)在Appdelegate文件中,将iOS应用程序配置为使用 mychat:// URI 方案打开。

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:app openURL:url options:options];
}

3)在xcode中,设置info->URL Type为mychat

二、打包

1) 导出js bundle包和图片资源 终端进入RN项目的根目录下创建文件夹,此处名为release_ios

react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_ios/

entry-file代表入口文件,platform是平台的意思,后面一串是指输出资源到哪个文件或文件夹。

2) 将资源包导入到iOS项目。 通过上述命令,可以在relise_ios文件夹下找到assetsmain.jsbundle。将这两个文件拖入到iOS工程下。勾选第一和第三选项

3) 打包发布 xCode->Product->Archive打ipa包

三、调试中遇见的一点小问题

iOS真机调试,reload的时候永远没反应,摇一摇弹出的调试界面也差了好几个按钮。把上文中所打的main.jsbundle移除后,真机运行直接奔溃。真真是一入红屏深似海:

Connection to http://localhost:8081/debugger-proxy?role=client timed out. Are you running node proxy? If you are running on the device, check if you have the right IP address in RCTWebSocketExecutor.m.

AFN弹出提示:“未能找到使用指定主机名的服务器”。也就是说RN并未调起js server。 确保mac和手机连的是同一网络之后,去xCode中搜索域名.xip.io。发现并没有这个文件。

在受到这两篇文章的启发之后,才明白 传送门👻: 在设备上运行 iOS 真机 No bundle URL present

我的iOS项目是从别处拷贝过来,而ip.txt文件是在没有设置SKIP_BUNDLING的情况下初次构建的时候创建的。在构建app之后,加入做了clean操作或者拷贝到其他机器,创建ip.txt的步骤就被省略了。 解决方法是:到guessPackagerHost方法中,不要返回localhost,直接返回本机地址即可。

关于null is not an object(evaluating '_RNGestureHandlerModule.default.Direction')

RN环境在6.0以上,React-navigation在4.x。重装过pod或者node module还是无济于事。遂在想是不是没有在podfile文件中加入。之后查询到该信息。

pod 'RNGestureHandler', :podspec => '../node_modules/react-native-gesture-handler/RNGestureHandler.podspec'