React Native填坑之旅 -- 從Native發事件給JS

小紅星閃啊閃發表於2021-10-05

程式碼在這裡

很多時候我們需要從原生髮送事件給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可以輔助呼叫resolvereject正好實現了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原生模組部分。

1sendEventInSeconds用於接收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模式來檢視從原生接收的事件了。

從原生髮出事件用到的地方不一定多,但是肯定是一個非常有用的功能點。希望這邊文章可以幫到你。

相關文章