Fabric 鸿蒙原生组件自定义实现说明

本文档基于项目中的 FButton 组件实现,详细介绍如何在 React Native OpenHarmony 平台上开发 Fabric 架构的自定义原生组件。

目录

概述

Fabric 是 React Native 的新架构,它提供了更高效的渲染机制和更好的类型安全。在 OpenHarmony 平台上,自定义原生组件需要实现以下部分:

  1. TypeScript 规范:定义组件的接口和属性
  2. C++ 层实现:Props、EventEmitter、ComponentDescriptor
  3. 绑定层实现:JSIBinder、NapiBinder
  4. 事件处理:EventEmitRequestHandler
  5. ArkTS 组件:实际的 UI 实现
  6. Package 注册:将组件注册到系统中

前置条件

  • React Native 0.77.1 或更高版本
  • OpenHarmony 开发环境
  • 已配置 Codegen 工具
  • 了解 C++、TypeScript 和 ArkTS 基础语法

开发步骤

步骤1:定义 TypeScript 规范

src/components/ 目录下创建组件的 TypeScript 规范文件,例如 FButtonNativeComponent.tsx

// 导入 React Native 的基础类型和工具
import {HostComponent, ViewProps} from 'react-native';
// DirectEventHandler 用于定义直接事件处理器,事件会同步传递给 JavaScript 层
import type {DirectEventHandler} from 'react-native/Libraries/Types/CodegenTypes';
// codegenNativeComponent 是 Codegen 工具的核心函数,用于生成原生组件绑定代码
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';

// 定义事件数据结构 - 描述从原生层传递到 JavaScript 层的事件数据格式
type OnFButtonClickEventData = Readonly<{
  name: string;  // 事件中包含的 name 字段
}>;

// 定义组件属性接口 - 继承 ViewProps 获得所有 View 的基础属性(如 style、onLayout 等)
interface FButtonProps extends ViewProps {
  buttonText: string;  // 必填属性:按钮显示的文本
  onButtonClick?: DirectEventHandler<OnFButtonClickEventData>  // 可选属性:按钮点击事件处理器
}

// 导出组件 - codegenNativeComponent 会根据这个规范自动生成 C++ 绑定代码
// 'FButton' 是组件名称,必须与后续 C++ 和 ArkTS 中的组件名称保持一致
export const FButton = codegenNativeComponent<FButtonProps>('FButton') as HostComponent<FButtonProps>;

关键点说明:

  • ViewProps:继承自 React Native 的 View 属性,提供基础的样式和布局属性
  • DirectEventHandler:用于定义直接事件处理器,事件会直接传递给 JavaScript 层
  • codegenNativeComponent:Codegen 工具会基于此规范生成 C++ 代码
  • 组件名称 'FButton' 必须与后续 C++ 和 ArkTS 中的组件名称保持一致

步骤2:配置 Codegen

package.json 中配置 Codegen:

{
  "harmony": {
    "alias": "rtn-calculator",
    "codegenConfig": [
      {
        "version": 2,
        "specPaths": [
          "src/specs"
        ]
      }
    ]
  }
}

注意: 如果组件规范文件在 src/components/ 目录下,需要确保 Codegen 能够扫描到该目录。通常 Codegen 会自动扫描项目中的 codegenNativeComponent 调用。

步骤3:实现 C++ Props

harmony/entry/src/main/cpp/ 目录下创建 FButtonProps.h

#pragma once  // 防止头文件被重复包含
// JSI (JavaScript Interface) 用于 JavaScript 和 C++ 之间的通信
#include <jsi/jsi.h>
// ViewProps 提供所有 View 组件的基础属性(样式、布局等)
#include <react/renderer/components/view/ViewProps.h>
// PropsParserContext 用于解析属性时的上下文信息
#include <react/renderer/core/PropsParserContext.h>

namespace facebook {
    namespace react {
        // FButtonProps 类:定义组件的属性结构
        // final 关键字表示该类不能被继承
        // 继承自 ViewProps 获得所有 View 的基础属性
        class JSI_EXPORT FButtonProps final : public ViewProps {
            public:
                // 默认构造函数
                FButtonProps() = default;
                // 带参数的构造函数:从 RawProps 中解析属性值
                // context: 属性解析上下文
                // sourceProps: 源属性对象(用于属性合并)
                // rawProps: 原始属性对象(从 JavaScript 层传递过来的原始数据)
                FButtonProps(const PropsParserContext &context, 
                            const FButtonProps &sourceProps, 
                            const RawProps &rawProps);
            #pragma mark - FButtonProps
                // 自定义属性:按钮文本,默认值为空字符串
                std::string buttonText{""};
        };
    }
}

创建对应的实现文件 FButtonProps.cpp

#include <react/renderer/components/rncore/Props.h>
#include <react/renderer/core/PropsParserContext.h>
// propsConversions.h 提供了属性转换工具函数
#include <react/renderer/core/propsConversions.h>
#include "FButtonProps.h"

