React Native 新架构原生组件开发指南

本文介绍如何基于 Fabric(新架构渲染器)开发原生 UI 组件的基础流程。涵盖属性、事件、方法等。

内容基于 react-native-troikaopen in new window 仓库中的 packages/ 下的 bottom-sheetimage-croppull-to-refreshwheel-pickernested-scroll 等实现整理。

本文不涉及自定义测量、布局等内容,如有需要,请参阅 react-native-troikaopen in new window 和官方源码。

建议:以独立库的方式编写原生组件,便于复用与发布。

可使用 react-native-create-libopen in new window 一行命令生成库脚手架(支持新架构与 Monorepo),再按本文配置与实现业务逻辑。

本文中工程配置等章节均按独立库的目录与约定编写;若在主工程内直接开发,需按主工程路径与包名做对应调整。

前置条件

  • 项目已启用新架构(Fabric)
  • 熟悉 React Native 与 TypeScript

一、TypeScript 规范(Codegen 入口)

规范文件命名约定:NativeComponent 结尾(如 ActivityIndicatorNativeComponent.ts),Codegen 会扫描 jsSrcsDir 下这类文件生成 C++/Java/ObjC 代码。

1.1 基本结构

// XxxNativeComponent.ts
import type { CodegenTypes, HostComponent, ViewProps } from 'react-native';
import { codegenNativeComponent } from 'react-native';

export interface NativeProps extends ViewProps {
  // 属性与事件见下文
}

export default codegenNativeComponent<NativeProps>('ComponentName') as HostComponent<NativeProps>;
  • 'ComponentName':与原生侧注册的组件名一致(Android getName()、iOS componentDescriptorProvider 对应组件)。
  • 继承 ViewProps 可得到 styletestID 等通用属性。

1.2 属性定义

  • 数值:使用 CodegenTypes.FloatCodegenTypes.DoubleCodegenTypes.Int32,不要用裸 number
  • 枚举/字面量:用 CodegenTypes.WithDefault<'a' | 'b', 'a'>
  • 颜色:ColorValue
  • 对象:会生成 C++ 结构体(iOS)和 ReadableMap(Android);属性名不要用 state(与生成的状态类型冲突)。

示例(来自 bottom-sheet 等):

export interface NativeProps extends ViewProps {
  peekHeight?: CodegenTypes.WithDefault<CodegenTypes.Int32, 200>;
  draggable?: CodegenTypes.WithDefault<boolean, true>;
  status?: CodegenTypes.WithDefault<'collapsed' | 'expanded' | 'hidden', 'collapsed'>;
}

复杂对象(来自 image-crop):

export interface ObjectRect {
  top: CodegenTypes.Float;
  left: CodegenTypes.Float;
  width: CodegenTypes.Float;
  height: CodegenTypes.Float;
}

export interface NativeProps extends ViewProps {
  objectRect?: ObjectRect;
}

1.3 事件(回调)

  • 事件 payload 类型命名建议:OnXxxEventPayload
  • 使用 CodegenTypes.DirectEventHandler<Payload>

示例:

export type OnCropEventPayload = { uri: string };
export type OnStateChangedEventPayload = { state: 'collapsed' | 'expanded' | 'hidden' };
export type OnSlideEventPayload = {
  progress: CodegenTypes.Float;
  offset: CodegenTypes.Float;
  expandedOffset: CodegenTypes.Float;
  collapsedOffset: CodegenTypes.Float;
};

export interface NativeProps extends ViewProps {
  onCrop?: CodegenTypes.DirectEventHandler<OnCropEventPayload>;
  onStateChanged?: CodegenTypes.DirectEventHandler<OnStateChangedEventPayload>;
  onSlide?: CodegenTypes.DirectEventHandler<OnSlideEventPayload>;
}

1.4 从 JS 调用原生方法(Commands)

需要“由 JS 调用的原生方法”时,使用 codegenNativeCommands(如 image-cropcrop()):

import { codegenNativeComponent, codegenNativeCommands } from 'react-native';

