react native如何与原生通信?

1. 引言

做RN的混合应用很难避开与原生层面的通信,有时候 App 需要访问平台 API,但 React Native 可能还没有相应的模块封装;或者你需要复用 Objective-C、Swift 或 C++代码或者java代码,而不是用 JavaScript 重新实现一遍;又或者你需要实现某些高性能、多线程的代码,譬如图片处理、数据库、或者各种高级扩展等等。

2. 都需要哪些通信

如果创建过RN项目就会清楚,RN在创建项目的时候会生成部分原生代码,包括android原生代码和object-c的代码,然后主要的代码都是UI层面的javascript代码。有很多时候我们的RN工程是app的一部份,那么问题就来了,这里就需要RN生成的原生代码与RN层通信,RN原生侧和app原生侧通信。

2.1 RN原生侧如何和RN UI侧通信

(1)RN原生侧的android如何与UI侧的javascript通信?

​ 以官网的toast为例,假设我们希望可以从 Javascript 发起一个 Toast 消息(一种会在屏幕下方弹出、保持一段时间的消息通知)。

我们首先来创建一个原生模块。一个原生模块是一个继承了ReactContextBaseJavaModule的 Java 类,它可以实现一些 JavaScript 所需的功能。我们这里的目标是可以在 JavaScript 里写ToastExample.show('Awesome', ToastExample.SHORT);,来调起一个短暂的 Toast 通知。

创建一个新的 Java 类并命名为ToastModule.java,放置到android/app/src/main/java/com/your-app-name/目录下,其具体代码如下:

java 复制代码
// ToastModule.java

package com.your-app-name;

import android.widget.Toast;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import java.util.Map;
import java.util.HashMap;

public class ToastModule extends ReactContextBaseJavaModule {
  private static ReactApplicationContext reactContext;

  private static final String DURATION_SHORT_KEY = "SHORT";
  private static final String DURATION_LONG_KEY = "LONG";

  public ToastModule(ReactApplicationContext context) {
    super(context);
    reactContext = context;
  }
}

ReactContextBaseJavaModule要求派生类实现getName方法。这个函数用于返回一个字符串名字,这个名字在 JavaScript 端标记这个模块。这里我们把这个模块叫做ToastExample,这样就可以在 JavaScript 中通过NativeModules.ToastExample访问到这个模块。

java 复制代码
 @Override
  public String getName() {
    return "ToastExample";
  }

一个可选的方法getContants返回了需要导出给 JavaScript 使用的常量。它并不一定需要实现,但在定义一些可以被 JavaScript 同步访问到的预定义的值时非常有用。说实话这段代码在真实的开发中可有可无,并不是必须实现的。

java 复制代码
@Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
    constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
    return constants;
  }

要导出一个方法给 JavaScript 使用,Java 方法需要使用注解@ReactMethod。方法的返回类型必须为void。React Native 的跨语言访问是异步进行的,所以想要给 JavaScript 返回一个值的唯一办法是使用回调函数或者发送事件 很重要,关键所在,关乎到你能不能与原生层通信。

java 复制代码
@ReactMethod
  public void show(String message, int duration) {
    Toast.makeText(getReactApplicationContext(), message, duration).show();
  }

原生侧的方法定义好了就,接下来就是注册定义的这个模块,因为只有定义了这个模块才能在UI层的javascript代码中调用此模块。

在 Java 这边要做的最后一件事就是注册这个模块。我们需要在应用的 Package 类的createNativeModules方法中添加这个模块。如果模块没有被注册,它也无法在 JavaScript 中被访问到。

创建一个新的 Java 类并命名为CustomToastPackage.java,放置到android/app/src/main/java/com/your-app-name/目录下,其具体代码如下:

java 复制代码
// CustomToastPackage.java

package com.your-app-name;

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 CustomToastPackage implements ReactPackage {

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  @Override
  public List<NativeModule> createNativeModules(
                              ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();

    modules.add(new ToastModule(reactContext));

    return modules;
  }

}

这个 package 需要在MainApplication.java文件的getPackages方法中提供。这个文件位于你的 react-native 应用文件夹的 android 目录中。具体路径是: android/app/src/main/java/com/your-app-name/MainApplication.java.文档上说的是在mainApplication.java中,但是有很多时候我们是app的一部分,所以没有自己的mainApplication文件,也不能有,因为一个app只能有一个主文件,主文件必须在app那边。那这个时候我们该怎么办呢?首先把下面那段代码提出到自己的主文件中