namespace facebook {
    namespace react {
        // FButtonProps 构造函数的实现
        FButtonProps::FButtonProps(
            const PropsParserContext &context,      // 属性解析上下文
            const FButtonProps &sourceProps,         // 源属性(用于属性继承和合并)
            const RawProps &rawProps                // 原始属性(从 JavaScript 传递的原始数据)
        ): ViewProps(context, sourceProps, rawProps),  // 先初始化父类 ViewProps
        // convertRawProp 函数从 rawProps 中提取 "buttonText" 属性
        // 参数说明:
        //   - context: 解析上下文
        //   - rawProps: 原始属性对象
        //   - "buttonText": 要提取的属性名称(必须与 TypeScript 规范中的属性名一致)
        //   - sourceProps.buttonText: 如果 rawProps 中没有该属性,使用源属性的值
        //   - {""}: 如果源属性也没有,使用默认值空字符串
        buttonText(convertRawProp(context, rawProps, "buttonText", sourceProps.buttonText, {""}))
            {}
    }
}

关键点说明:

  • FButtonProps 继承自 ViewProps,获得所有 View 的基础属性
  • convertRawProp 用于从 RawProps 中提取并转换属性值
  • 最后一个参数 {""} 是默认值
  • 所有属性都需要在头文件中声明,并在构造函数中初始化

步骤4:实现 C++ EventEmitter

创建 FButtonEventEmitter.h

#pragma once  // 防止头文件重复包含
// ViewEventEmitter 提供事件发送的基础功能
#include <react/renderer/components/view/ViewEventEmitter.h>
#include <jsi/jsi.h>

namespace facebook {
    namespace react {
        // FButtonEventEmitter 类:负责向 JavaScript 层发送事件
        // 继承自 ViewEventEmitter 获得基础的事件发送能力
        class JSI_EXPORT FButtonEventEmitter : public ViewEventEmitter {
            public:
                // 使用父类的构造函数
                using ViewEventEmitter::ViewEventEmitter;
                
                // 定义 OnButtonClick 事件的数据结构
                struct OnButtonClick {
                    std::string name;  // 事件数据:按钮名称
                };
    
                // 发送按钮点击事件的方法
                // value: 包含事件数据的结构体
                // const 表示该方法不会修改对象状态
                void onButtonClick(OnButtonClick value) const;
        };
    }
}

创建对应的实现文件 FButtonEventEmitter.cpp

#include "FButtonEventEmitter.h"

namespace facebook {
    namespace react {
        // onButtonClick 方法的实现:将事件发送到 JavaScript 层
        void facebook::react::FButtonEventEmitter::onButtonClick(OnButtonClick event) const { 
            // dispatchEvent 是父类提供的方法,用于向 JavaScript 层发送事件
            // 第一个参数 "onButtonClick" 是事件名称,必须与 TypeScript 规范中的事件名称一致
            // 第二个参数是一个 lambda 函数,用于构建事件数据对象
            dispatchEvent("onButtonClick", [event = std::move(event)](jsi::Runtime &runtime) {
                // 创建一个 JSI Object 作为事件数据载体
                auto payload = jsi::Object(runtime);
                // 将事件数据添加到 payload 中
                // setProperty 的参数:runtime、属性名、属性值
                payload.setProperty(runtime, "name", event.name);
                // 返回构建好的事件数据对象,这个对象会被传递给 JavaScript 层的 onButtonClick 处理器
                return payload;
            });
        }
    }
}

关键点说明:

  • FButtonEventEmitter 继承自 ViewEventEmitter
  • dispatchEvent 用于向 JavaScript 层发送事件
  • 事件名称 "onButtonClick" 必须与 TypeScript 规范中的事件名称一致
  • jsi::Object 用于构建事件数据,传递给 JavaScript 层

步骤5:实现 ComponentDescriptor

创建 FButtonComponentDescriptor.h

#pragma once
// ConcreteComponentDescriptor 用于描述组件的元信息
#include <react/renderer/core/ConcreteComponentDescriptor.h>
// ConcreteViewShadowNode 是 Fabric 架构中的 ShadowNode,用于管理组件的渲染树
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include "FButtonEventEmitter.h"
#include "FButtonProps.h"

namespace facebook{
    namespace react {
        // 组件名称常量,必须与 TypeScript 规范中的组件名称一致
        extern const char FButtonComponentName[] = "FButton";
        
        // 定义 FButtonShadowNode 类型别名
        // ConcreteViewShadowNode 是模板类,需要三个参数:
        //   1. 组件名称
        //   2. Props 类型(FButtonProps)
        //   3. EventEmitter 类型(FButtonEventEmitter)
        // ShadowNode 是 Fabric 架构的核心,用于管理组件的状态和属性
        using FButtonShadowNode = ConcreteViewShadowNode<FButtonComponentName, FButtonProps, FButtonEventEmitter>;
        
