原生模块
你的 React Native 应用代码可能需要与 React Native 或现有库未提供的原生平台 API 进行交互。你可以使用 Turbo 原生模块 自己编写集成代码。本指南将向你展示如何编写一个。
基本步骤如下:
- 定义一个类型化的 JavaScript 规范,使用最流行的 JavaScript 类型注解语言之一:Flow 或 TypeScript;
- 配置你的依赖管理工具以运行 Codegen,将规范转换为原生语言接口;
- 编写你的应用代码,使用你的规范;
- 编写你的原生平台代码,使用生成的接口,将你的原生代码连接到 React Native 运行时环境。
让我们通过构建一个示例 Turbo 原生模块来逐步完成这些步骤。本指南的其余部分假设你已经使用以下命令创建了应用:
npx @react-native-community/cli@latest init TurboModuleExample --version 0.76.0
本地持久存储
本指南将向你展示如何编写一个 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 原生模块既是你的规范,也是你编写的原生代码,以及从你的规范生成的 Codegen 接口。
要创建一个规范文件:
- 在你的应用的根文件夹中创建一个名为 specs的新文件夹。
- 创建一个名为 NativeLocalStorage.ts的新文件。
你可以在 附录 文档中看到可以在规范中使用的类型以及生成的原生类型。
以下是一个 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',
) as Spec;
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 原生模块编写应用代码
使用 NativeLocalStorage,以下是一个修改后的 App.tsx,它包含一些需要持久化的文本、一个输入字段和一些按钮来更新此值。
TurboModuleRegistry 支持两种检索 Turbo 原生模块的模式:
- get<T>(name: string): T | null如果 Turbo 原生模块不可用,将返回- null。
- getEnforcing<T>(name: string): T如果 Turbo 原生模块不可用,将抛出异常。假设模块总是可用。
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(editingValue ?? EMPTY);
    setValue('');
  }
  return (
    <SafeAreaView style={{flex: 1}}>
      <Text style={styles.text}>
        Current stored value is: {value ?? 'No Value'}
      </Text>
      <TextInput
        placeholder="Enter the text you want to store"
        style={styles.textInput}
        onChangeText={setEditingValue}
      />
      <Button title="Save" onPress={saveValue} />
      <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;
4. 编写你的原生平台代码
准备好所有内容后,我们将开始编写原生平台代码。我们分两部分进行:
本指南展示了如何创建一个仅适用于新架构的 Turbo 原生模块。如果你需要同时支持新架构和旧架构,请参考我们的 向后兼容指南。
- 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 {
  private 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();
  }
}
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 运行时中注册我们的模块,通过将其包装为 Turbo Native Package 来实现:
- Java
- Kotlin
package com.nativelocalstorage;
import com.facebook.react.TurboReactPackage;
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 TurboReactPackage {
  @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> get() {
        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.TurboReactPackage
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 : TurboReactPackage() {
  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。我们称这个过程为 "注册" 包。
在这种情况下,你需要将其添加到 getPackages 方法中。
稍后你将学习如何将你的 Turbo Native 模块作为 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。
- 打开 CocoPods 生成的 Xcode workspace 文件:
cd ios
open TurboModuleExample.xcworkspace
 
- 右键点击应用,选择 New Group,将新组命名为NativeLocalStorage。
 
- 在 NativeLocalStorage组中,创建New→File from Template。
 
- 使用 Cocoa Touch Class模板。
 
- 将类命名为 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 name 的 NSUserDefaults。
//  RCTNativeLocalStorage.m
//  TurboModuleExample
#import "RCTNativeLocalStorage.h"
static NSString *const RCTNativeLocalStorageKey = @"local-storage";
@interface RCTNativeLocalStorage()
@property (strong, nonatomic) NSUserDefaults *localStorage;
@end
@implementation RCTNativeLocalStorage
RCT_EXPORT_MODULE(NativeLocalStorage)
- (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];
  }
}
@end
重要注意事项:
- RCT_EXPORT_MODULE导出并注册模块,使用我们在 JavaScript 环境中访问它的标识符:- NativeLocalStorage。更多详情请参阅 docs。
- 你可以使用 Xcode 跳转到 Codegen @protocol NativeLocalStorageSpec。你也可以使用 Xcode 为你生成 stub。
在模拟器上构建并运行你的代码
- npm
- Yarn
npm run ios
yarn run ios