export interface NativeCommands {
  crop: (viewRef: React.ElementRef<HostComponent<NativeProps>>) => void;
  // 带参数示例:
  // setIndex: (viewRef: ..., index: number) => void;
}

export const Commands = codegenNativeCommands<NativeCommands>({
  supportedCommands: ['crop'],
});

export default codegenNativeComponent<NativeProps>('ImageCropView') as HostComponent<NativeProps>;

在业务组件中通过 ref + Commands.xxx(ref) 调用。

1.5 封装层(推荐)

不直接暴露 XxxNativeComponent,而是包一层以统一 API、跨平台或暴露 ref 方法(如 Commands):

// index.tsx
import React, { useRef, useImperativeHandle } from 'react';
import ImageCropNativeComponent, { Commands } from './ImageCropNativeComponent';
import type { NativeProps, OnCropEventPayload } from './ImageCropNativeComponent';

export interface ImageCropViewInstance {
  crop: () => void;
}

const ImageCropView = React.forwardRef<ImageCropViewInstance, NativeProps>((props, ref) => {
  const viewRef = useRef<React.ComponentRef<typeof ImageCropNativeComponent>>(null);
  useImperativeHandle(ref, () => ({
    crop: () => viewRef.current && Commands.crop(viewRef.current),
  }));
  return <ImageCropNativeComponent {...props} ref={viewRef} />;
});
export default ImageCropView;

二、工程配置

2.1 package.json — Codegen

{
  "codegenConfig": {
    "name": "bottomsheet",
    "type": "components",
    "jsSrcsDir": "src",
    "android": {
      "javaPackageName": "com.reactnative.bottomsheet"
    },
    "ios": {
      "componentProvider": {
        "BottomSheet": "RNBottomSheet"
      }
    }
  }
}
  • name:C++ 库/模块名,小写无连字符(如 bottomsheetimagecrop)。
  • type"components" 表示 Fabric 组件。
  • jsSrcsDir:扫描包含 NativeComponent 的 TS 的目录。
  • ios.componentProviderRN 组件名 → iOS 类名(如 "BottomSheet": "RNBottomSheet")。

2.2 react-native.config.js — 原生链接

module.exports = {
  dependency: {
    platforms: {
      android: {
        libraryName: 'bottomsheet',
        componentDescriptors: ['BottomSheetComponentDescriptor'],
      },
    },
  },
};
  • libraryName:与 codegenConfig.name 一致。
  • componentDescriptors:对应 C++ 的 XxxComponentDescriptor(纯 Java ViewManager 时由 Codegen 生成,此处列出即可)。

三、Android 实现

3.1 build.gradle 配置

修改build.gradle文件,使用 com.facebook.react 插件,并指定 codegen 生成目录:

apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'

android {
  namespace "com.reactnative.bottomsheet"
  sourceSets {
    main {
      java.srcDirs += ["${project.buildDir}/generated/source/codegen/java"]
    }
  }
}
dependencies {
  implementation 'com.facebook.react:react-native:+'
}

3.2 运行 Codegen

在主工程 android 目录执行:

./gradlew generateCodegenArtifactsFromSchema

生成物一般在:node_modules/你的包/android/build/generated/source/codegen/(java + jni)。

3.3 ViewManager + 接口与委托

新架构下 ViewManager 实现 Codegen 生成的 ManagerInterface,并由 ManagerDelegate 负责把属性更新转发到 setter:

public class BottomSheetManager extends SimpleViewManager<BottomSheetView>
    implements BottomSheetManagerInterface<BottomSheetView> {

  public static final String REACT_CLASS = "BottomSheet";

  private final BottomSheetManagerDelegate<BottomSheetView, BottomSheetManager> mDelegate =
      new BottomSheetManagerDelegate<>(this);

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

  @Override
  protected ViewManagerDelegate<BottomSheetView> getDelegate() {
    return mDelegate;
  }

  @NonNull
  @Override
  protected BottomSheetView createViewInstance(@NonNull ThemedReactContext reactContext) {
    return new BottomSheetView(reactContext);
  }

  @Override
  public void setPeekHeight(BottomSheetView view, int peekHeight) {
    view.setPeekHeight(peekHeight);
  }

  @Override
  public void setDraggable(BottomSheetView view, boolean draggable) {
    view.setDraggable(draggable);
  }

  // ... 其它 setXxx 实现
}
  • 属性由原来的 @ReactProp 改为实现接口方法(如 setPeekHeightsetDraggable)。