        // 定义 FButtonComponentDescriptor 类型别名
        // ComponentDescriptor 用于描述组件的元信息,系统通过它来创建和管理组件实例
        using FButtonComponentDescriptor = ConcreteComponentDescriptor<FButtonShadowNode>;
    }
}

关键点说明:

  • FButtonComponentName 必须与 TypeScript 规范中的组件名称一致
  • ConcreteViewShadowNode 是 Fabric 架构中的 ShadowNode,用于管理组件的渲染树
  • ConcreteComponentDescriptor 用于描述组件的元信息

步骤6:实现 JSIBinder

创建 FButtonJSIBinder.h

#pragma once
// ViewComponentJSIBinder 提供 View 组件的 JSI 绑定基础功能
#include "RNOHCorePackage/ComponentBinders/ViewComponentJSIBinder.h"

namespace rnoh {
// FButtonJSIBinder 类:负责在 JSI (JavaScript Interface) 层绑定组件
// JSI 是 React Native 新架构中 JavaScript 和 C++ 之间的直接通信接口
class FButtonJSIBinder : public ViewComponentJSIBinder {
  // 重写 createNativeProps 方法:定义组件的属性类型信息
  // 这个方法返回一个 JSI Object,描述组件有哪些属性以及它们的类型
  facebook::jsi::Object createNativeProps(facebook::jsi::Runtime& rt) override {
    // 先获取父类(ViewComponentJSIBinder)定义的属性
    auto object = ViewComponentJSIBinder::createNativeProps(rt);
    // 添加自定义属性 "buttonText",类型为 "string"
    // 这个属性名必须与 TypeScript 规范中的属性名一致
    object.setProperty(rt, "buttonText", "string");
    return object;
  }

  // 重写 createDirectEventTypes 方法:定义组件支持的事件类型
  // 直接事件(Direct Event)会同步传递给 JavaScript 层
  facebook::jsi::Object createDirectEventTypes(
      facebook::jsi::Runtime& rt) override {
    // 创建一个 JSI Object 来存储事件类型映射
    facebook::jsi::Object events(rt);
    // 事件名称映射规则:
    //   - JSI 层使用 "top" + 首字母大写的属性名格式:topButtonClick
    //   - JavaScript 层使用驼峰命名:onButtonClick
    // createDirectEvent 创建一个直接事件类型描述
    events.setProperty(rt, "topButtonClick", createDirectEvent(rt, "onButtonClick"));
    return events;
  }
};
} // namespace rnoh

关键点说明:

  • FButtonJSIBinder 继承自 ViewComponentJSIBinder
  • createNativeProps 定义组件的属性类型,用于 JSI 绑定
  • createDirectEventTypes 定义直接事件类型
  • 事件名称格式:"top" + 首字母大写的属性名,例如 "topButtonClick" 对应 "onButtonClick"
  • 属性类型使用字符串表示,如 "string""number""boolean"

步骤7:实现 NapiBinder

创建 FButtonNapiBinder.h

// ViewComponentNapiBinder 提供 View 组件的 NAPI 绑定基础功能
// NAPI (Node-API) 是 OpenHarmony 中用于 C++ 和 ArkTS 之间通信的接口
#include "RNOHCorePackage/ComponentBinders/ViewComponentNapiBinder.h"
#include "FButtonProps.h"

namespace rnoh {
// FButtonNapiBinder 类:负责将 C++ 层的 Props 转换为 NAPI 值,传递给 ArkTS 层
class FButtonNapiBinder : public ViewComponentNapiBinder {
public:
    // 重写 createProps 方法:将 C++ 的 Props 转换为 NAPI 值
    // env: NAPI 环境对象
    // shadowView: 包含组件属性和状态的 ShadowView 对象
    napi_value createProps(napi_env env, facebook::react::ShadowView const shadowView) override {
        // 先获取父类(ViewComponentNapiBinder)转换的基础 View 属性
        napi_value napiViewProps = ViewComponentNapiBinder::createProps(env, shadowView);
        
        // 尝试将 shadowView.props 转换为 FButtonProps 类型
        // std::dynamic_pointer_cast 是安全的类型转换,如果类型不匹配会返回 nullptr
        if (auto props = std::dynamic_pointer_cast<const facebook::react::FButtonProps>(shadowView.props)) {
            // 使用 ArkJS 工具类简化 NAPI 操作
            // getObjectBuilder: 获取对象构建器,用于添加属性
            // addProperty: 添加自定义属性 "buttonText",值为 props->buttonText
            // build: 构建并返回最终的 NAPI 对象
            return ArkJS(env)
                .getObjectBuilder(napiViewProps)
                .addProperty("buttonText", props->buttonText) 
                .build();
        }
        // 如果类型转换失败,返回基础的 View 属性
        return napiViewProps;
    };
};
} // namespace rnoh

关键点说明:

  • FButtonNapiBinder 继承自 ViewComponentNapiBinder
  • createProps 方法将 C++ 的 Props 转换为 NAPI 值,传递给 ArkTS 层
  • 使用 ArkJS 工具类简化 NAPI 操作
  • 需要检查 props 的类型,确保类型安全

步骤8:实现 EventEmitRequestHandler

创建 FButtonEventEmitRequestHandler.h

#pragma once
#include <napi/native_api.h>  // NAPI 接口,用于 C++ 和 ArkTS 之间的通信
#include "FButtonEventEmitter.h"
#include "RNOH/ArkJS.h"  // ArkJS 工具类,简化 NAPI 操作
#include "RNOH/EventEmitRequestHandler.h"  // 事件处理基类

using namespace facebook;
namespace rnoh {
    // 定义事件类型枚举,用于区分不同的事件
    enum FButtonEventType {
        FBUTTON_ON_BUTTON_CLICK = 0  // 按钮点击事件
    };

    // 解析事件类型的辅助函数
    // arkJs: ArkJS 工具对象
    // eventObject: 从 ArkTS 层传递过来的事件对象(NAPI 值)
    // eventName: 事件名称(通常是组件名称)
    FButtonEventType getFButtonEventType(ArkJS &arkJs, napi_value eventObject, std::string eventName) {
        auto eventType = eventName;
        LOG(INFO) << "getFButtonEventType = " + eventType;
        
        // 首先检查事件名称是否是 "FButton"(组件名称)
        if (eventType == "FButton") {
            // 从事件对象中获取 "type" 字段,这个字段在 ArkTS 组件中设置
            std::string type = arkJs.getString(arkJs.getObjectProperty(eventObject, "type"));
            
            // 根据 type 字段判断具体的事件类型
            if (type == "onButtonClick") {
                return FButtonEventType::FBUTTON_ON_BUTTON_CLICK;
            } else {
                throw std::runtime_error("Unknown FButton event type");
            }
        } else {
            throw std::runtime_error("Unknown component event type");
        }
    }

    // FButtonEventEmitRequestHandler 类:处理从 ArkTS 层发送到 C++ 层的事件
    // 当 ArkTS 组件调用 emitComponentEvent 时,事件会通过这个处理器传递到 C++ 层
    class FButtonEventEmitRequestHandler : public EventEmitRequestHandler {
        public:
        // 重写 handleEvent 方法:处理事件
        // ctx: 事件处理上下文,包含事件数据、组件 tag 等信息
        void handleEvent(EventEmitRequestHandler::Context const &ctx) override {
            // 创建 ArkJS 工具对象
            ArkJS arkJs(ctx.env);
            
            // 从 ShadowView 注册表中获取对应 tag 的 EventEmitter
            // ctx.tag 是组件的唯一标识
            auto eventEmitter = ctx.shadowViewRegistry->getEventEmitter<react::FButtonEventEmitter>(ctx.tag);
            
            // 如果找不到 EventEmitter,直接返回(可能组件已被销毁)
            if (eventEmitter == nullptr) {
                return;
            }

            // 解析事件类型
            // ctx.payload: 事件数据对象(从 ArkTS 层传递)
            // ctx.eventName: 事件名称(通常是组件名称)
            FButtonEventType type = getFButtonEventType(arkJs, ctx.payload, ctx.eventName);
            
            // 根据事件类型处理不同的事件
            switch (type) {
                case FBUTTON_ON_BUTTON_CLICK: {
                    // 从事件数据中提取 "name" 字段
                    std::string name = arkJs.getString(arkJs.getObjectProperty(ctx.payload, "name"));
                    LOG(INFO) << "FButtonEventEmitter OnButtonClick " << name;

                    // 构建事件数据结构
                    react::FButtonEventEmitter::OnButtonClick event = {name};
                    // 调用 EventEmitter 的方法,将事件发送到 JavaScript 层
                    eventEmitter->onButtonClick(event);
                    break;
                }
                default:
                    break;
            }
        }
    };
}

关键点说明:

  • EventEmitRequestHandler 用于处理从 ArkTS 层发送到 C++ 层的事件
  • getFButtonEventType 函数解析事件类型,根据 eventName 和事件对象中的 type 字段判断
  • handleEvent 方法处理事件,从 ctx.payload 中提取事件数据,然后调用 EventEmitter 的方法
  • ctx.shadowViewRegistry 用于获取对应 tag 的 EventEmitter

步骤9:实现 ArkTS 组件

harmony/entry/src/main/ets/components/ 目录下创建 FButtom.ets(注意:文件名可以是 FButton.ets,项目中使用了 FButtom.ets):

// 导入 React Native OpenHarmony 的核心类型和组件
import { Descriptor, RNOHContext, RNViewBase, ViewBaseProps } from "@rnoh/react-native-openharmony";

// 定义组件的属性接口,继承 ViewBaseProps 获得基础属性
export interface FButtonProps extends ViewBaseProps {
  buttonText: string;  // 按钮显示的文本
}

