程式碼在這裡
很多時候我們需要從原生髮送事件給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<RCTBridgeModule>
@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<NSString *> *)supportedEvents {
return @[@"FillingHole"];
}
@end
這裡省略了模組標頭檔案。
1: 在標頭檔案裡可以看到原生模組繼承了RCTEventEmitter
。
2: 所以在實現的時候需要實現這個類的方法supportedEvents
。就是把我們要從原生髮送的事件的名字加進去。如:
- (NSArray<NSString *> *)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模式來檢視從原生接收的事件了。
從原生髮出事件用到的地方不一定多,但是肯定是一個非常有用的功能點。希望這邊文章可以幫到你。