3.4 事件:注册与派发

与 iOS 的 EventEmitter(4.6)对应,Android 需要:注册事件名,并在合适时机派发事件

(1)定义事件类

为每个 TS 中声明的 DirectEventHandler 编写一个继承 Event<YourEvent> 的类,约定:getEventName() 返回 "topXxx"(与 Codegen 约定一致),JS 侧回调名为 "onXxx"

// OnCropEvent.java(参考 image-crop)
import com.facebook.react.uimanager.events.Event;

public class OnCropEvent extends Event<OnCropEvent> {
  public static final String Name = "topCrop";
  public static final String JSEventName = "onCrop";

  private final String uri;

  public OnCropEvent(int surfaceId, int viewTag, String uri) {
    super(surfaceId, viewTag);
    this.uri = uri;
  }

  @Override
  public String getEventName() {
    return Name;
  }

  @Override
  protected WritableMap getEventData() {
    WritableMap event = Arguments.createMap();
    event.putString("uri", uri);
    return event;
  }
}

(2)在 ViewManager 中注册

重写 getExportedCustomDirectEventTypeConstants(),把 topXxx 映射到 JS 的 onXxx

@Nullable
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
  return MapBuilder.of(
    OnCropEvent.Name, MapBuilder.of("registrationName", OnCropEvent.JSEventName)
  );
}

若有多个事件,用 MapBuilder.builder().put(...).put(...).build() 等一并注册。

(3)在 View 中派发事件

在需要回调 JS 的时机(如裁剪完成、状态变化),获取 surfaceIdviewIdEventDispatcher,构造事件并派发:

private void onCropped(String uri) {
  int surfaceId = UIManagerHelper.getSurfaceId(getContext());
  int viewId = getId();
  EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag((ReactContext) getContext(), viewId);
  if (eventDispatcher != null) {
    OnCropEvent event = new OnCropEvent(surfaceId, viewId, uri);
    eventDispatcher.dispatchEvent(event);
  }
}

可在 View 中封装 sendEvent(Event<?> event),内部用 getId()UIManagerHelper.getEventDispatcherForReactTag(...) 统一派发,各业务处只构造对应 Event 并调用 sendEvent

3.5 方法(Commands):实现与调用

与 iOS 的 handleCommand / RCTComponentViewHelpers(4.7)对应,Android 通过 ViewManager 实现接口中的命令方法,并在 View 中实现具体逻辑

在 TS 中通过 codegenNativeCommands 声明的方法(如 crop),Codegen 会在 XxxManagerInterface 中生成对应抽象方法。ViewManager 实现该接口并转发给 View 即可:

(1)在 ViewManager 中实现命令方法

@Override
public void crop(ImageCropView view) {
  view.crop();
}

(2)在 View 中实现具体逻辑

// ImageCropView.java
public void crop() {
  // 执行裁剪、保存等,完成后在合适时机调用 onCropped(uri) 派发 onCrop 事件
}

若命令带参数,接口会生成类似 setIndex(View view, int index),在 Manager 中实现并转发给 view.setIndex(index),在 View 中实现 setIndex 即可。JS 侧通过 Commands.xxx(ref, ...args) 调用时,会走到 Manager 的对应方法。

3.6 注册到应用

在应用自己的 PackagecreateViewManagers 中返回 new XxxManager()


四、iOS 实现

4.1 Podspec

Pod::Spec.new do |s|
  s.name         = "RNBottomSheet"
  s.version      = package["version"]
  s.source_files = "ios/**/*.{h,m,mm}"
  install_modules_dependencies(s)
