进阶 reactnative.dev 2026-10-11 06:04:21 · 36 阅读

第38章 Fabric 原生组件简介 · React Native

版本:0.77 原生组件 如果你想创建新的 React Native 组件来包装宿主组件(Host Component),比如 Android 上的某种独特的 CheckBox,或 iOS 上的 UIButton,那你就需要使用 Fabric 原生组件。 本指南将通过实现一个 web view 组件,带你了解如何构建 Fabric 原生组件。具体步骤如下: 用 Flow 或 TypeScript 定义一个 JavaScript 规范文件(specification)。 配置依赖管理系统,以便根据规范生成代码并实现自动链接。 编写原生代码。 在应用中使用该组件。 你需要一个由 plain template 生成的应用来使用这个组件: npx @react-native-community/cli@latest init Demo --install-pods false
创建一个 WebView 组件​ 本指南将演示如何创建一个 Web View 组件。我们会分别使用 Android 的 WebView 组件和 iOS 的 WKWebView 组件来实现。 先创建存放组件代码的目录结构: mkdir -p Demo/{specs,android/app/src/main/java/com/webview}
你会得到如下工作目录结构: Demo
├── android/app/src/main/java/com/webview
└── ios
└── specs
各目录的用途如下: android/app/src/main/java/com/webview 目录用于存放 Android 代码。 ios 目录用于存放 iOS 代码。 specs 目录用于存放 Codegen 的规范文件。 1. 为 Codegen 定义规范​ 规范必须用 TypeScript 或 Flow 来定义(详见 Codegen 文档)。Codegen 会根据它生成 C++、Objective-C++ 和 Java 代码,把你的平台代码连接到 React 所运行的 JavaScript 运行时。 规范文件必须命名为 NativeComponent.{ts|js},Codegen 才能识别它。NativeComponent 这个后缀不只是命名习惯,Codegen 实际上就是靠它来检测规范文件的。 使用以下规范来定义我们的 WebView 组件: TypeScriptFlowDemo/specs/WebViewNativeComponent.tsimport type {HostComponent, ViewProps} from 'react-native';
import type {BubblingEventHandler} from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';

type WebViewScriptLoadedEvent = {
result: 'success' | 'error';
};

export interface NativeProps extends ViewProps {
sourceURL?: string;
onScriptLoaded?: BubblingEventHandler | null;
}

export default codegenNativeComponent(
'CustomWebView',
) as HostComponent;
Demo/RCTWebView/js/RCTWebViewNativeComponent.js// @flow strict-local

import type {HostComponent, ViewProps} from 'react-native';
import type {BubblingEventHandler} from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';

type WebViewScriptLoadedEvent = $ReadOnly<{|
result: "success" | "error",
|}>;

type NativeProps = $ReadOnly<{|
...ViewProps,
sourceURL?: string;
onScriptLoaded?: BubblingEventHandler?;
|}>;

export default (codegenNativeComponent(
'CustomWebView',
): HostComponent);

