原生平台:原生组件 (Fabric)
本指南涵盖了通过为 Windows 平台实现一个原生组件,将原生 Windows UI 暴露给 React Native。有关 Windows 上原生开发的高级概述,请在阅读本指南之前参阅原生平台:概述。
注意: 有关为 Android 和 iOS 平台实现新原生组件的步骤,请参阅 reactnative.dev 原生组件指南。
架构说明: 本指南遵循创建 Fabric 原生组件 的建议,以支持 React Native 的新架构。它不适用于旧架构应用。要支持面向旧架构的 React Native for Windows 应用,请参阅原生平台:原生组件 (Paper)。有关 React Native for Windows 中 React Native 架构的更多信息,请参阅新架构与旧架构。
高级概述
为了为原生组件实现 Windows 支持,您需要
- 在 TypeScript 规范文件中定义原生组件的 API 表面
- 使用 React Native for Windows 的原生代码生成器,获取 TypeScript 规范文件并为 Windows 代码创建 C++ 头文件
- 编写 Windows C++ 代码以实现由生成的头文件指定的组件视图
- 在 JavaScript 中使用原生组件
分步指南
0. 设置
您需要一个已初始化并支持 Windows 的 React Native 库项目。
注意: 本指南的其余部分假设您已按照 原生平台:入门 指南设置了一个名为
testlib
的新库项目。
1. 在 TypeScript 中定义 API 接口
新库的默认模板不包含原生组件的示例,因此我们需要创建一个。在本指南中,我们将实现一个组件,它使用原生 UI 代码强制其子组件在圆形边界内渲染,遮罩掉边角。这个 CircleMask
可以用来将用户的方形帐户或个人资料图片渲染成圆形。
首先,我们需要在新的 TypeScript 规范文件 src\CircleMaskNativeComponent.ts
中创建组件的接口
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { ViewProps } from 'react-native';
export interface CircleMaskProps extends ViewProps {}
export default codegenNativeComponent<CircleMaskProps>('CircleMask');
此规范文件声明 React Native 期望每个平台都实现一个名为 CircleMask
的原生组件,该组件支持 CircleMaskProps
的属性。
注意: 每个原生组件规范文件都必须采用
<componentName> + NativeComponent.ts
的格式命名,才能被正确识别为规范文件,而不仅仅是库中的常规 TypeScript 文件。
2. 使用适用于 Windows 的 React Native 的原生库代码生成器
现在,在我们为 Fabric 原生组件实现原生 C++ 代码之前,我们需要运行 React Native for Windows 的原生库代码生成器,即 codegen-windows 命令,它将获取 TypeScript 规范文件并生成一些带有我们需要实现的 API 表面的 C++ 头文件。
首先,我们需要确保在库的 package.json
文件中正确定义了 codegenConfig
对象
"codegenConfig": {
"name": "RNTestlibSpec",
"type": "all",
"jsSrcsDir": "src",
"outputDir": {
"ios": "ios/generated",
"android": "android/generated"
},
"android": {
"javaPackageName": "com.testlib"
},
"includesGeneratedCode": true,
"windows": {
"namespace": "testlibCodegen",
"generators": [
"modulesWindows",
"componentsWindows"
],
"outputDirectory": "windows/testlib/codegen",
"separateDataTypes": true
}
}
配置与其他平台部分共享,但对于 Windows,相关字段是 type
、jsSrcsDirs
和 windows
对象。虽然默认配置已正确设置为支持库的原生模块,但我们仍需要修改它以支持我们对原生组件的需求。
具体来说,为了启用原生组件代码生成,我们将 "type: "modules"
更改为 "type": "all"
,并且添加了 "generators": [ "modulesWindows", "componentsWindows" ]
数组。
注意: 有关配置
codegenConfig
的更多信息,请参阅 codegen-windows 代码生成配置。
现在我们只需要运行 codegen-windows 命令
yarn react-native codegen-windows
注意: 默认情况下,
codegen-windows
命令在每次原生构建开始时自动运行。通过这种方式,TypeScript 规范文件中 API 表面的更改将反映在生成的头文件中,从而强制原生代码与组件所需的 API 表面保持同步。
现在我们应该在项目的代码生成输出目录中看到一些文件,即我们在配置中指定的 windows\testlib\codegen
文件夹。原生组件的代码生成文件进一步放置在 react\components\RNTestlibSpec
文件夹下。具体来说,对于我们的 CircleMask
组件,我们应该会看到一个相当大的 CircleMask.g.h
(下面已截断)
/*
* This file is auto-generated from CircleMaskNativeComponent spec file in flow / TypeScript.
*/
// clang-format off
#pragma once
#include <NativeModules.h>
#ifdef RNW_NEW_ARCH
#include <JSValueComposition.h>
#include <winrt/Microsoft.ReactNative.Composition.h>
#include <winrt/Microsoft.UI.Composition.h>
#endif // #ifdef It will not work with Old Architecture apps.
#ifdef RNW_NEW_ARCH
namespace testlibCodegen {
REACT_STRUCT(CircleMaskProps)
struct CircleMaskProps : winrt::implements<CircleMaskProps, winrt::Microsoft::ReactNative::IComponentProps> {
// Implementation truncated
};
struct CircleMaskEventEmitter {
// Implementation truncated
};
template<typename TUserData>
struct BaseCircleMask {
// Implementation truncated
};
template <typename TUserData>
void RegisterCircleMaskNativeComponent(
winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder,
std::function<void(const winrt::Microsoft::ReactNative::Composition::IReactCompositionViewComponentBuilder&)> builderCallback) noexcept {
packageBuilder.as<winrt::Microsoft::ReactNative::IReactPackageBuilderFabric>().AddViewComponent(
L"CircleMask", [builderCallback](winrt::Microsoft::ReactNative::IReactViewComponentBuilder const &builder) noexcept {
// Implementation truncated
});
}
} // namespace testlibCodegen
#endif // #ifdef RNW_NEW_ARCH
生成的代码包含三个值得注意的原生类型
- 一个
CircleMaskProps
结构体,捕获 TypeScriptCircleMaskProps
接口中指定的属性 - 一个
CircleMaskEventEmitter
结构体,以便组件可以触发任何指定的 JavaScript 事件 - 一个
BaseCircleMask
结构体,用作CircleMask
Fabric 组件视图的基类型 - 一个
RegisterCircleMaskNativeComponent
函数,用于注册CircleMask
Fabric 组件视图
另请注意 #ifdef RNW_NEW_ARCH
的使用,以确保这些类型仅在库由新架构应用使用时才包含。
注意: 有关此文件的非截断版本,请参阅原生模块示例的
CircleMask.g.h
。
3. 实现 Windows C++ 代码
现在代码生成已完成,是时候在 Windows 代码中实现一个 CircleMaskComponentView
了。React Native for Windows Fabric 组件视图用 C++ 实现,并使用 Microsoft::UI::Composition
API 渲染 UI,也称为 Windows 应用 SDK/WinUI 3 视觉层。
3.1 实现 Fabric 组件视图
要创建新的 Fabric 组件视图,我们需要创建两个新文件(在我们的示例中,windows\testlib
文件夹中的 CircleMask.h
和 CircleMask.cpp
)
#pragma once
#include "pch.h"
#ifdef RNW_NEW_ARCH
#include "codegen/react/components/RNTestlibSpec/CircleMask.g.h"
#include <winrt/Microsoft.ReactNative.Composition.Experimental.h>
#endif
namespace winrt::testlib::implementation {
void RegisterCircleMaskNativeComponent(
winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept;
#ifdef RNW_NEW_ARCH
struct CircleMaskComponentView : winrt::implements<CircleMaskComponentView, winrt::IInspectable>,
testlibCodegen::BaseCircleMask<CircleMaskComponentView> {
winrt::Microsoft::UI::Composition::Visual CreateVisual(
const winrt::Microsoft::ReactNative::ComponentView &view) noexcept override;
void Initialize(const winrt::Microsoft::ReactNative::ComponentView & /*view*/) noexcept override;
private:
winrt::Microsoft::ReactNative::ComponentView::LayoutMetricsChanged_revoker m_layoutMetricChangedRevoker;
winrt::Microsoft::UI::Composition::SpriteVisual m_visual{nullptr};
};
#endif // #ifdef RNW_NEW_ARCH
} // namespace winrt::testlib::implementation
#include "pch.h"
#include "CircleMask.h"
namespace winrt::testlib::implementation {
void RegisterCircleMaskNativeComponent(
winrt::Microsoft::ReactNative::IReactPackageBuilder const &packageBuilder) noexcept {
#ifdef RNW_NEW_ARCH
testlibCodegen::RegisterCircleMaskNativeComponent<CircleMaskComponentView>(
packageBuilder,
[](const winrt::Microsoft::ReactNative::Composition::IReactCompositionViewComponentBuilder &builder) {
// Turn off default border handling, as it overrides the Clip property of the visual and doesn't render
// correctly anyway This means we would have to implement drawing our own borders (which we don't do in this
// example)
builder.SetViewFeatures(
winrt::Microsoft::ReactNative::Composition::ComponentViewFeatures::Default &
~winrt::Microsoft::ReactNative::Composition::ComponentViewFeatures::NativeBorder);
});
#endif
}
#ifdef RNW_NEW_ARCH
winrt::Microsoft::UI::Composition::Visual CircleMaskComponentView::CreateVisual(
const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
auto compositor = view.as<winrt::Microsoft::ReactNative::Composition::ComponentView>().Compositor();
m_visual = compositor.CreateSpriteVisual();
auto ellipseGeometry = compositor.CreateEllipseGeometry();
auto clip = compositor.CreateGeometricClip();
clip.Geometry(ellipseGeometry);
m_visual.Clip(clip);
return m_visual;
}
void CircleMaskComponentView::Initialize(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
m_layoutMetricChangedRevoker = view.LayoutMetricsChanged(
winrt::auto_revoke,
[wkThis = get_weak()](
const winrt::IInspectable & /*sender*/, const winrt::Microsoft::ReactNative::LayoutMetricsChangedArgs &args) {
if (auto strongThis = wkThis.get()) {
auto visual = strongThis->m_visual;
// Turning off default border handling has the side-effect of also stopping the visual from being positioned,
// so unless that changes we have to position the visual ourselves
// See https://github.com/microsoft/react-native-windows/issues/14706
visual.Size(
{args.NewLayoutMetrics().Frame.Width * args.NewLayoutMetrics().PointScaleFactor,
args.NewLayoutMetrics().Frame.Height * args.NewLayoutMetrics().PointScaleFactor});
visual.Offset({
args.NewLayoutMetrics().Frame.X * args.NewLayoutMetrics().PointScaleFactor,
args.NewLayoutMetrics().Frame.Y * args.NewLayoutMetrics().PointScaleFactor,
0.0f,
});
auto ellipseGeometry = strongThis->m_visual.Clip()
.as<winrt::Microsoft::UI::Composition::CompositionGeometricClip>()
.Geometry()
.as<winrt::Microsoft::UI::Composition::CompositionEllipseGeometry>();
winrt::Windows::Foundation::Numerics::float2 radius = {
args.NewLayoutMetrics().Frame.Width * args.NewLayoutMetrics().PointScaleFactor / 2,
args.NewLayoutMetrics().Frame.Height * args.NewLayoutMetrics().PointScaleFactor / 2};
ellipseGeometry.Center(radius);
ellipseGeometry.Radius(radius);
}
});
}
#endif // #ifdef RNW_NEW_ARCH
} // namespace winrt::testlib::implementation
如您所见,CircleMask.h
文件定义了两件事
- 一个
RegisterCircleMaskNativeComponent
函数,用于向 React Native 注册CircleMask
Fabric 组件视图 - 一个包含
CircleMask
Fabric 组件视图的CircleMaskComponentView
结构体
这两者都依赖于我们之前生成的 CircleMask.g.h
文件中提供的类型。然后在 CircleMask.cpp
中,我们有新原生组件的实现细节。再次注意 #ifdef RNW_NEW_ARCH
的使用,以确保 Fabric 组件视图代码仅在库由新架构应用使用时才包含。
注意: 有关如何同时为 Fabric 和 Paper 实现
CircleMask
组件的更完整示例,请参阅原生模块示例项目中的实现。
3.2 将原生组件的文件添加到原生项目
由于我们创建了一些新的原生文件(上面的 CircleMask.h
和 CircleMask.cpp
),我们需要确保它们包含在原生 Windows 项目中(在我们的示例中是 windows\testlib\testlib.vcxproj
和 windows\testlib\testlib.vcxproj.filters
),以便它们包含在原生构建中
<ItemGroup>
<ClInclude Include="testlib.h" />
+ <ClInclude Include="CircleMask.h" />
<ClInclude Include="ReactPackageProvider.h">
<DependentUpon>ReactPackageProvider.idl</DependentUpon>
</ClInclude>
<ClInclude Include="resource.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="testlib.cpp" />
+ <ClCompile Include="CircleMask.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ReactPackageProvider.cpp">
<DependentUpon>ReactPackageProvider.idl</DependentUpon>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="targetver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="testlib.h">
<Filter>Header Files</Filter>
</ClInclude>
+ <ClInclude Include="CircleMask.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="testlib.cpp">
<Filter>Source Files</Filter>
</ClCompile>
+ <ClCompile Include="CircleMask.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
注意: 代码生成器在
windows/testlib/codegen
文件夹中创建的所有头文件都已包含在内,无需手动添加此处。
3.3 向 React 包提供程序注册 Fabric 组件视图
每个 React Native for Windows 库都包含一个 IReactPackageProvider
,其中包含库的所有原生模块(和/或组件),以便 React Native 可以在运行时使用它们。我们需要做的最后一点原生工作是更新 windows\testlib\ReactPackageProvider.cpp
中的 ReactPackageProvider::CreatePackage
#include "pch.h"
#include "ReactPackageProvider.h"
#if __has_include("ReactPackageProvider.g.cpp")
#include "ReactPackageProvider.g.cpp"
#endif
#include "testlib.h"
#include "CircleMask.h"
using namespace winrt::Microsoft::ReactNative;
namespace winrt::testlib::implementation {
void ReactPackageProvider::CreatePackage(IReactPackageBuilder const &packageBuilder) noexcept {
AddAttributedModules(packageBuilder, true);
RegisterCircleMaskNativeComponent(packageBuilder);
}
} // namespace winrt::testlib::implementation
这里的关键是添加 #include "CircleMask.h"
头文件,并添加对我们之前创建的 RegisterCircleMaskNativeComponent
函数的调用。这确保了新的原生组件包含在库的包中。
4. 在 JavaScript 中使用原生组件
现在,如果我们回到 CircleMaskNativeComponent.ts
TypeScript 规范文件,我们会看到它导出了 CircleMaskProps
接口以及原生组件。下一步是在我们的 JavaScript 代码中使用这些导出的项。
由于库的目的是将原生功能公开给库外部的代码(即我们的适用于 Windows 的 React Native 应用代码),因此默认是在项目的索引中导出该功能,在本例中为 src\index.tsx
import Testlib from './NativeTestlib';
export function multiply(a: number, b: number): number {
return Testlib.multiply(a, b);
}
export {default as CircleMask} from './CircleMaskNativeComponent';
export * from './CircleMaskNativeComponent';
因此,我们可以看到 testlib
JavaScript 模块只是将我们的新原生组件导出为 CircleMask
,并将 CircleMaskNativeComponent
模块中的所有其他内容按原样导出。
注意: 库不必将其任何原生组件直接暴露给其使用者。此示例仅说明了按原样导出
CircleMask
原生组件的最简单情况。库可以而且通常会将其原生组件包装在 JavaScript 组件中,因此可能会向其客户提供完全不同的 API 表面。
后续步骤
实现原生库后,最后一步是在适用于 Windows 的 React Native 应用中使用它。继续阅读 原生平台:使用原生库。