end

install_modules_dependencies(s) 会拉取 Fabric/Codegen 等依赖。

4.2 运行 Codegen

在工程根目录执行:

bundle exec pod install

会为已配置的 Fabric 组件生成 C++ 与 ObjC 辅助代码。

4.3 组件类:继承 RCTViewComponentView

  • .m 改为 .mm(因为要引 C++ 生成的头文件)。
  • 头文件继承 RCTViewComponentView
// RNBottomSheet.h
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>

@interface RNBottomSheet : RCTViewComponentView
@end

4.4 必选:ComponentDescriptorProvider

.mm 中实现,让 Fabric 找到该组件的 Descriptor:

+ (void)load {
  [super load];
}

+ (ComponentDescriptorProvider)componentDescriptorProvider {
  return concreteComponentDescriptorProvider<BottomSheetComponentDescriptor>();
}

+ (BOOL)shouldBeRecycled {
  return NO;
}

需要 #import Codegen 生成的头文件,例如:

#import <react/renderer/components/bottomsheet/ComponentDescriptors.h>
#import <react/renderer/components/bottomsheet/EventEmitters.h>
#import <react/renderer/components/bottomsheet/Props.h>
#import <react/renderer/components/bottomsheet/RCTComponentViewHelpers.h>
using namespace facebook::react;

4.5 属性:updateProps

updateProps:oldProps: 中根据新 Props 更新视图属性:

- (void)updateProps:(const facebook::react::Props::Shared &)props
           oldProps:(const facebook::react::Props::Shared &)oldProps {
  const auto &oldViewProps = static_cast<const BottomSheetProps &>(*_props);
  const auto &newViewProps = static_cast<const BottomSheetProps &>(*props);

  if (newViewProps.draggable != oldViewProps.draggable) {
    self.draggable = newViewProps.draggable;
  }
  if (newViewProps.peekHeight != oldViewProps.peekHeight) {
    self.peekHeight = newViewProps.peekHeight;
  }
  if (newViewProps.status != oldViewProps.status) {
    self.status = newViewProps.status;
  }

  [super updateProps:props oldProps:oldProps];
}

4.6 事件:EventEmitter

获取当前组件的 EventEmitter 并调用 Codegen 生成的回调(如 onSlideonStateChanged):

- (const BottomSheetEventEmitter &)eventEmitter {
  return static_cast<const BottomSheetEventEmitter &>(*_eventEmitter);
}

- (void)dispatchOnSlide:(CGFloat)top {
  BottomSheetEventEmitter::OnSlide payload = {
    .progress = ...,
    .offset = ...,
    .expandedOffset = ...,
    .collapsedOffset = ...
  };
  [self eventEmitter].onSlide(payload);
}

4.7 Commands(从 JS 调用的方法)

若在 TS 里定义了 codegenNativeCommands,需实现对应协议并转发到 RCTComponentViewHelpers

@interface RNImageCropView () <RCTImageCropViewViewProtocol>
@end

- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
  RCTImageCropViewHandleCommand(self, commandName, args);
}

- (void)crop {
  // 实际裁剪逻辑
}

五、小结

  1. TS 规范:建 *NativeComponent.ts,包含 NativeProps、DirectEventHandler,可选 codegenNativeCommands、codegenNativeComponent。
  2. Codegen 配置:在 package.json 的 codegenConfig 中配置 name、type: components、android/ios。
  3. 链接配置:在 react-native.config.js 中配置 libraryName、componentDescriptors。
  4. Android:View + ViewManager 实现 *ManagerInterface + Delegate;事件:事件类 + getExportedCustomDirectEventTypeConstants + EventDispatcher.dispatchEvent;Commands:Manager 实现接口方法并转发给 View。
  5. iOS:Podspec → 运行 Codegen → RCTViewComponentView + componentDescriptorProvider + updateProps + eventEmitter + 可选 handleCommand。
  6. 封装:业务层用封装组件 + ref 暴露 Commands,不直接暴露 NativeComponent。
上次更新: