Native 模块
您的 React Native 应用代码可能需要与 React Native 或现有库未提供的原生平台 API 交互。您可以使用 Turbo Native 模块 自行编写集成代码。本指南将向您展示如何编写一个。
基本步骤为:
- 使用最流行的 JavaScript 类型注解语言之一:Flow 或 TypeScript 定义一个带类型的 JavaScript 规范;
- 配置您的依赖管理系统以运行 Codegen,该工具将规范转换为原生语言接口;
- 使用您的规范编写应用程序代码;以及
- 使用生成的接口编写您的原生平台代码,将原生代码编写并挂载到 React Native 运行时环境中。
让我们通过构建一个示例 Turbo Native 模块来逐步完成每个步骤。本指南的其余部分假设您已经运行以下命令创建了您的应用:
npx @react-native-community/cli@latest init TurboModuleExample --version 0.83
原生持久存储
本指南将向您展示如何实现 Web Storage API:localStorage。此 API 对于可能在您的项目中编写应用代码的 React 开发者来说非常熟悉。
为了让它在移动端工作,我们需要使用 Android 和 iOS 的 API:
- Android: SharedPreferences,以及
- iOS: NSUserDefaults。
1. 声明带类型的规范
React Native 提供了一个名为 Codegen 的工具,它接收用 TypeScript 或 Flow 编写的规范,并为 Android 和 iOS 生成特定平台代码。规范声明了将在您的原生代码和 React Native JavaScript 运行时之间传递的方法和数据类型。Turbo Native 模块既是您的规范,也是您编写的原生代码,以及从规范生成的 Codegen 接口。
要创建规范文件:
- 在您的应用根文件夹中,创建一个名为
specs的新文件夹。 - 创建一个名为
NativeLocalStorage.ts的新文件。
您可以在 附录 文档中查看所有可在规范中使用的类型及生成的原生类型。
如果您想更改模块名称及相关的规范文件,请确保始终使用 “Native” 作为前缀(例如 NativeStorage 或 NativeUsersDefault)。
以下是 localStorage 规范的一个实现:
- TypeScript
- Flow
import type {TurboModule} from 'react-native';
import {TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): string | null;
removeItem(key: string): void;
clear(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NativeLocalStorage',
);
import type {TurboModule} from 'react-native';
import {TurboModule, TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
setItem(value: string, key: string): void;
getItem(key: string): ?string;
removeItem(key: string): void;
clear(): void;
}
2. 配置 Codegen 运行
规范被 React Native Codegen 工具使用,用以为我们生成特定平台的接口和样板代码。为此,Codegen 需要知道从哪里获取规范以及如何处理它。请更新您的 package.json,添加以下内容:
"start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "NativeLocalStorageSpec",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.nativelocalstorage"
}
},
"dependencies": {
Codegen 配置好后,我们需要准备原生代码,以挂载到生成的代码上。
- Android
- iOS
Codegen 是通过 generateCodegenArtifactsFromSchema 这个 Gradle 任务执行的:
cd android
./gradlew generateCodegenArtifactsFromSchema
BUILD SUCCESSFUL in 837ms
14 actionable tasks: 3 executed, 11 up-to-date
当您构建 Android 应用时,这个过程会自动执行。
Codegen 作为 CocoaPods 生成的项目自动添加的脚本阶段的一部分运行。
cd ios
bundle install
bundle exec pod install
输出类似如下:
...
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. 使用 Turbo Native 模块编写应用代码
使用 NativeLocalStorage,下面是一个修改过的 App.tsx,其中包含一些我们希望持久化的文本、一个输入框以及若干个按钮,用来更新这个值。
TurboModuleRegistry 支持两种检索 Turbo Native 模块的方式:
get<T>(name: string): T | null,如果 Turbo Native 模块不可用则返回null。getEnforcing<T>(name: string): T,如果 Turbo Native 模块不可用则抛出异常。此方法假定模块始终可用。
import React from 'react';
import {
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 [editingValue, setEditingValue] = React.useState<
string | null
>(null);
React.useEffect(() => {
const storedValue = NativeLocalStorage?.getItem('myKey');
setValue(storedValue ?? '');
}, []);
function saveValue() {
NativeLocalStorage?.setItem(editingValue ?? EMPTY, 'myKey');
setValue(editingValue);
}
function clearAll() {
NativeLocalStorage?.clear();
setValue('');
}
function deleteValue() {
NativeLocalStorage?.removeItem('myKey');
setValue('');
}
return (
<SafeAreaView style={{flex: 1}}>
<Text style={styles.text}>
当前存储的值是:{value ?? '无值'}
</Text>
<TextInput
placeholder="输入您想存储的文本"
style={styles.textInput}
onChangeText={setEditingValue}
/>
<Button title="保存" onPress={saveValue} />
<Button title="删除" onPress={deleteValue} />
<Button title="清空" 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;
4. 编写您的原生平台代码
准备就绪后,我们开始编写原生平台代码。这里分两部分进行:
本指南展示如何创建仅适用于新架构(New Architecture)的 Turbo Native 模块。如果您需要同时支持新架构和旧架构(Legacy Architecture),请参考我们的向后兼容指南。
- Android
- iOS
现在是时候编写一些 Android 平台代码,以确保 localStorage 在应用关闭后依然存在。
第一步是实现生成的 NativeLocalStorageSpec 接口:
- Java
- Kotlin
package com.nativelocalstorage;
import android.content.Context;
import android.content.SharedPreferences;
import com.nativelocalstorage.NativeLocalStorageSpec;
import com.facebook.react.bridge.ReactApplicationContext;
public class NativeLocalStorageModule extends NativeLocalStorageSpec {
public static final String NAME = "NativeLocalStorage";
public NativeLocalStorageModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return NAME;
}
@Override
public void setItem(String value, String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString(key, value);
editor.apply();
}
@Override
public String getItem(String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
String username = sharedPref.getString(key, null);
return username;
}
@Override
public void removeItem(String key) {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
sharedPref.edit().remove(key).apply();
}
@Override
public void clear() {
SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
sharedPref.edit().clear().apply();
}
}
package com.nativelocalstorage
import android.content.Context
import android.content.SharedPreferences
import com.nativelocalstorage.NativeLocalStorageSpec
import com.facebook.react.bridge.ReactApplicationContext
class NativeLocalStorageModule(reactContext: ReactApplicationContext) : NativeLocalStorageSpec(reactContext) {
override fun getName() = NAME
override fun setItem(value: String, key: String) {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.putString(key, value)
editor.apply()
}
override fun getItem(key: String): String? {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val username = sharedPref.getString(key, null)
return username.toString()
}
override fun removeItem(key: String) {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.remove(key)
editor.apply()
}
override fun clear() {
val sharedPref = getReactApplicationContext().getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val editor = sharedPref.edit()
editor.clear()
editor.apply()
}
companion object {
const val NAME = "NativeLocalStorage"
}
}
接下来我们需要创建 NativeLocalStoragePackage。它通过将我们的模块包装为基础原生包,提供一个对象以注册模块到 React Native 运行时:
- Java
- Kotlin
package com.nativelocalstorage;
import com.facebook.react.BaseReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import java.util.HashMap;
import java.util.Map;
public class NativeLocalStoragePackage extends BaseReactPackage {
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(NativeLocalStorageModule.NAME)) {
return new NativeLocalStorageModule(reactContext);
} else {
return null;
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put(NativeLocalStorageModule.NAME, new ReactModuleInfo(
NativeLocalStorageModule.NAME, // name
NativeLocalStorageModule.NAME, // className
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCXXModule
true // isTurboModule
));
return map;
}
};
}
}
package com.nativelocalstorage
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
class NativeLocalStoragePackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == NativeLocalStorageModule.NAME) {
NativeLocalStorageModule(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
NativeLocalStorageModule.NAME to ReactModuleInfo(
name = NativeLocalStorageModule.NAME,
className = NativeLocalStorageModule.NAME,
canOverrideExistingModule = false,
needsEagerInit = false,
isCxxModule = false,
isTurboModule = true
)
)
}
}
最后,我们需要告诉 React Native 我们的主应用如何找到这个 Package。这就是在 React Native 中“注册”该包。
在本例中,你需要将其添加到 getPackages 方法的返回列表中。
后续你还将学习如何将你的原生模块发布为 npm 包,我们的构建工具将自动为你完成自动链接。
- Java
- Kotlin
package com.inappmodule;
import android.app.Application;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactHost;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactHost;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.soloader.SoLoader;
import com.nativelocalstorage.NativeLocalStoragePackage;
import java.util.ArrayList;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost reactNativeHost = new DefaultReactNativeHost(this) {
@Override
public List<ReactPackage> getPackages() {
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 NativeLocalStoragePackage());
return packages;
}
@Override
public String getJSMainModuleName() {
return "index";
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
public boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
public boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
public ReactHost getReactHost() {
return DefaultReactHost.getDefaultReactHost(getApplicationContext(), reactNativeHost);
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// 如果你选择启用新架构,我们加载该应用的原生入口点。
DefaultNewArchitectureEntryPoint.load();
}
}
}
package com.inappmodule
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
import com.nativelocalstorage.NativeLocalStoragePackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(NativeLocalStoragePackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// 如果你选择启用新架构,我们加载该应用的原生入口点。
load()
}
}
}
你现在可以在模拟器上构建并运行你的代码了:
- npm
- Yarn
npm run android
yarn run android
现在是时候编写一些 iOS 平台代码,确保 localStorage 在应用关闭后依然能够保留。
准备你的 Xcode 项目
我们需要使用 Xcode 准备你的 iOS 项目。完成下面6个步骤后,你将拥有实现了生成的 NativeLocalStorageSpec 接口的 RCTNativeLocalStorage。
- 打开 CocoaPods 生成的 Xcode 工作区:
cd ios
open TurboModuleExample.xcworkspace
- 右键点击 app,选择
New Group,将新建的组命名为NativeLocalStorage。
- 在
NativeLocalStorage组内,创建New→File from Template。
- 选择
Cocoa Touch Class。
- 将 Cocoa Touch 类命名为
RCTNativeLocalStorage,语言选择Objective-C。
- 将
RCTNativeLocalStorage.m重命名为RCTNativeLocalStorage.mm,使其成为 Objective-C++ 文件。
使用 NSUserDefaults 实现 localStorage
首先更新 RCTNativeLocalStorage.h:
// RCTNativeLocalStorage.h
// TurboModuleExample
#import <Foundation/Foundation.h>
#import <NativeLocalStorageSpec/NativeLocalStorageSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeLocalStorage : NSObject
@interface RCTNativeLocalStorage : NSObject <NativeLocalStorageSpec>
@end
然后更新实现,使用带有自定义 suite 名称 的 NSUserDefaults。
// RCTNativeLocalStorage.m
// TurboModuleExample
#import "RCTNativeLocalStorage.h"
static NSString *const RCTNativeLocalStorageKey = @"local-storage";
@interface RCTNativeLocalStorage()
@property (strong, nonatomic) NSUserDefaults *localStorage;
@end
@implementation RCTNativeLocalStorage
- (id) init {
if (self = [super init]) {
_localStorage = [[NSUserDefaults alloc] initWithSuiteName:RCTNativeLocalStorageKey];
}
return self;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeLocalStorageSpecJSI>(params);
}
- (NSString * _Nullable)getItem:(NSString *)key {
return [self.localStorage stringForKey:key];
}
- (void)setItem:(NSString *)value
key:(NSString *)key {
[self.localStorage setObject:value forKey:key];
}
- (void)removeItem:(NSString *)key {
[self.localStorage removeObjectForKey:key];
}
- (void)clear {
NSDictionary *keys = [self.localStorage dictionaryRepresentation];
for (NSString *key in keys) {
[self removeItem:key];
}
}
+ (NSString *)moduleName
{
return @"NativeLocalStorage";
}
@end
重要事项:
- 你可以使用 Xcode 跳转到 Codegen 中的
@protocol NativeLocalStorageSpec。也可以利用 Xcode 为你生成存根代码。
在你的应用中注册 Native 模块
最后一步,是更新 package.json,告诉 React Native JS 端的 Native 模块规范与 native 代码中具体实现这些规范的桥接。
修改 package.json 内容如下:
"start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "NativeLocalStorageSpec",
"type": "modules",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.sampleapp.specs"
},
"ios": {
"modulesProvider": {
"NativeLocalStorage": "RCTNativeLocalStorage"
}
}
},
"dependencies": {
此时需要重新安装 pods,以确保 codegen 重新运行生成新文件:
# 从 ios 文件夹中执行
bundle exec pod install
open SampleApp.xcworkspace
现在从 Xcode 构建你的应用应该能成功。
在模拟器上构建并运行你的代码
- npm
- Yarn
npm run ios
yarn run ios