该规范主要由三部分构成(不含导入语句): WebViewScriptLoadedEvent 是事件从 Native 端向 JavaScript 端传递数据所需的辅助数据类型。 NativeProps 定义了组件可接受的 Props。 codegenNativeComponent 语句用于为自定义组件生成代码,并定义组件名称,以便与 Native 实现进行匹配。 与 Native Modules 类似,你可以在 specs/ 目录中放置多个规范文件。关于可用类型及其对应的平台类型,请参阅附录。 2. 配置 Codegen 运行 React Native 的 Codegen 工具会利用该规范来生成平台特定的接口和样板代码。为此,Codegen 需要知道规范文件的位置以及处理方式。请更新 package.json 以包含以下配置: JSON "start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "AppSpec",
"type": "components",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.webview"
},
"ios": {
"componentProvider": {
"CustomWebView": "RCTWebView"
}
}
},
"dependencies": {
Codegen 的配置完成后,我们需要准备原生代码,以便将生成代码接入。 请注意,在 iOS 上,我们是声明式地将规范导出的 JS 组件名称(CustomWebView)与将以原生方式实现该组件的 iOS 类进行映射。 2. 构建原生代码 现在是编写原生平台代码的时候了,这样当 React 需要渲染视图时,平台就能创建相应的原生视图并将其渲染到屏幕上。 你需要同时处理 Android 和 iOS 平台。 此指南向你展示了如何创建仅适用于新架构的原生组件。如果需要同时支持新架构和旧架构,请参阅我们的向后兼容性指南。 Android 部分:现在该编写一些 Android 平台代码,以便能够渲染 web 视图。你需要遵循以下步骤: 运行 Codegen 编写 ReactWebView 的代码 编写 ReactWebViewManager 的代码 编写 ReactWebViewPackage 的代码 在应用中注册 ReactWebViewPackage 1. 通过 Gradle 运行 Codegen 只需运行一次,即可生成脚手架代码,供你选择的 IDE 使用。 Demo/cd android
./gradlew generateCodegenArtifactsFromSchema
Codegen 将生成你需要实现的 ViewManager 接口以及 web 视图的 ViewManager 委托。 2. 编写 ReactWebView ReactWebView 是一个组件,它封装了 React Native 在使用我们自定义组件渲染时所使用的 Android 原生视图。 在 android/src/main/java/com/webview 文件夹中创建一个名为 ReactWebView.java 或 ReactWebView.kt 的文件,并添加以下代码: JavaKotlinDemo/android/src/main/java/com/webview/ReactWebView.javapackage com.webview;

import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.Event;

public class ReactWebView extends WebView {
public ReactWebView(Context context) {
super(context);
configureComponent();
}

public ReactWebView(Context context, AttributeSet attrs) {
super(context, attrs);
configureComponent();
}
```java public ReactWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
configureComponent();
}

private void configureComponent() {
this.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
this.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
emitOnScriptLoaded(OnScriptLoadedEventResult.success);
}
});
}

public void emitOnScriptLoaded(OnScriptLoadedEventResult result) {
ReactContext reactContext = (ReactContext) context;
int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, getId());
WritableMap payload = Arguments.createMap();
payload.putString("result", result.name());

OnScriptLoadedEvent event = new OnScriptLoadedEvent(surfaceId, getId(), payload);
if (eventDispatcher != null) {
eventDispatcher.dispatchEvent(event);
}
}

public enum OnScriptLoadedEventResult {
success,
error
}

private class OnScriptLoadedEvent extends Event {
private final WritableMap payload;

OnScriptLoadedEvent(int surfaceId, int viewId, WritableMap payload) {
super(surfaceId, viewId);
this.payload = payload;
}

@Override
public String getEventName() {
return "onScriptLoaded";
}

@Override
public WritableMap getEventData() {
return payload;
}
}
}
Demo/android/src/main/java/com/webview/ReactWebView.ktpackage com.webview

import android.content.Context
import android.util.AttributeSet
import android.webkit.WebView
import android.webkit.WebViewClient
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.Event

class ReactWebView: WebView {
constructor(context: Context) : super(context) {
configureComponent()
}

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
configureComponent()
}

``` 这段代码定义了 Fabric 原生组件的构造函数和配置逻辑,其中 `configureComponent` 方法初始化布局参数和 WebView 客户端,监听页面加载完成事件;`emitOnScriptLoaded` 方法通过事件分发器发送脚本加载结果事件,包含表面 ID、视图 ID 及结果类型(成功或失败);内部枚举类 `OnScriptLoadedEventResult` 定义了事件结果类型;内部类 `OnScriptLoadedEvent` 继承自事件基类,封装事件名称和数据负载。

ReactWebView 继承了 Android 的 WebView,因此你可以轻松复用平台已定义的所有属性。

该类定义了三个 Android 构造函数,但将实际实现推迟到了私有的 configureComponent 函数中。这个函数负责初始化组件的所有特定属性:在此处,你设置 WebView 的布局并定义用于定制 WebView 行为的 WebClient。在这段代码中,通过实现 WebClient 的 onPageFinished 方法,ReactWebView 会在页面加载完成时发出一个事件。

接着,代码定义了一个辅助函数来实际发出事件。要发出事件,你需要:

  • 获取 ReactContext 的引用;
  • 检索你所展示视图的 surfaceId;
  • 获取与该视图关联的 eventDispatcher 引用;
  • 使用 WritableMap 对象构建事件负载;
  • 创建需要发送给 JavaScript 的事件对象;
  • 调用 eventDispatcher.dispatchEvent 发送事件。

文件的最后部分定义了发送事件所需的数据类型:

  • OnScriptLoadedEventResult,包含 OnScriptLoaded 事件的可能结果;
  • 实际的 OnScriptLoaded 事件,它需要继承 React Native 的 Event 类。

3. 编写 WebViewManager

WebViewManager 是连接 React Native 运行时与原生视图的类。

当 React 接收到来自应用渲染特定组件的指令时,它会使用已注册的视图管理器来创建视图并传递所有必需的属性。

以下是 ReactWebViewManager 的代码。

JavaKotlinDemo/android/src/main/java/com/webview/ReactWebViewManager.java
package com.webview;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.CustomWebViewManagerInterface;
import com.facebook.react.viewmanagers.CustomWebViewManagerDelegate;

import java.util.HashMap;
import java.util.Map;

@ReactModule(name = ReactWebViewManager.REACT_CLASS)
class ReactWebViewManager extends SimpleViewManager implements CustomWebViewManagerInterface {
private final CustomWebViewManagerDelegate delegate =
new CustomWebViewManagerDelegate<>(this);

@Override
public ViewManagerDelegate getDelegate() {
return delegate;
}

@Override
public String getName() {
return REACT_CLASS;
}

@Override
public ReactWebView createViewInstance(ThemedReactContext context) {
return new ReactWebView(context);
}

@ReactProp(name = "sourceUrl")
@Override
public void setSourceURL(ReactWebView view, String sourceURL) {
if (sourceURL == null) {
view.emitOnScriptLoaded(ReactWebView.OnScriptLoadedEventResult.error);
return;
}
view.loadUrl(sourceURL, new HashMap<>());
}

public static final String REACT_CLASS = "CustomWebView";

@Override
public Map getExportedCustomBubblingEventTypeConstants() {
Map map = new HashMap<>();
Map bubblingMap = new HashMap<>();
bubblingMap.put("phasedRegistrationNames", new HashMap() {{
put("bubbled", "onScriptLoaded");
put("captured", "onScriptLoadedCapture");
}});
map.put("onScriptLoaded", bubblingMap);
return map;
}
}
Demo/android/src/main/java/com/webview/ReactWebViewManager.ktpackage com.webview

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.CustomWebViewManagerInterface;
import com.facebook.react.viewmanagers.CustomWebViewManagerDelegate;

@ReactModule(name = ReactWebViewManager.REACT_CLASS)
class ReactWebViewManager(context: ReactApplicationContext) : SimpleViewManager(), CustomWebViewManagerInterface {
private val delegate: CustomWebViewManagerDelegate =
CustomWebViewManagerDelegate(this)

override fun getDelegate(): ViewManagerDelegate = delegate

override fun getName(): String = REACT_CLASS

override fun createViewInstance(context: ThemedReactContext): ReactWebView = ReactWebView(context)

@ReactProp(name = "sourceUrl")
override fun setSourceURL(view: ReactWebView, sourceURL: String?) {
if (sourceURL == null) {
view.emitOnScriptLoaded(ReactWebView.OnScriptLoadedEventResult.error)
return;
}
view.loadUrl(sourceURL, emptyMap())
}

companion object {
const val REACT_CLASS = "CustomWebView"
}

override fun getExportedCustomBubblingEventTypeConstants(): Map =
mapOf(
"onScriptLoaded" to
mapOf(
"phasedRegistrationNames" to
mapOf(
"bubbled" to "onScriptLoaded",
"captured" to "onScriptLoadedCapture"
)))
}
ReactWebViewManager 继承 React 中的 SimpleViewManager 类,并实现由 Codegen 生成的 CustomWebViewManagerInterface。 它持有对 CustomWebViewManagerDelegate 的引用,该委托对象同样由 Codegen 生成。 接下来重写了 getName 函数,该函数必须返回在 spec 的 codegenNativeComponent 函数调用中使用的相同名称。 createViewInstance 函数负责实例化一个新的 ReactWebView。 随后,ViewManager 需要定义如何根据 React 组件的属性更新原生视图。在这个示例中,你需要决定如何处理 React 设置在 WebView 上的 sourceURL 属性。 最后,如果组件可以触发事件,你需要通过重写 getExportedCustomBubblingEventTypeConstants 来映射冒泡事件,或通过重写 getExportedCustomDirectEventTypeConstants 来映射直接事件。 4. 编写 ReactWebViewPackage 与原生模块一样,原生组件也需要实现 ReactPackage 类。这是一个可用于在 React Native 运行时中注册组件的对象。 ReactWebViewPackage 的代码如下: JavaKotlinDemo/android/src/main/java/com/webview/ReactWebViewPackage.javapackage com.webview;

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 com.facebook.react.uimanager.ViewManager;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ReactWebViewPackage extends BaseReactPackage {
@Override
public List> createViewManagers(ReactApplicationContext reactContext) {
return Collections.singletonList(new ReactWebViewManager(reactContext));
}

@Override
public NativeModule getModule(String s, ReactApplicationContext reactApplicationContext) {
if (ReactWebViewManager.REACT_CLASS.equals(s)) {
return new ReactWebViewManager(reactApplicationContext);
}
return null;
}

@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map getReactModuleInfos() {
Map map = new HashMap<>();
map.put(ReactWebViewManager.REACT_CLASS, new ReactModuleInfo(
ReactWebViewManager.REACT_CLASS, // 名称
ReactWebViewManager.REACT_CLASS, // 类名
false, // 是否允许覆盖现有模块
false, // 是否需要提前初始化
false, // 是否为 C++ 模块
true // 是否为 Turbo 模块
));
return map;
}
};
}
}
Demo/android/src/main/java/com/webview/ReactWebViewPackage.ktpackage com.webview

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 com.facebook.react.uimanager.ViewManager

class ReactWebViewPackage : BaseReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext): List> {
return listOf(ReactWebViewManager(reactContext))
}

override fun getModule(s: String, reactApplicationContext: ReactApplicationContext): NativeModule? {
when (s) {
ReactWebViewManager.REACT_CLASS -> ReactWebViewManager(reactApplicationContext)
}
return null
}

`ReactWebViewPackage` 继承了 `BaseReactPackage`,并实现了正确注册组件所需的全部方法。 `createViewManagers` 是工厂方法,用来创建管理自定义视图的 ViewManager。 `getModule` 根据 React Native 需要渲染的 View 返回对应的 ViewManager。 `getReactModuleInfoProvider` 提供模块在 runtime 中注册时所需的全部信息。 5. 在应用中注册 ReactWebViewPackage​ 最后,你需要在应用中注册 `ReactWebViewPackage`。做法是修改 `MainApplication` 文件,把 `ReactWebViewPackage` 添加到 `getPackages` 函数返回的包列表中。

override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)

override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
load()
}
}
}

接下来,编写用于渲染 WebView 的 iOS 平台代码。需要执行以下步骤:

运行 Codegen。

编写 RCTWebView 的代码。

在应用中注册 RCTWebView。

1. 运行 Codegen​

你可以手动运行 Codegen,但更简单的方式是使用即将演示该组件的应用来自动执行。 cd ios
bundle install
bundle exec pod install
重要的是,你会看到来自 Codegen 的日志输出,我们将使用这些日志在 Xcode 中构建 WebView 原生组件。 警告 注意不要将生成的代码提交到代码仓库。生成的代码特定于某个 React Native 版本。使用 npm peerDependencies 来限制与 React Native 版本的兼容性。

