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()
返回该实例。由于静态变量在类加载时就被创建,因此无需考虑多线程环境下的线程安全问题。
使用静态变量和静态方法实现的饿汉式单例模式具有简单、线程安全的特点。但请注意,由于它在类加载时就创建实例,因此可能会浪费一些内存空间,尤其在实例较为庞大或创建过程较为复杂时。
这样实现的单例模式可以保证在多线程环境下安全地创建单例对象。
接下来我们还需要创建两个回调函数的接口定义:OnSafetyClickListener
和 OnSafetyResultCallback
。这些接口定义了一些回调方法,用于处理事件和获取结果。同时供了对回调函数实例进行获取和设置的方法,如 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. 参考资料
stackoverflow.com/questions/6...