React Native 新架构原生组件开发指南
本文介绍如何基于 Fabric(新架构渲染器)开发原生 UI 组件的基础流程。涵盖属性、事件、方法等。
内容基于 react-native-troika 仓库中的 packages/ 下的 bottom-sheet、image-crop、pull-to-refresh、wheel-picker、nested-scroll 等实现整理。
本文不涉及自定义测量、布局等内容,如有需要,请参阅 react-native-troika 和官方源码。
建议:以独立库的方式编写原生组件,便于复用与发布。
可使用 react-native-create-lib 一行命令生成库脚手架(支持新架构与 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':与原生侧注册的组件名一致(AndroidgetName()、iOScomponentDescriptorProvider对应组件)。- 继承
ViewProps可得到style、testID等通用属性。
1.2 属性定义
- 数值:使用
CodegenTypes.Float、CodegenTypes.Double、CodegenTypes.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-crop 的 crop()):
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++ 库/模块名,小写无连字符(如bottomsheet、imagecrop)。type:"components"表示 Fabric 组件。jsSrcsDir:扫描包含NativeComponent的 TS 的目录。ios.componentProvider:RN 组件名 → 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改为实现接口方法(如setPeekHeight、setDraggable)。
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 的时机(如裁剪完成、状态变化),获取 surfaceId、viewId 和 EventDispatcher,构造事件并派发:
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 注册到应用
在应用自己的 Package 的 createViewManagers 中返回 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 生成的回调(如 onSlide、onStateChanged):
- (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 {
// 实际裁剪逻辑
}
五、小结
- TS 规范:建
*NativeComponent.ts,包含 NativeProps、DirectEventHandler,可选 codegenNativeCommands、codegenNativeComponent。 - Codegen 配置:在 package.json 的 codegenConfig 中配置 name、type: components、android/ios。
- 链接配置:在 react-native.config.js 中配置 libraryName、componentDescriptors。
- Android:View + ViewManager 实现 *ManagerInterface + Delegate;事件:事件类 + getExportedCustomDirectEventTypeConstants + EventDispatcher.dispatchEvent;Commands:Manager 实现接口方法并转发给 View。
- iOS:Podspec → 运行 Codegen → RCTViewComponentView + componentDescriptorProvider + updateProps + eventEmitter + 可选 handleCommand。
- 封装:业务层用封装组件 + ref 暴露 Commands,不直接暴露 NativeComponent。