java 复制代码
protected List<ReactPackage> getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
  packages.add(new CustomToastPackage()); // <-- 添加这一行,类名替换成你的Package类的名字 name.
  return packages;
}

如何让这段代码生效?

java 复制代码
    private void loadBundleFromFilePath(String bundleFile, String moduleName) {
        Log.i("Info","加载资源开始");
        mReactRootView = null;
        mReactRootView = new RNGestureHandlerEnabledRootView(this);
        String bundlePath = "assets://index.android.bundle";
        ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setCurrentActivity(this)
                .addPackages(getPackages())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .setJSBundleFile(bundlePath);
        mReactInstanceManager = builder.build();
        mReactRootView.startReactApplication(mReactInstanceManager, moduleName, null);
        setContentView(mReactRootView);
        Log.i("Info", "加载资源结束");
    }

就是在你初始化ReactInstanceManager的地方把这个方法执行并添加到RN的实例中。到此已经把android的方法注入到RN中,可以在javascript中调用。

调用方式如下:

javascript 复制代码
import { NativeModules } from 'react-native';
const ToastExample = NativeModules.ToastExample;
ToastExample.show('Awesome', ToastExample.SHORT);

是不是非常简单就让原生侧和javascript侧成功通信了?

(2)RN原生侧的iOS如何与UI侧的javascript通信?

​ 还是以官网的例子为例,我们开始。

​ 官网的例子就是在 Javascript 中可以访问到 iOS 的日历功能。

在 React Native 中,一个"原生模块"就是一个实现了"RCTBridgeModule"协议的 Objective-C 类,其中 RCT 是 ReaCT 的缩写。

objective-c 复制代码
// CalendarManager.h
#import <React/RCTBridgeModule.h>

@interface CalendarManager : NSObject <RCTBridgeModule>
@end

为了实现RCTBridgeModule协议,你的类需要包含RCT_EXPORT_MODULE()宏。这个宏也可以添加一个参数用来指定在 JavaScript 中访问这个模块的名字。如果你不指定,默认就会使用这个 Objective-C 类的名字。如果类名以 RCT 开头,则 JavaScript 端引入的模块名会自动移除这个前缀。

objective-c 复制代码
// CalendarManager.m
#import "CalendarManager.h"

@implementation CalendarManager

// To export a module named CalendarManager
RCT_EXPORT_MODULE();

// This would name the module AwesomeCalendarManager instead
// RCT_EXPORT_MODULE(AwesomeCalendarManager);

@end

你必须明确的声明要给 JavaScript 导出的方法,否则 React Native 不会导出任何方法。声明通过RCT_EXPORT_METHOD()宏来实现:

objective-c 复制代码
#import "CalendarManager.h"
#import <React/RCTLog.h>

@implementation CalendarManager

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}

@end

现在从 Javascript 里可以这样调用这个方法:

javascript 复制代码
import { NativeModules } from 'react-native';
const CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent(
  'Birthday Party',
  '4 Privet Drive, Surrey'
);

到此RN自己的原生侧与javascript如何通信已经全部介绍完成,我们在真实的项目中既然是通信么就必须双向才是真正的通信,这时候就需要我们在原生侧定义方法的时候传入一个callback或者使用promise来接收javascript传递过来的消息来做一些操作。我们在项目中使用的是promise。

2.2 RN原生侧与app原生侧通信

RN原生与app原生通信我们对于android我们使用单例的模式,对于iOS我们使用delegate代理的方式。

(1)RN原生侧如何通过单例与app安卓相互通信

​ 因为自己本身是前端,只能靠自己理解的java中的实例来讲,万一讲的不对,还请指正。

在Java编程中,单例模式(Singleton Pattern)是一种创建对象的设计模式,它确保一个类只有一个实例,并提供全局访问点以获取该实例。所以依靠这一点,这样就可以在RN原生侧的java和app共享这个实例,从而达到通信的作用。网上大多数的是单向的通信,要么是app调用RN的方法实现功能,要么是RN调用app的方法实现功能。都没办法完成我们想要双向通信的功能,所以和app的小伙伴一起想出来的这么一种方式来实现RN原生侧和app侧双向通信。

在RN原生侧这里使用静态变量和静态方法来实现单例模式,这种实现方式被称为饿汉式单例(Eager Initialization Singleton),它在类加载时就创建了实例,并在整个应用程序生命周期中保持不变。

示例代码:

java 复制代码
ppublic class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() { }

    public static Singleton getInstance() {
        return instance;
    }
}

在上述示例中,Singleton 类中的静态变量 instance 在类加载时被初始化,并通过静态方法 getInstance() 返回该实例。由于静态变量在类加载时就被创建,因此无需考虑多线程环境下的线程安全问题。

使用静态变量和静态方法实现的饿汉式单例模式具有简单、线程安全的特点。但请注意,由于它在类加载时就创建实例,因此可能会浪费一些内存空间,尤其在实例较为庞大或创建过程较为复杂时。

这样实现的单例模式可以保证在多线程环境下安全地创建单例对象。

接下来我们还需要创建两个回调函数的接口定义:OnSafetyClickListenerOnSafetyResultCallback。这些接口定义了一些回调方法,用于处理事件和获取结果。同时供了对回调函数实例进行获取和设置的方法,如 getSafetyClickListener()setSafetyClickListener()

代码如下:

java 复制代码
private OnSafetyClickListener safetyClickListener;
    private OnSafetyResultCallback safetyResultCallback;

    public OnSafetyClickListener getSafetyClickListener() {
        return safetyClickListener;
    }

    public OnSafetyResultCallback getSafetyResultCallback() {
        return safetyResultCallback;
    }

    public void setSafetyClickListener(OnSafetyClickListener safetyClickListener) {
        this.safetyClickListener = safetyClickListener;
    }

    public void setSafetyResultCallback(OnSafetyResultCallback safetyResultCallback) {
        this.safetyResultCallback = safetyResultCallback;
    }

    public interface OnSafetyClickListener {

        public void onSafetyClick(String string);
    }

    public interface OnSafetyResultCallback {
        void onSuccess(String string);

        void onFailure(String string);

    }

这样一个完整的单例就设计完成了。但是在代码中如何使用呢?

首先在RN原生侧的初始化方法中通过调用 Singleton.getsInstance() 方法获取 Singleton 的单例实例,并使用 setSafetyResultCallback() 方法设置一个新的 OnSafetyResultCallback 回调函数的实例。这个新的回调函数实例是通过匿名内部类的方式创建的,重写了 onSuccess()onFailure() 方法。

通过设置这个新的回调函数实例,可以在相应的事件触发时执行特定的逻辑。

java 复制代码
   @Override
    public void initialize() {
        super.initialize();
        Log.i("success","initialize");
        Singleton.getsInstance().setSafetyResultCallback(new Singleton.OnSafetyResultCallback() {
            @Override
            public void onSuccess(String string) {
                if ("Ex0000".equals(string)) {
                    return;
                }

                try {
                    JSONObject jsObject = new JSONObject(new JSONObject(string).getString("data"));
                    if ("xxxx".equals(jsObject.getString("requestNo"))) {
                    } else {
                        if(rnPromise != null) {
                            rnPromise.resolve(string);
                        }
                    }

                } catch (JSONException e) {
                    Toast.makeText(reactApplicationContext,"参数异常",Toast.LENGTH_SHORT).show();
                    e.printStackTrace();
                }

            }

            @Override
            public void onFailure(String string) {
                Log.i("fail", "failAuth");
                Log.i("fail", string);
                if(rnPromise !=null) {
                    rnPromise.resolve(string);
                }
            }

        });
    }

在App侧需要和RN原生侧通信的地方需要调用 Singleton.getsInstance() 方法获取 Singleton 的单例实例,并使用 setSafetyClickListener() 方法设置一个新的 OnSafetyClickListener 回调函数的实例。这个新的回调函数实例是通过匿名内部类的方式创建的,重写了 onSafetyClick() 方法。

java 复制代码
        Singleton.getsInstance().setSafetyClickListener(new Singleton.OnSafetyClickListener() {
            @Override
            public void onSafetyClick(String str) {
                String string = "测试数据";
                String result = null;
                if (Singleton.getsInstance().getSafetyResultCallback() != null) {
                    Singleton.getsInstance().getSafetyResultCallback().onAuthSuccess(string);
                }
                System.out.println("SDK------>>>APP:"+ str);
            }

        });

通过如上操作可以实现RN原生侧与app侧双向通信。

(2) RN原生侧如何通过委托delegation与app iOS相互通信

delegate就是一个对象A代表另一个对象B处理一些事情(实现某种功能)。这里的对象A为代理方,对象B为委托方。代理方所要处理的事情是委托方通过协议指定的。这里我们提到了协议,代理方和委托放三个概念。

首先在RN的原生iOS侧声明一个协议并在协议中明确可以做什么和必须做什么;然后委托方添加一个属性deleget,该属性向外界说明我有一个代理职位(需要遵从定义的协议),最后委托方在需要完成某种功能的(协议中已提前确定)时候,告诉代理帮我完成这个功能。

objective-c 复制代码
//
//  MessageSingleton.h
//  
//
//  Created by Visupervi on 2021/9/11.
//

#ifndef MessageSingleton_h
#define MessageSingleton_h


#endif /* MessageSingleton_h */

NS_ASSUME_NONNULL_BEGIN
@class MessageSingleton;

// 设置协议
@protocol MessageSingletonDelegate <NSObject>

typedef void (^MsgBlock)(NSObject *appBackMsg);

@property (copy) MsgBlock appBackMsgBlock;

- (NSObject *)messageSingleton:(MessageSingleton*) messageSingleton logNowDate:(NSDate*)nowDate;

- (void) messageSingleton:(MessageSingleton*) messageSingleton closeMsg:(NSString*)msg;

- (NSObject *)messageSingleton:(MessageSingleton*)messageSingleton sendMsg:(NSDictionary*)msg withCallBackBlock:(MsgBlock)successBlock withCallBackBlock:(MsgBlock)failBlock;

@end

@interface MessageSingleton : NSObject

+ (instancetype)sharedInstance;

/**
 *  添加单个 delegate 到MessageSingleton.
 */
- (void)addDelegate:(id<MessageSingletonDelegate>) messageSingletonDelegate;

/**
 *  从MessageSingleton里移除一个delegate
 */
- (void)removeDelegate:(id<MessageSingletonDelegate>) messageSingletonDelegate;

/**
 *  移除所有添加过的delegate.
 */
- (void)removeAllDelegates;

// 定义block
typedef void (^MessageBlock)(NSObject *backMsg);

@property (copy) MessageBlock messsageBlock;

// 函数Block回掉接口函数
- (void)callMessageSingletonUpdateDelegate:(NSObject *) params withCallBackBlock: (MessageBlock) msgDataBlock;
- (void)callMessageSingletonCloseView:(NSString *)string;

@end

NS_ASSUME_NONNULL_END
objective-c 复制代码
//
//  MessageSingleton.m
//  
//
//  Created by Visupervi on 2021/9/11.
//

#import <Foundation/Foundation.h>
#import "MessageSingleton.h"

@interface MessageSingleton()
/**
 *  用来存储所有添加过的delegate
 *  NSHashTable 与 NSMutableSet相似,但NSHashTable可以持有元素的弱引用,而且在对象被销毁后能正确地将其移除。
 */
@property (strong, nonatomic, readonly) NSHashTable *delegates;

/**
 *  delegateLock 用于给delegate的操作加锁,防止多线程同时调用
 */
@property (strong, nonatomic) NSLock *delegateLock;
@end

@implementation MessageSingleton
+ (instancetype)sharedInstance {
    static MessageSingleton *messageSingleton = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        messageSingleton = [[self alloc] init];
    });
    return messageSingleton;
}

#pragma mark - get & set
- (instancetype)init {
    self = [super init];
    if (self) {
        _delegates = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];
    }
    return self;
}

- (NSLock *)delegateLock {
    if (_delegateLock == nil) {
        _delegateLock = [[NSLock alloc] init];
    }
    return _delegateLock;
}

#pragma mark - delegate
- (void)addDelegate:(id<MessageSingletonDelegate>) messageSingletonDelegate {
    
    [self.delegateLock lock];//防止多线程同时调用
    [self.delegates addObject:messageSingletonDelegate];
    [self.delegateLock unlock];
}