// 定义组件描述符类型,Descriptor 是泛型类型,包含组件名称和属性类型
export type FButtonDescriptor = Descriptor<"FButton", FButtonProps>

// @Component 装饰器:标记这是一个 ArkTS 组件
@Component
export struct FButton {
  // 组件名称常量,必须与 TypeScript 和 C++ 中的组件名称一致
  static NAME: string = "FButton"
  
  // RNOHContext: React Native OpenHarmony 的上下文对象,提供各种服务
  // ! 表示这个属性必须被初始化
  ctx!: RNOHContext;
  
  // tag: 组件的唯一标识符,由系统分配,用于在注册表中查找组件
  // 注意:实际使用时,tag 应该从外部传入,而不是硬编码为 101
  tag: number = 101;

  // 描述符变化监听器的取消订阅函数
  private unregisterDescriptorChangeListener?: () => void = undefined;

  // @State 装饰器:标记为状态变量,当值改变时会触发 UI 更新
  // descriptor: 组件的描述符,包含 props、状态等信息
  @State
  private descriptor: FButtonDescriptor = {} as FButtonDescriptor
  
  // buttonText: 按钮显示的文本,从 props 中提取
  @State
  private buttonText: string = "";

  // bgColor: 按钮背景颜色,可以通过命令动态修改
  @State
  private bgColor: string = "#ffae00";

  // aboutToAppear: 组件生命周期方法,在组件即将显示时调用
  aboutToAppear(): void {
    // 从描述符注册表中获取组件的描述符
    // getDescriptor 根据 tag 查找对应的组件描述符
    this.descriptor = this.ctx.descriptorRegistry.getDescriptor<FButtonDescriptor>(this.tag);
    
    // 订阅描述符变化,当 JavaScript 层更新 props 时,会触发回调
    // subscribeToDescriptorChanges 返回一个取消订阅的函数
    this.unregisterDescriptorChangeListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag, (newDescriptor) => {
      // 更新描述符
      this.descriptor = newDescriptor as FButtonDescriptor;
      // 从新的描述符中提取 buttonText 属性并更新状态
      // rawProps 是原始属性对象,包含从 JavaScript 层传递的所有属性
      this.buttonText = (newDescriptor.rawProps as FButtonProps).buttonText;
    });
    
    // 初始化时从描述符中提取 buttonText 属性
    this.buttonText = (this.descriptor.rawProps as FButtonProps).buttonText;
    
    // 注册命令回调:接收来自 JavaScript 层的命令
    // registerCommandCallback 的参数:
    //   - this.descriptor.tag: 组件的 tag(使用 descriptor.tag 而不是 this.tag)
    //   - 回调函数:接收命令名称和参数
    // 当 JavaScript 调用 UIManager.dispatchViewManagerCommand 时,会触发这个回调
    this.ctx.componentCommandReceiver.registerCommandCallback(this.descriptor.tag, (cmd, args: ESObject) => {
      // 处理 "updateBgColor" 命令
      if (cmd === "updateBgColor") {
        // args 是一个数组,第一个元素是新的背景颜色值
        this.bgColor = (args as Array<ESObject>)[0] as string;
      }
    })
  }

  // aboutToDisappear: 组件生命周期方法,在组件即将消失时调用
  aboutToDisappear(): void {
    // 取消订阅描述符变化监听,避免内存泄漏
    this.unregisterDescriptorChangeListener?.();
  }

  // build: 组件的构建方法,定义组件的 UI 结构
  build() {
    // RNViewBase: React Native 的基础 View 组件,必须包裹自定义组件
    // 它负责处理布局、样式等基础功能
    RNViewBase({ ctx: this.ctx, tag: this.tag }) {
      // 使用 ArkTS 的 Button 组件
      Button(this.buttonText)
        .width(200)      // 设置按钮宽度
        .height(48)      // 设置按钮高度
        .backgroundColor(this.bgColor)  // 设置背景颜色(可以从命令动态修改)
        .onClick(() => {
          // 按钮点击事件处理
          // emitComponentEvent: 向 React Native 层发送事件
          // 参数说明:
          //   - this.descriptor.tag: 组件的 tag
          //   - "FButton": 组件名称(必须与 C++ 中的组件名称一致)
          //   - 事件数据对象:
          //     * type: 事件类型,必须与 EventEmitRequestHandler 中解析的类型一致
          //     * name: 事件数据,会传递给 JavaScript 层的 onButtonClick 处理器
          this.ctx.rnInstance.emitComponentEvent(this.descriptor.tag, "FButton", {
            type: "onButtonClick",  // 事件类型
            name: this.buttonText    // 事件数据
          })
        })
    }
  }
}

关键点说明:

  • @Component 装饰器标记这是一个 ArkTS 组件
  • static NAME 必须与 C++ 和 TypeScript 中的组件名称一致
  • tag 是组件的唯一标识,由系统分配
  • descriptorRegistry 用于获取和订阅组件的描述符(包含 props)
  • componentCommandReceiver 用于接收来自 JavaScript 的命令(如 UIManager.dispatchViewManagerCommand
  • rnInstance.emitComponentEvent 用于发送事件,第一个参数是组件名称,第二个参数是事件数据
  • 事件数据中的 type 字段必须与 EventEmitRequestHandler 中解析的类型一致

步骤10:注册组件到 Package

创建或修改 FabricTurboModulePackage.h

#pragma once
#include "RNOH/Package.h"

namespace rnoh {
class FabricTurboModulePackage : public Package {
    public:
        FabricTurboModulePackage(Package::Context ctx) : Package(ctx) {}
    
        std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate() override ;
        
        std::vector<facebook::react::ComponentDescriptorProvider> createComponentDescriptorProviders() override;
        ComponentNapiBinderByString createComponentNapiBinderByName() override;
        ComponentJSIBinderByString createComponentJSIBinderByName() override;
        EventEmitRequestHandlers createEventEmitRequestHandlers() override;
    };
}

实现 FabricTurboModulePackage.cpp

#include "FabricTurboModulePackage.h"
#include "FButtonComponentDescriptor.h"      // 组件描述符
#include "FButtonEventEmitRequestHandler.h"  // 事件处理器
#include "FButtonJSIBinder.h"                // JSI 绑定器
#include "FButtonNapiBinder.h"               // NAPI 绑定器
#include "FabricTurboModuleSpec.h"           // TurboModule 规范(如果需要)

using namespace rnoh;
using namespace facebook;

// TurboModule 工厂委托类(如果 Package 中需要包含 TurboModule)
// TurboModule 用于提供 JavaScript 可以直接调用的原生方法
class FabricTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
    public:
        // 创建 TurboModule 实例
        // ctx: Package 上下文
        // name: TurboModule 名称
        SharedTurboModule createTurboModule(Context ctx, const std::string& name) const override {
            // 根据名称创建对应的 TurboModule
            if (name == "FabricTurboModule") {
                return std::make_shared<NativeFabricTurboModuleSpecJSI>(ctx, name);
            }
            return nullptr;  // 如果名称不匹配,返回 nullptr
        }
};

// 实现 createTurboModuleFactoryDelegate 方法
// 返回 TurboModule 工厂委托实例
std::unique_ptr<TurboModuleFactoryDelegate> FabricTurboModulePackage::createTurboModuleFactoryDelegate() {
    return std::make_unique<FabricTurboModuleFactoryDelegate>();
}

// 注册组件描述符提供者
// 这个方法告诉系统有哪些组件可以使用,系统会根据这个列表创建组件描述符
std::vector<facebook::react::ComponentDescriptorProvider> FabricTurboModulePackage::createComponentDescriptorProviders() {
    return {
        // concreteComponentDescriptorProvider 是一个模板函数,用于创建组件描述符提供者
        // 它接收 ComponentDescriptor 类型作为模板参数
        react::concreteComponentDescriptorProvider<react::FButtonComponentDescriptor>(),
        // 如果有多个组件,可以在这里继续添加
    };
}

// 注册事件处理器
// 当 ArkTS 组件发送事件时,系统会根据组件名称查找对应的事件处理器
EventEmitRequestHandlers rnoh::FabricTurboModulePackage::createEventEmitRequestHandlers() {
    // 返回一个包含所有事件处理器的向量
    // 每个事件处理器负责处理特定组件的事件
    return { std::make_shared<FButtonEventEmitRequestHandler>() };
}

// 注册 JSI Binder
// JSI Binder 用于在 JSI 层绑定组件,定义组件的属性和事件类型
ComponentJSIBinderByString rnoh::FabricTurboModulePackage::createComponentJSIBinderByName() {
    // 返回一个 map,key 是组件名称,value 是对应的 JSI Binder 实例
    // 组件名称 "FButton" 必须与 TypeScript 规范中的组件名称一致
    return { {"FButton", std::make_shared<FButtonJSIBinder>()} };
}

// 注册 NAPI Binder
// NAPI Binder 用于将 C++ 的 Props 转换为 NAPI 值,传递给 ArkTS 层
ComponentNapiBinderByString rnoh::FabricTurboModulePackage::createComponentNapiBinderByName() {
    // 返回一个 map,key 是组件名称,value 是对应的 NAPI Binder 实例
    return { {"FButton", std::make_shared<FButtonNapiBinder>()} };
}

关键点说明:

  • createComponentDescriptorProviders 注册组件的描述符提供者
  • createEventEmitRequestHandlers 注册事件处理器
  • createComponentJSIBinderByName 注册 JSI 绑定器
  • createComponentNapiBinderByName 注册 NAPI 绑定器
  • 组件名称作为 key,必须与组件定义中的名称一致

步骤11:配置 CMakeLists.txt

harmony/entry/src/main/cpp/CMakeLists.txt 中添加组件的源文件:

