原生模块
需要进行架构审查:本文档旨在支持针对 React Native 的“旧”或“遗留”架构的开发。它可能适用也可能不适用于新架构的开发,需要审查并可能需要更新。有关 React Native Windows 中 React Native 架构的信息,请参阅新旧架构。
有关 Windows 上原生开发的最新信息,请参阅原生平台:概述。
本文档和底层平台代码仍在开发中。 示例(C# 和 C++/WinRT):
有时,应用程序需要访问 React Native 尚未提供相应模块的平台 API。您可能希望重用一些现有的 .NET 代码,而无需在 JavaScript 中重新实现它,或者为图像处理、数据库或任何数量的高级扩展编写一些高性能、多线程的代码。
React Native 的设计使得您可以编写真正的原生代码并充分利用平台的强大功能。这是一项更高级的功能,我们不希望它成为常规开发过程的一部分,但它的存在至关重要。如果 React Native 不支持您需要的原生功能,您应该能够自己构建它。
注意:如果您正在构建一个包含 UI 组件的微件,请查看原生 UI 组件指南。
概述
原生模块包含(或包装)原生代码,然后可以将其暴露给 JS。要在 React Native for Windows 中实现这一点,您必须在较高层面执行以下操作:
- 编写您的原生模块,它是调用您的原生代码的类。
- 向该类添加自定义属性。这些属性允许您定义可以从 JavaScript 引用的方法、属性、常量和事件。
- 注册您的原生模块。请注意,在您的应用程序中定义的原生模块会自动注册。
- 将包添加到您的 React Native 应用程序。
- 在您的 JavaScript 代码中使用您的原生模块。
React Native for Windows 支持使用 C# 和 C++ 编写原生模块。下面提供了两者的示例。有关选择哪种语言的更多信息,请参阅选择 C++ 或 C# 作为原生代码说明。
注意:如果您无法使用基于反射的注释方法,则可以直接使用 ABI 定义原生模块。这在不使用属性编写原生模块文档中进行了概述。
初始设置
遵循原生模块设置指南以创建 Visual Studio 基础设施,用于为 React Native Windows 编写您自己的独立原生模块。
设置好开发环境和项目结构后,您就可以开始编写代码了。
打开 windows 文件夹中的 Visual Studio 解决方案,并将新文件直接添加到应用程序项目中。
原生模块示例
1. 在 package.json 中配置 windows codegen
将以下对象添加到您包的 package.json 文件中
"codegenConfig": {
"name": "NameOfYourApp",
"type": "modules",
"jsSrcsDir": "src",
"windows": {
"namespace": "YourAppCodegenNamespace",
"cppStringType": "std::string", /* optional */
"separateDataTypes": false, /* optional */
"outputDirectory": "codegen" /* optional */
}
},
name、type、jsSrcsDir 的值与 react-native 共享,如此处所述。windows 对象将导致 windows-codegen 任务为项目中定义的任何 TurboModule 规范文件生成 windows 特定的 codegen。
windows.namespace: 必需。此属性控制这些生成文件使用的 C++ 命名空间。windows.cppStringType: 可选。此属性控制生成文件中使用的 C++ 字符串类型。默认值为std::string。您可以选择std::string或std::wstring。windows.separateDataTypes: 可选。- 默认值为
false,在这种情况下,NativeAbcSpec.g.h是从NativeAbc.ts或其他可识别的文件扩展名生成的。 - 当它为
true时- 将生成
NativeAbcDataTypes.h和NativeAbcSpec.g.h。NativeAbcDataTypes.h包含在NativeAbc.ts中定义的所有自定义类型。 NativeAbcSpec.g.h包含所有剩余代码。NativeAbcSpec.g.h不包含NativeAbcDataTypes.h。- 如果没有自定义类型,即使
windows.separateDataTypes为true,也不会生成NativeAbcDataTypes.h。
- 将生成
- 默认值为
windows.outputDirectory: 可选。此属性控制生成文件的位置。默认值为codegen,它将文件放在工作目录下的codegen文件夹中。如果该文件夹目前不存在,它将被创建。
2. 创建 JavaScript 规范
模块应该在 JavaScript 的类型化方言(TypeScript 或 Flow)中定义。Codegen 将使用这些规范来验证您的原生代码提供的接口。
包含此规范的文件必须满足两个要求:
- 使用 Flow 时,文件**必须**命名为
Native<MODULE_NAME>,并带有.js或.jsx扩展名;使用 TypeScript 时,则带有.ts或.tsx扩展名。 - 文件**必须**导出
TurboModuleRegistrySpec对象。
示例规范文件 NativeFancyMath.ts
import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getConstants() : {
E: number,
Pi: number,
};
add(a: number, b: number, callback: (value: number) => void) : void;
}
export default TurboModuleRegistry.get<Spec>(
'FancyMath'
) as Spec | null;
请注意,即使此文件使用
TurboModuleRegistry,原生模块仍然可以使用此 JavaScript。该代码是前瞻性的,将支持原生模块或 TurboModules。
属性
| 属性 | 使用 |
|---|---|
ReactModule | 指定该类是一个原生模块。 |
ReactMethod | 指定一个异步方法。 |
ReactSyncMethod | 指定一个同步方法。 |
ReactConstant | 指定表示常量的字段或属性。 |
ReactConstantProvider | 指定提供一组常量的方法。 |
ReactEvent | 指定表示事件的字段或属性。 |
ReactStruct | 指定可在原生方法中使用的 struct。 |
ReactInit | 指定一个类初始化模块。 |
ReactFunction | 指定您希望向原生代码公开的 JavaScript 函数。 |
3. 编写您的原生模块
以下是一个用 C# 编写的名为 FancyMath 的原生模块示例。它是一个简单的类,定义了两个数值常量和一个 'add' 方法。
FancyMath.cs:
using System;
using Microsoft.ReactNative.Managed;
namespace NativeModuleSample
{
[ReactModule]
class FancyMath
{
[ReactConstant]
public double E = Math.E;
[ReactConstant("Pi")]
public double PI = Math.PI;
[ReactMethod("add")]
public double Add(double a, double b)
{
double result = a + b;
AddEvent(result);
return result;
}
[ReactEvent]
public ReactEvent<double> AddEvent { get; set; }
}
}
首先,您会看到我们正在使用 Microsoft.ReactNative.Managed 共享库,它提供了最简单(也是推荐的)的原生模块编写体验。Microsoft.ReactNative.Managed 提供了在运行时发现原生模块注释以构建绑定机制。
[ReactModule] 属性表示该类是一个 React Native 原生模块。它有一个可选参数用于 JavaScript 可见的模块名称,以及一个可选参数用于已注册事件发射器的名称。默认情况下,JavaScript 可见的名称与类名相同,默认事件发射器是 RCTDeviceEventEmitter。
您可以像这样覆盖 JavaScript 模块名称:[ReactModule("math")]。
您可以像这样指定不同的事件发射器:[ReactModule(EventEmitter = "mathEmitter")]。
注意:使用默认事件发射器
RCTDeviceEventEmitter时,所有原生事件名称必须在**所有原生模块中(甚至包括 RN 内置的模块)全局唯一**。但是,指定您自己的事件发射器意味着您还需要创建并注册它。此过程在原生模块和 React Native Windows(高级主题)文档中进行了概述。
[ReactConstant] 属性是您定义常量的方式。这里 FancyMath 定义了两个常量:E 和 Pi。访问这些常量时,您应该使用 FancyMath.getConstants().E。如果您想在 JS 中使用另一个名称,您可以像这样覆盖 JS 名称:[ReactConstant("e")]。
[ReactMethod] 属性是您定义方法的方式。在 FancyMath 中,我们有一个方法 add,它接受两个双精度浮点数并返回它们的和。与之前一样,您可以选择性地自定义名称,例如:[ReactMethod("add")]。
[ReactEvent] 属性是您定义事件的方式。在 FancyMath 中,我们有一个事件 AddEvent,它使用 ReactEvent<double> 委托,其中 double 表示事件数据的类型。现在,无论何时我们在原生代码中调用 AddEvent 委托(如上所述),JavaScript 中都会触发一个名为 "AddEvent" 的事件。与之前一样,您可以选择性地自定义 JS 中的名称,例如:[ReactEvent("addEvent")]。
4. 注册您的原生模块
重要提示:当您通过 CLI 创建新项目时,生成的
ReactApplication类将自动注册应用程序中定义的所有原生模块。**您无需手动注册在应用程序范围内定义的原生模块,因为它们会自动注册。**
现在,我们希望将新的 FancyMath 模块注册到 React Native,以便我们可以从 JavaScript 代码中使用它。为此,我们首先要创建一个实现 Microsoft.ReactNative.IReactPackageProvider 的 ReactPackageProvider。
ReactPackageProvider.cs:
using Microsoft.ReactNative.Managed;
namespace NativeModuleSample
{
public sealed class ReactPackageProvider : IReactPackageProvider
{
public void CreatePackage(IReactPackageBuilder packageBuilder)
{
packageBuilder.AddAttributedModules();
}
}
}
在这里,我们实现了 CreatePackage 方法,该方法接收 packageBuilder 来构建包的内容。由于我们使用反射来发现和绑定原生模块,因此我们调用 AddAttributedModules 扩展方法来注册程序集中所有具有 ReactModule 属性的原生模块。
现在我们有了 ReactPackageProvider,是时候在我们的 ReactApplication 中注册它了。我们只需将提供程序添加到 PackageProviders 属性即可。
App.xaml.cs:
using Microsoft.ReactNative;
namespace SampleApp
{
sealed partial class App : ReactApplication
{
public App()
{
/* Other Init Code */
PackageProviders.Add(new Microsoft.ReactNative.Managed.ReactPackageProvider()); // Includes any modules in this project
PackageProviders.Add(new NativeModuleSample.ReactPackageProvider());
/* Other Init Code */
}
}
}
此示例假设我们上面创建的 NativeModuleSample.ReactPackageProvider 与我们的应用程序位于不同的项目(程序集)中。但是,您会注意到,默认情况下,我们还添加了 Microsoft.ReactNative.Managed.ReactPackageProvider。
Microsoft.ReactNative.Managed.ReactPackageProvider 是一个便利功能,它确保应用程序项目中定义的所有原生模块和视图管理器都会自动注册。因此,如果您直接在应用程序项目中创建原生模块,实际上就不需要定义单独的 ReactPackageProvider。
属性
| 属性 | 使用 |
|---|---|
REACT_MODULE | 指定该类是一个原生模块,使该模块可被 AddAttributedModules 函数识别。 |
REACT_MODULE_NOREG | 指定该类是一个原生模块。 |
REACT_METHOD | 指定一个异步方法。 |
REACT_SYNC_METHOD | 指定一个同步方法。 |
REACT_GET_CONSTANTS | 指定一个提供一组常量的方法。(首选) |
REACT_CONSTANT | 指定表示常量的字段或属性。 |
REACT_CONSTANTPROVIDER | 指定提供一组常量的方法。 |
REACT_EVENT | 指定表示事件的字段或属性。 |
REACT_STRUCT | 指定可在原生方法中使用的 struct(不要将定义嵌套在 REACT_MODULE 内部)。 |
REACT_INIT | 指定一个类初始化模块。 |
ReactFunction | 指定您希望向原生代码公开的 JavaScript 函数。 |
3. 编写您的原生模块
这是一个用 C++ 编写的名为 FancyMath 的原生模块示例。它是一个简单的类,定义了两个数值常量和一个 'add' 方法。
FancyMath.h:
#pragma once
#include "pch.h"
#include <codegen\FancyMathSpec.g.h> // This file will be generated by the windows-codegen command from your js spec file
#include <functional>
#define _USE_MATH_DEFINES
#include <math.h>
#include "NativeModules.h"
namespace NativeModuleSample
{
REACT_MODULE(FancyMath);
struct FancyMath
{
// The namespace here will align with the codegenConfig.windows.namespace property in your package.json
using ModuleSpec = YourAppCodegenNamespace::FancyMathSpec;
REACT_GET_CONSTANTS(GetConstants)
SampleLibraryCodegen::FancyMathSpec_Constants GetConstants() noexcept {
SampleLibraryCodegen::FancyMathSpec_Constants constants;
constants.E = M_E;
constants.Pi = M_PI;
return constants;
}
REACT_METHOD(Add, L"add");
double Add(double a, double b) noexcept
{
double result = a + b;
AddEvent(result);
return result;
}
REACT_EVENT(AddEvent);
std::function<void(double)> AddEvent;
};
}
REACT_MODULE 宏属性表示该类是一个 React Native 原生模块。它接收类名作为第一个参数。所有其他宏属性也接收其目标作为第一个参数。REACT_MODULE 有一个可选参数用于 JavaScript 可见的模块名称,以及一个可选参数用于已注册事件发射器的名称。默认情况下,JavaScript 可见的名称与类名相同,默认事件发射器是 RCTDeviceEventEmitter。
指定 ModuleSpec 很重要。定义 ModuleSpec 将强制您的模块定义与 JS 规范文件相同的方法。
注意:使用
REACT_METHOD等注释的方法必须具有noexcept说明符,否则程序将无法编译。模块作者应确保在方法内部处理所有异常。
您可以像这样覆盖 JavaScript 模块名称:REACT_MODULE(FancyMath, L"math")。
您可以像这样指定不同的事件发射器:REACT_MODULE(FancyMath, L"math", L"mathEmitter")。
注意:使用默认事件发射器
RCTDeviceEventEmitter时,所有原生事件名称必须在**所有原生模块中(甚至包括 RN 内置的模块)全局唯一**。但是,指定您自己的事件发射器意味着您还需要创建并注册它。此过程在原生模块和 React Native Windows(高级主题)文档中进行了概述。
然后我们定义常量,这是通过使用 REACT_GET_CONSTANTS 宏属性完成的。在这种情况下,我们返回一个使用 REACT_STRUCT 定义的结构体。这将生成代码以自动将结构体转换为 JSValueObject。
添加自定义方法同样容易,只需使用 REACT_METHOD 属性修饰公共方法即可。在 FancyMath 中,我们有一个方法 add,它接受两个双精度浮点数并返回它们的和。同样,我们在 REACT_METHOD 宏属性中指定了可选的 name 参数,因此在 JS 中我们调用 add 而不是 Add。
原生模块无法检查参数类型和参数数量是否与 JavaScript 调用和原生代码接受的参数匹配。但是,框架将验证 promise 类参数的数量是否匹配:如果 JavaScript API 是 async,它将期望原生方法实现签名中有一个“promise 类”参数。
“promise 类”参数可以是
React::ReactPromise<T>- 回调函数或函数对象。
请参阅使用异步 Windows API。
这是一个返回字符串的 async 方法示例
REACT_METHOD(GetString, L"getString");
void GetString(React::ReactPromise<std::string>&& result) noexcept
{
if (error) {
result.Reject("Failure");
} else {
std::string text = DoSomething();
result.Resolve(text);
}
}
这也可以与 C++/WinRT 事件处理程序或 IAsyncOperation<T> 结合使用,如下所示
REACT_METHOD(GetString, L"getString");
void GetString(React::ReactPromise<std::string>&& result) noexcept
{
if (error) {
result.Reject("Failure");
} else {
something.Completed([result] (const auto& operation, const auto& status) {
// do error checking, e.g. status should be Completed
winrt::hstring result{operation.GetResults()};
result.Resolve(winrt::to_string(result));
});
}
}
有关更多详细信息,请参阅JavaScript 和 Windows Runtime 字符串。
当 API 返回 JavaScript 对象或将 JavaScript 对象作为输入参数时,可以使用 JSValue 类型。
原生模块将希望使用 REACT_METHOD 而不是 REACT_SYNC_METHOD,因为后者排除了网络调试并具有性能影响。使用网络调试时,您将看到一个异常,内容为:“Chrome 中不支持在原生模块上调用同步方法。请考虑提供替代方法来在调试模式下公开此方法,例如通过提前公开常量。”请参阅:MessageQueue.js。
要添加自定义事件,我们使用 REACT_EVENT 属性修饰一个 std::function<void(double)> 委托,其中 double 代表事件数据的类型。现在,无论何时我们在原生代码中调用 AddEvent 委托(如上所述),JavaScript 中都会触发一个名为 "AddEvent" 的事件。与之前一样,您可以选择性地自定义 JS 中的名称,例如:REACT_EVENT(AddEvent, "addEvent")。
4. 注册您的原生模块
重要提示:当您通过 CLI 创建新项目时,生成的
ReactApplication类将自动注册应用程序中定义的所有原生模块。**您无需手动注册在应用程序范围内定义的原生模块,因为它们会自动注册。**
现在,我们希望将新的 FancyMath 模块注册到 React Native,以便我们可以从 JavaScript 代码中使用它。为此,我们首先要创建一个实现 Microsoft.ReactNative.IReactPackageProvider 的 ReactPackageProvider。它从定义接口定义(.idl)文件开始
ReactPackageProvider.idl:
namespace NativeModuleSample
{
[webhosthidden]
[default_interface]
runtimeclass ReactPackageProvider : Microsoft.ReactNative.IReactPackageProvider
{
ReactPackageProvider();
};
}
之后我们添加 .h 和 .cpp 文件
ReactPackageProvider.h:
#pragma once
#include "ReactPackageProvider.g.h"
using namespace winrt::Microsoft::ReactNative;
namespace winrt::NativeModuleSample::implementation
{
struct ReactPackageProvider : ReactPackageProviderT<ReactPackageProvider>
{
ReactPackageProvider() = default;
void CreatePackage(IReactPackageBuilder const& packageBuilder) noexcept;
};
}
namespace winrt::NativeModuleSample::factory_implementation
{
struct ReactPackageProvider : ReactPackageProviderT<ReactPackageProvider, implementation::ReactPackageProvider> {};
}
ReactPackageProvider.cpp:
#include "pch.h"
#include "ReactPackageProvider.h"
#include "ReactPackageProvider.g.cpp"
#include <ModuleRegistration.h>
// NOTE: You must include the headers of your native modules here in
// order for the AddAttributedModules call below to find them.
#include "FancyMath.h"
namespace winrt::NativeModuleSample::implementation
{
void ReactPackageProvider::CreatePackage(IReactPackageBuilder const& packageBuilder) noexcept
{
AddAttributedModules(packageBuilder, true);
}
}
如果使用 REACT_MODULE_NOREG 而不是 REACT_MODULE,AddAttributedModules 将不会为您注册此模块。此类模块应手动注册
packageBuilder.AddTurboModule(L"FancyMath", MakeModuleProvider<NativeModuleSample::FancyMath>());
在这里,我们实现了 CreatePackage 方法,该方法接收 packageBuilder 来构建包的内容。由于我们使用宏和模板来发现和绑定原生模块,因此我们调用 AddAttributedModules 函数来注册我们 DLL 中所有具有 REACT_MODULE 宏属性的原生模块。在此处指定 true 会将所有原生模块注册为 TurboModules 而不是原生模块。这将避免原生模块调用中发生的一些额外序列化。如果由于某种原因您需要模块继续作为原生模块运行,您可以在此处指定 false。
有关 TurboModules 的更多详细信息,请参阅原生模块与 Turbo 模块。
现在我们有了 ReactPackageProvider,是时候在我们的 ReactApplication 中注册它了。我们只需将提供程序添加到 PackageProviders 属性即可。
App.cpp:
#include "pch.h"
#include "App.h"
#include "ReactPackageProvider.h"
#include "winrt/NativeModuleSample.h"
namespace winrt::SampleApp::implementation {
App::App() noexcept {
/* Other Init Code */
PackageProviders().Append(make<ReactPackageProvider>()); // Includes all modules in this project
PackageProviders().Append(winrt::NativeModuleSample::ReactPackageProvider());
/* Other Init Code */
}
} // namespace winrt::SampleApp::implementation
此示例假设我们上面创建的 NativeModuleSample::ReactPackageProvider 与我们的应用程序位于不同的项目(程序集)中。但是,您会注意到,默认情况下,我们还添加了 SampleApp::ReactPackageProvider。
SampleApp::ReactPackageProvider 是一个便利功能,它确保应用程序项目中定义的所有原生模块和视图管理器都会自动注册。因此,如果您直接在应用程序项目中创建原生模块,实际上就不需要定义单独的 ReactPackageProvider。
JavaScript 和 Windows Runtime 字符串
请注意,JavaScript 字符串是 UTF8(即 std::string),但 WinRT 字符串是 UTF16(即 C++/WinRT 中的 winrt::hstring),因此在 JavaScript 和 WinRT API 之间进行互操作时,您需要在这两种编码之间进行转换。请参阅 C++/WinRT 中的字符串处理,特别是 winrt::to_string 和 winrt::to_hstring。
5. 在 JS 中使用您的原生模块
现在我们有一个已注册到 React Native Windows 的原生模块。我们如何在 JS 中访问它?这是一个简单的 RN 应用程序
NativeModuleSample.js:
import React, { Component } from 'react';
import {
AppRegistry,
Alert,
Text,
View,
} from 'react-native';
import FancyMath from './NativeFancyMath'
import { NativeEventEmitter } from 'react-native';
const FancyMathEventEmitter = new NativeEventEmitter(FancyMath);
class NativeModuleSample extends Component {
componentDidMount() {
// Subscribing to FancyMath.AddEvent
FancyMathEventEmitter.addListener('AddEvent', eventHandler, this);
}
componentWillUnmount() {
// Unsubscribing from FancyMath.AddEvent
FancyMathEventEmitter.removeListener('AddEvent', eventHandler, this);
}
eventHandler(result) {
console.log("Event was fired with: " + result);
}
_onPressHandler() {
// Calling FancyMath.add method
FancyMath.add(
/* arg a */ FancyMath.getConstants().Pi,
/* arg b */ FancyMath.E,
/* callback */ function (result) {
Alert.alert(
'FancyMath',
`FancyMath says ${FancyMath.getConstants().Pi} + ${FancyMath.getConstants().E} = ${result}`,
[{ text: 'OK' }],
{cancelable: false});
});
}
render() {
return (
<View>
<Text>FancyMath says PI = {FancyMath.getConstants().Pi}</Text>
<Text>FancyMath says E = {FancyMath.getConstants().E}</Text>
<Button onPress={this._onPressHandler} title="Click me!"/>
</View>);
}
}
AppRegistry.registerComponent('NativeModuleSample', () => NativeModuleSample);
要访问您的原生模块,您需要从您的规范文件导入,在本例中是 NativeFancyMath。由于我们的模块触发事件,我们还引入了 NativeEventEmitter。
要访问我们的 FancyMath 常量,我们只需调用 FancyMath.getConstants().E 和 FancyMath.getConstants().Pi。
由于 JS 引擎的异步性质,对方法的调用有所不同。如果原生方法不返回任何内容,我们可以直接调用该方法。但是,在这种情况下,FancyMath.add() 返回一个值,因此除了两个必要的参数之外,我们还包含一个回调函数,该函数将使用 FancyMath.add() 的结果进行调用。在上面的示例中,我们可以看到回调会弹出一个包含结果值的警报对话框。
对于事件,您会看到我们创建了一个 NativeEventEmitter 实例,传入了我们的 FancyMath 模块,并将其命名为 FancyMathEventEmitter。然后,我们可以使用 FancyMathEventEmitter.addListener() 和 FancyMathEventEmitter.removeListener() 方法订阅我们的 FancyMath.AddEvent。在这种情况下,当原生代码中触发 AddEvent 时,将调用 eventHandler,它会将结果记录到控制台日志中。