- (void)removeDelegate:(id<MessageSingletonDelegate>) messageSingletonDelegate {
    [self.delegateLock lock];//防止多线程同时调用
    [self.delegates removeObject:messageSingletonDelegate];
    [self.delegateLock unlock];
}

- (void)removeAllDelegates {
    [self.delegateLock lock];//防止多线程同时调用
    [self.delegates removeAllObjects];
    [self.delegateLock unlock];
}

#pragma mark - call delegates
- (void)callMessageSingletonLogNowDateDelegate {
    [self.delegateLock lock];
    for (id delegate in self.delegates) {//遍历delegates ,call delegate
        if ([delegate respondsToSelector:@selector(messageSingleton:logNowDate:)]) {
            [delegate messageSingleton:self logNowDate:[NSDate date]];
        }
    }
    [self.delegateLock unlock];
}

- (void)callMessageSingletonUpdateDelegate: (NSObject *) params withCallBackBlock:(MessageBlock) msgDataBlock{
    [self.delegateLock lock];
    
    for (id delegate in self.delegates) {
        if ([delegate respondsToSelector:@selector(messageSingleton:sendMsg:withCallBackBlock:withCallBackBlock:)]) {
            [delegate messageSingleton:self sendMsg:params withCallBackBlock:^(NSObject *  appBackMsg) {
                msgDataBlock(appBackMsg);
            } withCallBackBlock:^(NSObject * appBackMsg) {
                msgDataBlock(appBackMsg);
            }];
//
        }
    }
    [self.delegateLock unlock];
}

-(void)callMessageSingletonCloseView: (NSString *)string{
    [self.delegateLock lock];
    
    for (id delegate in self.delegates) {
        if ([delegate respondsToSelector:@selector(messageSingleton:closeMsg:)]) {
            
            [delegate messageSingleton:self closeMsg:string];
//
        }
    }
    [self.delegateLock unlock];
}


@end

经过上面的操作就在RN的iOS原生侧完成了委托的设计,然后app在需要帮我们RN侧完成一些事情的时候只需要在用到的时候实现一下MessageSingleton。示例代码如下:

objective-c 复制代码
@interface ViewController () <MessageSingletonDelegate>

@end

  - (void)messageSingleton:(MessageSingleton*) messageSingleton sendMsg:(NSDictionary*)msg withCallBackBlock:(MsgBlock)successBlock withCallBackBlock:(MsgBlock)failBlock{
    NSMutableDictionary *myOptions = [NSMutableDictionary dictionary];
    NSLog(@"%s", "SDK传给原生");
    NSLog(@"%@", msg);
    [myOptions setValue:@"********message from App *********" forKey:@"msg"];
    successBlock(myOptions);
}
2.3 总结

做混合应用,与各方的通信很重要,这也是混合开发的难点所在。一般的业务只需要单向通信就好了,就怕在业务中遇到很奇葩的事情,我就曾经遇到过,不能在RN中发送请求,只能把参数发给App侧,让app侧代RN发请求,然后把结果返回给RN。所以才有了上面的总结和输出,希望可以帮助到需要的人。

3. 参考资料

reactnative.cn/docs/native...

reactnative.cn/docs/native...

stackoverflow.com/questions/6...

www.jianshu.com/p/487714491...

juejin.cn/post/703517...

相关推荐
Jolyne_6 分钟前
前端常用的树处理方法总结
前端·算法·面试
wordbaby8 分钟前
后端的力量,前端的体验:React Router Server Action 的魔力
前端·react.js
Alang9 分钟前
Mac Mini M4 16G 内存本地大模型性能横评:9 款模型实测对比
前端·llm·aigc
林太白9 分钟前
Rust-连接数据库
前端·后端·rust
wordbaby17 分钟前
让数据“流动”起来:React Router Client Action 与组件的无缝协作
前端·react.js
宁静_致远22 分钟前
React 性能优化:深入理解 useMemo 、useCallback 和 memo
前端·react.js·面试
旺仔牛仔QQ糖23 分钟前
项目中TypeScript 编译器的工作流程
前端·typescript
coding丨24 分钟前
自制微信小程序popover菜单,气泡悬浮弹窗
前端·javascript·vue.js
anyup32 分钟前
10000+ 个点位轻松展示,使用 Leaflet 实现地图海量标记点聚类
前端·数据可视化·cursor
林太白34 分钟前
Rust认识安装
前端·后端·rust