# add_library: 定义一个库目标
# rnoh_app: 库的名称
# SHARED: 表示这是一个动态链接库(.so 文件)
add_library(rnoh_app SHARED
    # 其他包的源文件(如果有)
    ${rtn_calculator_package_src}
    ${rtn_calculator_generated_dir_src}
    
    # Package 提供者:负责注册所有的 Package
    "./PackageProvider.cpp"
    
    # NAPI 桥接文件:提供 C++ 和 ArkTS 之间的通信桥梁
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
    
    # 添加 FButton 相关源文件
    # 注意:只需要添加 .cpp 文件,头文件(.h)不需要添加
    # 但需要确保头文件路径在 include_directories 中正确配置
    "./FButtonProps.cpp"              # Props 实现文件
    "./FButtonEventEmitter.cpp"       # EventEmitter 实现文件
    "./FabricTurboModulePackage.cpp"   # Package 实现文件
    "./FabricTurboModuleSpec.cpp"     # TurboModule 规范实现(如果需要)
)

关键点说明:

  • 确保所有 .cpp 文件都添加到 add_library
  • 头文件不需要添加,只需要确保包含路径正确

步骤12:在 Index.ets 中注册组件构建器

harmony/entry/src/main/ets/pages/Index.ets 中注册组件构建器:

// 导入自定义的 ArkTS 组件
import { FButton } from '../components/FButtom';

// 定义需要自定义构建的组件名称列表
// 这个数组列出了所有需要由 ArkTS 实现的 React Native 组件
// 系统会根据这个列表决定哪些组件需要调用自定义构建器
const arkTsComponentNames: Array<string> = ["FButton"]

// @Builder 装饰器:标记这是一个构建器函数
// 构建器函数用于根据组件名称动态创建对应的 ArkTS 组件
@Builder
export function buildCustomRNComponent(ctx: ComponentBuilderContext) {
  // ctx.componentName: 当前要构建的组件名称
  // ctx.tag: 系统为组件分配的唯一标识符
  // ctx.rnComponentContext: React Native OpenHarmony 的上下文对象
  
  // 根据组件名称判断应该创建哪个组件
  if (ctx.componentName === FButton.NAME) {
    // 创建 FButton 组件实例
    // 传递必要的参数:
    //   - ctx: React Native OpenHarmony 上下文
    //   - tag: 组件的唯一标识符
    FButton({
      ctx: ctx.rnComponentContext,
      tag: ctx.tag
    })
  }
  // 如果有其他自定义组件,可以继续添加 else if 分支
}

// wrapBuilder: 将构建器函数包装成系统需要的格式
// 这个包装后的构建器会被传递给 RNApp
const wrappedCustomRNComponentBuilder = wrapBuilder(buildCustomRNComponent)

然后在 RNApp 中使用:

RNApp({
  // ... 其他配置(如 JSBundleProvider、packages 等)
  
  // customComponentBuilder: 自定义组件构建器
  // 当系统遇到 arkTsComponentNames 中列出的组件时,会调用这个构建器来创建组件
  customComponentBuilder: wrappedCustomRNComponentBuilder,
  
  // ... 其他配置
})

关键点说明:

  • arkTsComponentNames 数组列出所有需要自定义构建的组件
  • buildCustomRNComponent 函数根据组件名称返回对应的 ArkTS 组件
  • ctx.tag 是系统分配的组件标识
  • ctx.rnComponentContext 是 React Native OpenHarmony 的上下文对象

步骤13:在 JavaScript 中使用

App.tsx 或任何 React 组件中使用:

import React, {useRef, useState} from 'react';
import {
  UIManager,        // UIManager 用于向原生组件发送命令
  findNodeHandle,  // findNodeHandle 用于获取组件的原生节点句柄
} from 'react-native';
import { FButton } from './src/components/FButtonNativeComponent';

function App(): React.JSX.Element {
  // useRef: 创建一个 ref 对象,用于获取组件引用
  // ref 是访问原生组件的唯一方式,用于发送命令
  const fButtonRef = useRef(null);
  
  // useState: 创建状态变量,用于存储按钮点击的结果
  const [result, setResult] = useState("");

  return (
    <SafeAreaView>
      <Text>Hello World {result}</Text>

      {/* FButton 组件使用示例 */}
      <FButton
        buttonText='FButtonTest11111'  // 设置按钮文本属性
        ref={fButtonRef}                // 绑定 ref,用于后续发送命令
        onButtonClick={(e) => {
          // 事件处理器:当按钮被点击时调用
          // e.nativeEvent: 包含从原生层传递过来的事件数据
          // e.nativeEvent.name: 事件数据中的 name 字段(在 EventEmitter 中设置)
          console.log("Button clicked:", e.nativeEvent.name);
          setResult(`click ${e.nativeEvent.name}`);
          
          // 发送命令到原生组件
          // dispatchViewManagerCommand 用于向原生组件发送命令
          // 参数说明:
          //   - findNodeHandle(fButtonRef.current): 获取组件的原生节点句柄
          //   - "updateBgColor": 命令名称,必须在 ArkTS 组件的 registerCommandCallback 中注册
          //   - ["#009688"]: 命令参数数组,会传递给 ArkTS 组件的命令回调函数
          UIManager.dispatchViewManagerCommand(
            findNodeHandle(fButtonRef.current),
            "updateBgColor",
            ["#009688"]  // 新的背景颜色值
          );
        }}
      />
    </SafeAreaView>

  );
}