3. 编写 RCTWebView​

我们需要使用 Xcode 完成以下 5 个步骤来准备 iOS 项目:

打开由 CocoaPods 生成的 Xcode 工作区: cd ios
open Demo.xcworkspace

右键点击 app 文件夹,选择“新建组”(New Group),并将该组命名为 WebView。

在 WebView 组中,选择“新建”(New)→“从模板创建文件”(File from Template)。

使用 Objective-C 文件模板,并将其命名为 RCTWebView。

重复第 4 步,创建一个名为 RCTWebView.h 的头文件。

将 RCTWebView.m 重命名为 RCTWebView.mm,使其成为 Objective-C++ 文件。 Demo/iosPodfile
...
Demo
├── AppDelegate.swift
...
├── RCTWebView.h
└── RCTWebView.mm

创建头文件和实现文件后,即可开始实现它们。 以下是 RCTWebView.h 文件的代码,它声明了组件接口。 Demo/RCTWebView/RCTWebView.h#import
#import

NS_ASSUME_NONNULL_BEGIN

@interface RCTWebView : RCTViewComponentView

// 你可以在这里声明需要从此视图访问的原生方法

@end

NS_ASSUME_NONNULL_END

该类定义了一个继承自 RCTViewComponentView 的 RCTWebView。RCTViewComponentView 是 React Native 提供的所有原生组件的基类。 实现文件(RCTWebView.mm)的代码如下: Demo/RCTWebView/RCTWebView.mm#import "RCTWebView.h"

