代码在这里
很多时候我们需要从原生发送事件给JS。比如在官方文档提到的一个日历事件。你定好了一个会议,或者一个活动,之后再指定的日期发生。或者关闭了贡献单车,蓝牙收到关锁成功的信号。又或者地理围栏这样的APP,在你进入/离开一个地理围栏的时候,都需要从原生发送事件给JS。
首先是一个简单的例子
调用一个原生方法设置一个延时触发的原生时间,类似于调用原生的setTimeout
。在到时间之后一个事件会从原生发送到JS。
首先UI上会有一个可以输入时间的文本框,在用户输入了时间并点击了OK按钮之后。App就会调用原生方法执行原生的setTimeout
方法。
App调用的原生方法是一个在前端的promise方法。所以,这个方法可以用async-await
调用。
在JS的部分会注册一个事件的监听器,一但收到原生事件就会执行JS代码。在本例中为了简单只是输出了一条log。
既然是从原生接收和发送事件,那么一个原生的模块是必不可少的。还不是很了解这部分的同学可以移步到这是iOS的,这是Android的
一点需要更新的是,现在官方推荐在实现Android原生模块的时候使用`ReactContextBaseJavaModule`。主要是出于类型安全的考虑。
在iOS实现一个原生模块
// header file
@interface FillingHoleModule: RCTEventEmitter
@end
// implementation
#import "FillingHoleModule.h"
@implementation FillingHoleModule
RCT_EXPORT_MODULE(FillingHoleModule)
RCT_EXPORT_METHOD(sendEventInSeconds: (NSUInteger) seconds resolver:(RCTPromiseResolveBlock) resolve
rejecter: (RCTPromiseRejectBlock) reject) {
@try {
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * seconds);
dispatch_after(delay, dispatch_get_main_queue(), ^(void) {
[self sendEventWithName:@"FillingHole" body:@{@"filling": @"hole", @"with": @"RN"}];
});
NSLog(@"Resolved");
resolve(@"done");
} @catch (NSException *exception) {
NSLog(@"Rejected %@", exception);
reject(@"Failed", @"Cannot setup timeout", nil);
}
}
- (NSArray *)supportedEvents {
return @[@"FillingHole"];
}
@end
这里省略了模块头文件。
1: 在头文件里可以看到原生模块继承了RCTEventEmitter
。
2: 所以在实现的时候需要实现这个类的方法supportedEvents
。就是把我们要从原生发送的事件的名字加进去。如:
- (NSArray *)supportedEvents {
return @[@"FillingHole"];
}
3: 在方法sendEventInSeconds
里的try-catch可以辅助调用resolve
和reject
正好实现了JS部分的promise
4:这部分在objc里相当于setTimeout
:
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * seconds);
dispatch_after(delay, dispatch_get_main_queue(), ^(void) {
[self sendEventWithName:@"FillingHole" body:@{@"filling": @"hole", @"with": @"RN"}];
});
代码[self sendEventWithName:@"FillingHole" body:@{@"filling": @"hole", @"with": @"RN"}];
完成了从原生到JS发送事件的功能。
在Android的原生模块
public class FillingEventHole extends ReactContextBaseJavaModule {
FillingEventHole(ReactApplicationContext context) {
super(context);
}
@NonNull
@Override
public String getName() {
return "FillingHoleModule";
}
@ReactMethod
public void sendEventInSeconds(long seconds, Promise promise) {
Log.d("FillEventHole", "Event from native comes in" + seconds);
try {
new android.os.Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
WritableMap params = Arguments.createMap();
params.putString("filling", "hole");
params.putString("with", "RN");
FillingEventHole.this.sendEvent("FillingHole", params);
}
}, seconds * 1000);
promise.resolve("Done");
} catch (Exception e) {
promise.reject(e);
}
}
private void sendEvent(String eventName, @Nullable WritableMap params) {
this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
}
}
注意:具体原生的部分步骤比iOS的多一些。这里就都省略了,如果需要参考上文提到的Android原生模块部分。
1:sendEventInSeconds
用于接收JS传递的时间信息,并开始Android这里的setTimeout
。
2:try-catch部分和iOS的一样,配合处理resolve和reject。
3:Android的setTimeout
:
new android.os.Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
WritableMap params = Arguments.createMap();
params.putString("filling", "hole");
params.putString("with", "RN");
FillingEventHole.this.sendEvent("FillingHole", params);
}
}, seconds * 1000);
4: 方法FillingEventHole.this.sendEvent("FillingHole", params);
调用在原生模块里实现的setEvent
方法来发送事件。
5:事件发送方法:
private void sendEvent(String eventName, @Nullable WritableMap params) {
this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
}
前端实现
在开始前端的部分之前,先说几句废话。
Android和iOS实现的原生模块必须是同名的。模块名要同名,原生方法也要同名。在前端调用原生方法的时候需要保证类型是对应的。比如上例的原生方法public void sendEventInSeconds(long seconds, Promise promise)
时间参数是数字类型的,那么在JS调用的时候就必须是数字类型的,否则就会出错。或者,你可以提供定制的类型转化方法。
在前端只需要:
import {
//...
NativeEventEmitter,
NativeModules,
EmitterSubscription,
} from 'react-native';
const {FillingHoleModule} = NativeModules; // 1
const eventEmitter = new NativeEventEmitter(FillingHoleModule); // 2
const App = () => {
useEffect(() => {
// 3
const eventListener = eventEmitter.addListener('FillingHole', event => {
console.log('You received an event', JSON.stringify(event));
});
listenersRef.current = eventListener;
return () => {
// 4
listenersRef.current?.remove();
};
});
// 5
const handlePress = async () => {
console.log('>', text);
try {
await FillingHoleModule.sendEventInSeconds(+text);
} catch (e) {
console.error('Create event failed, ', e);
}
};
return (
//...
);
}
1:从NativeModules
拿到定义的原生模块
2:使用定义好的原生模块初始化NativeEventEmiotter
3:添加原生事件的listener
4:在最后销毁listener
5:在用户点击按钮的时候调用原生模块暴露的方法出发原生事件的setTimeout
方法。
3>和4>都是react hooks的内容,不熟的同学可以参考官网。
这里还有一个实用的小技巧,使用useRef
保存了组件事件的listener。
最后
准备工作都做好了之后就可以跑起来。摇一摇手机开启debug模式来查看从原生接收的事件了。
从原生发出事件用到的地方不一定多,但是肯定是一个非常有用的功能点。希望这边文章可以帮到你。