在原生模块中触发事件
¥Emitting Events in Native Modules
在某些情况下,你可能需要一个原生模块,用于监听平台层的某些事件,然后将它们触发到 JavaScript 层,以便你的应用能够对这些原生事件做出响应。在其他情况下,你可能需要执行长时间运行的操作,这些操作可以触发事件,以便在事件发生时更新 UI。
¥In some circustamces, you may want to have a Native Module that listen to some events in the platform layer and then emit them to the JavaScript layer, to let you application react to such native events. In other cases, you might have long running operations that can emits events so that the UI can be updated when those happen.
这两个都是从原生模块触发事件的良好用例。在本指南中,你将学习如何做到这一点。
¥Both are good use cases for emitting events from a Native Modules. In this guide, you'll learn how to do that.
当新键添加到存储时触发事件
¥Emitting an Event when a new key added to the storage
在本例中,你将学习如何在新键添加到存储时触发事件。更改键的值不会触发事件,但添加新键会触发事件。
¥In this example, you will learn how to emit an event when a new key is added to the storage. Changing the value of the key will not emit the event, but adding a new key will.
本指南基于 原生模块 指南。在深入研究本指南之前,请务必熟悉该指南,并可能实现指南中的示例。
¥This guide starts from the Native Module guide. Make sure to be familiar with that guide before diving into this one, potentially implementing the example in the guide.
步骤 1:更新 NativeLocalStorage 的规范
¥Step 1: Update the Specs of NativeLocalStorage
第一步是更新 NativeLocalStorage
规范的规范,让 React Native 知道该模块可以触发事件。
¥The first step would be to update the specs of the NativeLocalStorage
specs to let React Native aware that the module can emit events.
- TypeScript
- Flow
打开 NativeLocalStorage.ts
文件并进行如下更新:
¥Open the NativeLocalStorage.ts
file and update it as it follows:
+import type {TurboModule, CodegenTypes} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
+export type KeyValuePair = {
+ key: string,
+ value: string,
+}
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): string | null;
removeItem(key: string): void;
clear(): void;
+ readonly onKeyAdded: CodegenTypes.EventEmitter<KeyValuePair>;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeLocalStorage',
);
打开 NativeLocalStorage.js
文件并进行如下更新:
¥Open the NativeLocalStorage.js
file and update it as it follows:
// @flow
+import type {TurboModule, CodegenTypes} from 'react-native';
import {TurboModule, TurboModuleRegistry} from 'react-native';
+export type KeyValuePair = {
+ key: string,
+ value: string,
+}
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): ?string;
removeItem(key: string): void;
clear(): void;
+ onKeyAdded: CodegenTypes.EventEmitter<KeyValuePair>
}
export default (TurboModuleRegistry.get<Spec>(
'NativeLocalStorage'
): ?Spec);
使用 import type
语句,您将从 react-native
导入 CodegenTypes
,其中包含 EventEmitter
类型。这允许您使用 CodegenTypes.EventEmitter<KeyValuePair>
定义 onKeyAdded
属性,指定事件将发出 KeyValuePair
类型的有效负载。
¥With the import type
statement, you are importing the CodegenTypes
from react-native
, which includes the EventEmitter
type. This allows you to define the onKeyAdded
property using CodegenTypes.EventEmitter<KeyValuePair>
, specifying that the event will emit a payload of type KeyValuePair
.
当事件发出时,你期望它接收一个 string
类型的参数。
¥When the event is emitted, you expect for it to receive a parameter of type string
.
步骤 2:生成 Codegen
¥Step 2: Generate Codegen
鉴于你已更新原生模块的规范,现在必须重新运行 Codegen 以生成原生代码中的工件。
¥Given that you have updated the specs for your Native Module, you now have to rerun Codegen to generate the artifacts in the native code.
这与原生模块指南中介绍的过程相同。
¥This is the same process presented in the Native Modules guide.
- Android
- iOS
Codegen 通过 generateCodegenArtifactsFromSchema
Gradle 任务执行:
¥Codegen is executed through the generateCodegenArtifactsFromSchema
Gradle task:
cd android
./gradlew generateCodegenArtifactsFromSchema
BUILD SUCCESSFUL in 837ms
14 actionable tasks: 3 executed, 11 up-to-date
这将在你构建 Android 应用时自动运行。
¥This is automatically run when you build your Android application.
Codegen 作为脚本阶段的一部分运行,会自动添加到 CocoaPods 生成的项目中。
¥Codegen is run as part of the script phases that's automatically added to the project generated by CocoaPods.
cd ios
bundle install
bundle exec pod install
输出将如下所示:
¥The output will look like this:
...
Framework build type is static library
[Codegen] Adding script_phases to ReactCodegen.
[Codegen] Generating ./build/generated/ios/ReactCodegen.podspec.json
[Codegen] Analyzing /Users/me/src/TurboModuleExample/package.json
[Codegen] Searching for Codegen-enabled libraries in the app.
[Codegen] Found TurboModuleExample
[Codegen] Searching for Codegen-enabled libraries in the project dependencies.
[Codegen] Found react-native
...
步骤 3:更新应用代码
¥Step 3: Update the App code
现在,是时候更新应用的代码以处理新事件了。
¥Now, it's time to update the code of the App to handle the new event.
打开 App.tsx
文件并进行如下修改:
¥Open the App.tsx
file and modify it as it follows:
import React from 'react';
import {
+ Alert,
+ EventSubscription,
SafeAreaView,
StyleSheet,
Text,
TextInput,
Button,
} from 'react-native';
import NativeLocalStorage from './specs/NativeLocalStorage';
const EMPTY = '<empty>';
function App(): React.JSX.Element {
const [value, setValue] = React.useState<string | null>(null);
+ const [key, setKey] = React.useState<string | null>(null);
+ const listenerSubscription = React.useRef<null | EventSubscription>(null);
+ React.useEffect(() => {
+ listenerSubscription.current = NativeLocalStorage?.onKeyAdded((pair) => Alert.alert(`New key added: ${pair.key} with value: ${pair.value}`));
+ return () => {
+ listenerSubscription.current?.remove();
+ listenerSubscription.current = null;
+ }
+ }, [])
const [editingValue, setEditingValue] = React.useState<
string | null
>(null);
- React.useEffect(() => {
- const storedValue = NativeLocalStorage?.getItem('myKey');
- setValue(storedValue ?? '');
- }, []);
function saveValue() {
+ if (key == null) {
+ Alert.alert('Please enter a key');
+ return;
+ }
NativeLocalStorage?.setItem(editingValue ?? EMPTY, key);
setValue(editingValue);
}
function clearAll() {
NativeLocalStorage?.clear();
setValue('');
}
function deleteValue() {
+ if (key == null) {
+ Alert.alert('Please enter a key');
+ return;
+ }
NativeLocalStorage?.removeItem(key);
setValue('');
}
+ function retrieveValue() {
+ if (key == null) {
+ Alert.alert('Please enter a key');
+ return;
+ }
+ const val = NativeLocalStorage?.getItem(key);
+ setValue(val);
+ }
return (
<SafeAreaView style={{flex: 1}}>
<Text style={styles.text}>
Current stored value is: {value ?? 'No Value'}
</Text>
+ <Text>Key:</Text>
+ <TextInput
+ placeholder="Enter the key you want to store"
+ style={styles.textInput}
+ onChangeText={setKey}
+ />
+ <Text>Value:</Text>
<TextInput
placeholder="Enter the text you want to store"
style={styles.textInput}
onChangeText={setEditingValue}
/>
<Button title="Save" onPress={saveValue} />
+ <Button title="Retrieve" onPress={retrieveValue} />
<Button title="Delete" onPress={deleteValue} />
<Button title="Clear" onPress={clearAll} />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
text: {
margin: 10,
fontSize: 20,
},
textInput: {
margin: 10,
height: 40,
borderColor: 'black',
borderWidth: 1,
paddingLeft: 5,
paddingRight: 5,
borderRadius: 5,
},
});
export default App;
有一些相关的更改需要注意:
¥There are a few relevant changes to look at:
-
你需要从
react-native
导入EventSubscription
类型来处理EventSubscription
。¥You need to import the
EventSubscription
type fromreact-native
to handle theEventSubscription
-
你需要使用
useRef
来跟踪EventSubscription
引用。¥You need to use a
useRef
to keep track of theEventSubscription
reference -
你可以使用
useEffect
钩子注册监听器。onKeyAdded
函数接受一个回调函数,该回调函数以KeyValuePair
类型的对象为参数。¥You register the listener using an
useEffect
hook. TheonKeyAdded
function takes a callback with an object of typeKeyValuePair
as a function parameter. -
每次从 Native 向 JS 触发事件时,都会执行添加到
onKeyAdded
的回调。¥The callback added to
onKeyAdded
is executed every time the event is emitted from Native to JS. -
在
useEffect
清理函数中,你remove
事件订阅,并将 ref 设置为null
。¥In the
useEffect
cleanup function, youremove
the event subscription and you set the ref tonull
.
其余更改是常规的 React 更改,用于改进应用以支持此新功能。
¥The rest of the changes are regular React changes to improve the App for this new feature.
步骤 4:编写原生代码
¥Step 4: Write your Native Code
一切准备就绪后,让我们开始编写原生平台代码。
¥With everything prepared, let's start writing native platform code.
- Android
- iOS
假设你遵循了 原生模块指南 中描述的 iOS 指南,那么剩下要做的就是在你的应用中插入触发事件的代码。
¥Assuming you followed the guide for iOS described in the Native Modules guide, what's left to do is to plug the code that emit the events in your app.
为此,你必须:
¥To do so, you have to:
-
打开
NativeLocalStorage.kt
文件¥Open the
NativeLocalStorage.kt
file -
进行如下修改:
¥Modify it as it follows:
package com.nativelocalstorage
import android.content.Context
import android.content.SharedPreferences
import com.nativelocalstorage.NativeLocalStorageSpec
+import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.WritableMap
class NativeLocalStorageModule(reactContext: ReactApplicationContext) : NativeLocalStorageSpec(reactContext) {
override fun getName() = NAME
override fun setItem(value: String, key: String) {
+ var shouldEmit = false
+ if (getItem(key) != null) {
+ shouldEmit = true
+ }
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.putString(key, value)
editor.apply()
+ if (shouldEmit == true) {
+ val eventData = Arguments.createMap().apply {
+ putString("key", key)
+ putString("value", value)
+ }
+ emitOnKeyAdded(eventData)
+ }
}
override fun getItem(key: String): String? {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val username = sharedPref.getString(key, null)
return username.toString()
}
首先,你需要导入一些类型,用于创建需要从 Native 发送到 JS 的 eventData。导入的内容如下:
¥First, you need to import a couple of types that you need to use to create the eventData that needs to be sent from Native to JS. These imports are:
-
import com.facebook.react.bridge.Arguments
-
import com.facebook.react.bridge.WritableMap
其次,你需要实现将事件实际发送到 JS 的逻辑。对于复杂类型,例如规范中定义的 KeyValuePair
,Codegen 将生成一个以 ReadableMap
作为参数的函数。你可以使用 Arguments.createMap()
工厂方法创建 ReadableMap
,并使用 apply
函数填充映射。你有责任确保映射中使用的键与 JS 规范类型中定义的属性相同。
¥Secondly, you need to implement the logic that actually emits the event to JS. In case of complex types, like the KeyValuePair
defined in the specs, Codegen will generate a function that expects a ReadableMap
as a parameter. You can create the ReadableMap
by using the Arguments.createMap()
factory method, and use the apply
function to populate the map. It's your responsibility to make sure that the the keys you are using in the map are the same properties that are defined in the spec type in JS.
假设你遵循了 原生模块指南 中描述的 iOS 指南,那么剩下要做的就是在你的应用中插入触发事件的代码。
¥Assuming you followed the guide for iOS described in the Native Modules guide, what's left to do is to plug the code that emit the events in your app.
为此,你必须:
¥To do so, you have to:
-
打开
RCTNativeLocalStorage.h
文件。¥Open the
RCTNativeLocalStorage.h
file. -
将基类从
NSObject
更改为NativeLocalStorageSpecBase
¥Change the base class from
NSObject
toNativeLocalStorageSpecBase
#import <Foundation/Foundation.h>
#import <NativeLocalStorageSpec/NativeLocalStorageSpec.h>
NS_ASSUME_NONNULL_BEGIN
-@interface RCTNativeLocalStorage : NSObject <NativeLocalStorageSpec>
+@interface RCTNativeLocalStorage : NativeLocalStorageSpecBase <NativeLocalStorageSpec>
@end
NS_ASSUME_NONNULL_END
-
打开
RCTNativeLocalStorage.mm
文件。¥Open the
RCTNativeLocalStorage.mm
file. -
修改它以便在需要时触发事件,例如:
¥Modify it to emit the events when needed, for example:
- (void)setItem:(NSString *)value key:(NSString *)key {
+ BOOL shouldEmitEvent = NO;
+ if (![self getItem:key]) {
+ shouldEmitEvent = YES;
+ }
[self.localStorage setObject:value forKey:key];
+ if (shouldEmitEvent) {
+ [self emitOnKeyAdded:@{@"key": key, @"value": value}];
+ }
}
NativeLocalStorageSpecBase
是一个基类,它提供了 emitOnKeyAdded
方法及其基本实现和样板代码。有了此类,你无需处理将事件发送到 JS 所需的所有 Objective-C 和 JSI 之间的转换。
¥The NativeLocalStorageSpecBase
is a base class that provides the emitOnKeyAdded
method and its basic implementation and boilerplate. Thanks to this class, you don't have to handle all the conversion between Objective-C and JSI that is required to send the event to JS.
对于复杂类型,例如规范中定义的 KeyValuePair
,Codegen 将生成一个可在原生端填充的通用字典。你有责任确保字典中使用的键与 JS 规范类型中定义的属性相同。
¥In case of complex types, like the KeyValuePair
defined in the specs, Codegen will generate a generic dictionary that you can populate on the native side. It's your responsibility to make sure that the the keys you are using in the dictionary are the same properties that are defined in the spec type in JS.
步骤 5:运行你的应用
¥Step 5: Run Your App
如果你现在尝试运行你的应用,你应该会看到此行为。
¥If you now try to run your app, you should see this behavior.