#import
#import
#import
#import
#import

using namespace facebook::react;

@interface RCTWebView ()
@end

@implementation RCTWebView {
NSURL * _sourceURL;
WKWebView * _webView;
}

-(instancetype)init
{
if(self = [super init]) {
_webView = [WKWebView new];
_webView.navigationDelegate = self;
[self addSubview:_webView];
}
return self;
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast(_props);
const auto &newViewProps = *std::static_pointer_cast(props);

// 在此处处理你的 props
if (oldViewProps.sourceURL != newViewProps.sourceURL) {
NSString *urlString = [NSString stringWithCString:newViewProps.sourceURL.c_str() encoding:NSUTF8StringEncoding];
_sourceURL = [NSURL URLWithString:urlString];
if ([self urlIsValid:newViewProps.sourceURL]) {
[_webView loadRequest:[NSURLRequest requestWithURL:_sourceURL]];
}
}

[super updateProps:props oldProps:oldProps];
}

-(void)layoutSubviews
{
[super layoutSubviews];
_webView.frame = self.bounds;

}

#pragma mark - WKNavigationDelegate

-(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
CustomWebViewEventEmitter::OnScriptLoaded result = CustomWebViewEventEmitter::OnScriptLoaded{CustomWebViewEventEmitter::OnScriptLoadedResult::Success};
self.eventEmitter.onScriptLoaded(result);
}

- (BOOL)urlIsValid:(std::string)propString
{
if (propString.length() > 0 && !_sourceURL) {
CustomWebViewEventEmitter::OnScriptLoaded result = CustomWebViewEventEmitter::OnScriptLoaded{CustomWebViewEventEmitter::OnScriptLoadedResult::Error};
这段代码用 Objective-C++ 编写,包含以下要点: @interface 实现了两个协议: - RCTCustomWebViewViewProtocol,由 Codegen 生成; - WKNavigationDelegate,由 WebKit 框架提供,用于处理 web view 的导航事件; - init 方法:实例化 WKWebView,将其加入 subviews,并设置 navigationDelegate; - updateProps 方法:当组件的 props 变化时,由 React Native 调用; - layoutSubviews 方法:定义自定义视图的布局方式; - webView:didFinishNavigation: 方法:用于处理 WKWebView 加载完成页面后的逻辑; - urlIsValid:(std::string)propString 方法:校验作为 prop 传入的 URL 是否有效; - eventEmitter 方法:用于获取强类型的 eventEmitter 实例的工具方法; - componentDescriptorProvider:返回由 Codegen 生成的 ComponentDescriptor; 添加 WebKit 框架 注意:这一步仅因为我们创建的是 Web 视图才需要。iOS 上的 web 组件需要链接 Apple 提供的 WebKit 框架。如果你的组件不需要访问 web 相关功能,可以跳过这一步。 Web view 需要使用 Apple 通过 Xcode 和设备自带框架之一——WebKit——提供的部分功能。 你可以在 RCTWebView.mm 中看到新添加的 #import 这行原生代码。 要在应用中链接 WebKit 框架,请按以下步骤操作: 在 Xcode 中点击你的项目 选择应用 target 选择 General 标签页 向下滚动找到 "Frameworks, Libraries, and Embedded Contents" 部分,点击 + 按钮 在搜索栏中输入 WebKit 过滤 选中 WebKit 框架 点击 Add。 3. 使用你的原生组件 最后,就可以在应用中使用新组件了。把生成的 App.tsx 更新为: Demo/App.tsximport React from 'react';
import {Alert, StyleSheet, View} from 'react-native';
import WebView from './specs/WebViewNativeComponent'; function App(): React.JSX.Element { return ( { Alert.alert('Page Loaded'); }} /> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', alignContent: 'center', }, webview: { width: '100%', height: '100%', }, }); export default App; 这段代码创建一个应用,使用我们新创建的 WebView 组件加载 react.dev 网站。当网页加载完成时,应用会弹出一个提示框。 4. 使用 WebView 组件运行你的 App Android iOS yarn run android yarn run ios Android iOS

评论 (0)