关键点说明:

  • 使用 ref 获取组件引用,用于发送命令
  • onButtonClick 事件处理器接收 nativeEvent 对象
  • UIManager.dispatchViewManagerCommand 用于向原生组件发送命令
  • 命令名称 "updateBgColor" 必须在 ArkTS 组件的 registerCommandCallback 中处理

完整代码示例

文件结构

project/
├── src/
│   └── components/
│       └── FButtonNativeComponent.tsx    # TypeScript 规范
├── harmony/
│   └── entry/
│       └── src/
│           ├── main/
│           │   ├── cpp/
│           │   │   ├── FButtonProps.h/cpp
│           │   │   ├── FButtonEventEmitter.h/cpp
│           │   │   ├── FButtonComponentDescriptor.h
│           │   │   ├── FButtonJSIBinder.h
│           │   │   ├── FButtonNapiBinder.h
│           │   │   ├── FButtonEventEmitRequestHandler.h
│           │   │   ├── FabricTurboModulePackage.h/cpp
│           │   │   └── CMakeLists.txt
│           │   └── ets/
│           │       ├── components/
│           │       │   └── FButtom.ets
│           │       └── pages/
│           │           └── Index.ets

常见问题

1. 组件无法显示

可能原因:

  • 组件名称不一致(TypeScript、C++、ArkTS 中的名称必须一致)
  • 未在 Index.ets 中注册组件构建器
  • arkTsComponentNames 数组中未包含组件名称

解决方法:

  • 检查所有文件中的组件名称是否一致
  • 确保在 buildCustomRNComponent 中正确返回组件
  • 确保 RNApp 配置了 customComponentBuilder

2. 属性无法传递

可能原因:

  • NapiBinder 中未正确转换属性
  • ArkTS 组件中未正确读取 descriptor.rawProps

解决方法:

  • 检查 FButtonNapiBinder::createProps 是否正确添加属性
  • 检查 ArkTS 组件中是否正确从 descriptor.rawProps 读取属性
  • 确保订阅了描述符变化

3. 事件无法触发

可能原因:

  • 事件名称不一致
  • EventEmitRequestHandler 中事件类型解析错误
  • JSIBinder 中事件类型定义错误

解决方法:

  • 检查 TypeScript 规范中的事件名称
  • 检查 JSIBinder 中的事件映射(topButtonClick -> onButtonClick
  • 检查 EventEmitRequestHandler 中的事件类型判断逻辑
  • 检查 ArkTS 组件中 emitComponentEvent 的第一个参数(组件名称)和事件数据中的 type 字段

4. 命令无法执行

可能原因:

  • 命令名称不一致
  • ArkTS 组件中未注册命令回调

解决方法:

  • 确保 JavaScript 中的命令名称与 ArkTS 中注册的命令名称一致
  • 确保在 aboutToAppear 中正确注册了命令回调
  • 检查 tag 是否正确传递

5. Codegen 未生成代码

可能原因:

  • package.json 中未配置 Codegen
  • 规范文件路径不正确
  • Codegen 命令未执行

解决方法:

  • 检查 package.json 中的 harmony.codegenConfig 配置
  • 运行 npm run codegen-harmony 生成代码
  • 检查生成的文件是否在 harmony/entry/src/main/cpp/generated/ 目录下

6. 编译错误

可能原因:

  • CMakeLists.txt 中未添加源文件
  • 头文件包含路径错误
  • 命名空间使用错误

解决方法:

  • 检查 CMakeLists.txt 中是否包含所有 .cpp 文件
  • 检查 #include 路径是否正确
  • 确保命名空间使用正确(facebook::reactrnoh

总结

开发 Fabric 自定义组件需要实现多个层次的代码:

  1. TypeScript 层:定义组件接口和属性
  2. C++ 层:实现 Props、EventEmitter、ComponentDescriptor
  3. 绑定层:实现 JSIBinder 和 NapiBinder
  4. 事件层:实现 EventEmitRequestHandler
  5. UI 层:实现 ArkTS 组件
  6. 注册层:在 Package 中注册所有组件

每个层次都有其特定的职责,需要仔细实现和测试。建议按照本文档的步骤逐步实现,并在每个步骤后进行测试,确保功能正常。

参考资源

  • React Native 新架构文档
  • OpenHarmony 开发文档
  • 项目中的 FButton 实现示例
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