众所周知 React Native 自 2021 年开始就宣布支持新的渲染器 Fabric 并开始逐渐迁移 新架构 ,据 RN 开发团队的计划逐步迁移并在 2024 年底将会将新架构上线到 RN 正式版默认启用,并逐步放弃对旧架构的支持(当然段时间不会立即将旧架构部分移除,会在一定时间内保持兼容,就如同燃油车到电车的过程,那自然现在的阶段就会出现油电混动车)所以作为依赖库的维护者,我们现在开发和迁移自己的依赖就是最好的时机。

本篇博客就是基于我在迁移一个之前的开源依赖库 (react-native-ad 这是最早做国内字节跳动穿山甲、腾讯优量汇、快手等平台聚合广告的开源 React Native 移动应用流量变现库),最近这段时间我一直在做依赖库的升级和适配,既然选择这个时间点来升级迭代,那必然是要同时适配新旧架构的,当然我也相信我们应该是最早一批国内 React Native 广告聚合库支持 RN 新架构的,欢迎有需要的小伙伴联系我们,我们也会积极参与社区活动帮助开发者们适配 RN 新架构。

当然,这篇博客其实更多的是面向 React Native 库开发者以及 IOS 开发者想往 React Native 跨平台开发发展和想深入学习原生开发的前端开发小伙伴们。因为篇幅所限,所以首篇博客内容会比较偏向于对比新旧架构对于 Native Component 的用法之间区别。

首先就是 JavaScript(Typescript)层,在旧架构中组件的定义和参数传递约束性其实是比较低的,几乎没有什么很强的定义映射,在旧架构中通常使用 requireNativeComponent 接口导出原生层实现的 Native Components 组件。

在新架构中则是引入了一个“新概念” Codegen,简单来说就是可以先定义 Typescript 的 Fabric Native Components 接口部分,然后由 React Native Codegen 首先生成 C++ 接口定义,随后库作者实现相应接口即可,在 IOS 中由于 ObjectC++ 能够比较友好的调用 C++ 的定义所以写起来现在就会比较舒服了,但是 Android 部分由于 Java 层和 C++ 的互调用还是需要走 JNI 所以除了 C++ 的接口部分之外还生成了 Java 与 C++ 之间的一些 JNI 桥接代码,当然篇幅所限 Android 部分的实现我下一篇博客接着讲,这篇博客主要讲 IOS 部分的实现。

通过下列代码可以对比出来新旧架构层在 Typescript(目前 Codegen 暂时仅支持  FlowTypeScript 定义用于定义接口)的定义上区别

// 旧架构
import React from 'react';
import { requireNativeComponent, ViewStyle } from 'react-native';

const FeedAdComponent = requireNativeComponent('FeedAd');
export interface FeedAdProps {
    codeid: string;
    style?: ViewStyle;
    onAdError?: Function;
}

const TxFeedAd = (props: FeedAdProps) => {
    return (
        <FeedAdComponent
            codeid={props.codeid}
            style={props.style}
            onAdError={props.onAdError}
        />
    )
}

// 新架构
import type { ViewProps } from 'react-native';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type {
  DirectEventHandler,
  Int32,
} from 'react-native/Libraries/Types/CodegenTypes'; // https://github.com/reactwg/react-native-new-architecture/discussions/27

export type AdFailWithErrorEventReply = Readonly<{
  code: Int32;
  message: string;
}>;

interface FeedViewProps extends ViewProps {
  codeId: string;
  onAdError?: DirectEventHandler<
    Readonly<AdFailWithErrorEventReply>
  >; // 广告加载、渲染失败
}

export default codegenNativeComponent<FeedViewProps>('FeedView');

通过上面 react-native-ad 中 Feed 广告的重构部分代码摘录就能看出来,首先旧架构中 requireNativeComponent 获取到的原生对象其实是一个 any 类型的 Comment 所以在导出组件时,通常需要另外定义一个用于导出合理类型声明的函数组件套用。而新架构上的话 codegenNativeComponent 直接通过定义类型泛型能够直接导出指定 props 类型的 Comment 组件。

其次,能够看出来新架构这边是更需要规范定义 props 参数类型的如回调函数类型需要使用 DirectEventHandler 或者 EventHandler 泛型定义,而在旧架构中对类型定义就比较随意定义了一个 Function 类型,除此之外细心的小伙伴们可能发现了在回调函数中的参数我这边用到了 Int32 这个类型,前端的小伙伴可能对这个类型就比较陌生了,还记得我们上面说到的 Codegen 会通过我们这里的 Typescript 定义的代码生成 C++ 接口代码吗?是的你没猜错,这里 Int32 就是对应 C++ 中的 Int32 数据类型,因此接口层定义肯定是不支持全部的 JS 类型的,仅支持部分数据类型映射(这里可以参考官方文档

当然,除了通过 Typescript 接口定义之外,还需要配置 Codegen(这边指的是旧的库项目中直接准备升级的话,如果是通过 react-native-bob 创建的新依赖库的话会默认配置好的)首先文件名是有规范性的,这是为了方便 Codegen 能够识别哪些 ts 代码文件需要生成 NativeSpec(C++ 接口定义代码)要求如下:

1、该文件必须命名为 <MODULE_NAME>NativeComponent ,使用 Flow 时带有 .js 或 .jsx 扩展名,或者 .ts 或 .tsx

2、该文件必须导出 HostComponent 对象

完成这些之后,以上面的 FeedView 广告组件为例,我们目前的文件夹结构应该如下

.
├── android
├── ios
├── package.json
├── src
│   ├── FeedViewNativeComponent.ts
│   └── index.tsx
├── tsconfig.json
└── yarn.lock

定义文件符合规范后,就是在 package.json 中配置 codegenConfig 部分了,其中 name 配置库的名称(可以简单理解为生成代码前缀部分),type 表示生成的代码类型(目前改参数有 componentsmodulesall 三个类型分别代表 Fabric Native Components、 Turbo Native Modules 和包含两种类型的全部),jsSrcsDir 为 Codegen 解析的 js 规范代码文件的相对路径。

{
  "name": "react-native-ad",
  "version": "1.0.0",
  "description": "This is an aggregate advertising package.",
  "private": true,
  "main": "lib/commonjs/index",
  "module": "lib/module/index",
  "types": "lib/typescript/src/index.d.ts",
  "react-native": "src/index",
  "source": "src/index",

  ……

  "codegenConfig": {
    "name": "RCTAdViewSpec",
    "type": "all",
    "jsSrcsDir": "src"
  }
}

对于 IOS 部分,还需要创建或者修改库项目的 .podspec 文件,该文件名需要根据 package.json 中 name 字段部分命名相同,例如我上面包名为 react-native-ad 那么在项目根目录下就是需要创建 react-native-ad.podspec 文件

require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
  s.name            = "react-native-ad"
  s.version         = package["version"]
  s.summary         = package["description"]
  s.description     = package["description"]
  s.homepage        = package["homepage"]
  s.license         = package["license"]
  s.platforms       = { :ios => "11.0" }
  s.author          = package["author"]
  s.source          = { :git => package["repository"], :tag => "#{s.version}" }

  s.source_files    = "ios/**/*.{h,m,mm,swift}"

  install_modules_dependencies(s)
end

主要部分是 install_modules_dependencies 函数,RN 团队对这个函数封装了新架构所需要的依赖项,且支持旧架构部分依赖项的适配。

接下来我将演示如何添加一个 FeedView 原生 IOS 组件并支持新旧架构

//
//  RCTFeedView.h
//  ios
//
//  Created by Bin on 2024/6/16.
//

// This guard prevent this file to be compiled in the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
// 新架构
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>

#ifndef RCTFeedViewNativeComponent_h
#define RCTFeedViewNativeComponent_h

NS_ASSUME_NONNULL_BEGIN

@interface RCTFeedView : RCTViewComponentView
@end

NS_ASSUME_NONNULL_END

#endif /* RCTFeedViewNativeComponent_h */
#else
// 旧架构
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>

#import "RCTFeedViewManager.h"

@interface RCTFeedView : UIView
- (instancetype)init:(RCTFeedViewManager*) manager;

- (void)setSlotId:(NSString*) slotId; // 设置广告位 ID
@end

#endif /* RCT_NEW_ARCH_ENABLED */

在 RCTFeedView.h 文件的定义上,旧架构的接口定义和之前写法并无太大差别,只是需要增加新架构的逻辑代码。此处能够看到 React Native 框架提供了一个 RCT_NEW_ARCH_ENABLED 的宏定义,通过这个宏我们就能够在代码文件中区分新旧架构需要的代码了。其次需要关注的点在于新架构中定义 View 接口目前是继承于 RCTViewComponentView 。

//
//  RCTFeedView.mm
//  ios
//
//  Created by Bin on 2024/6/16.
//
// react-native 新架构 comments

#ifdef RCT_NEW_ARCH_ENABLED
// 新架构
#import "RCTFeedView.h"

#import <Foundation/Foundation.h>
#import <react/renderer/components/RNADViewSpec/ComponentDescriptors.h>
#import <react/renderer/components/RNADViewSpec/EventEmitters.h>
#import <react/renderer/components/RNADViewSpec/Props.h>
#import <react/renderer/components/RNADViewSpec/RCTComponentViewHelpers.h>

#import "RCTFabricComponentsPlugins.h"
#import "Utils.h"

#import "RCTFeedViewManager.h"

using namespace facebook::react;

@interface RCTFeedView () <RCTFeedViewViewProtocol, NSObject>

@end

@implementation RCTFeedView {
}

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

- (instancetype)initWithFrame:(CGRect)frame
{
  if (self = [super initWithFrame:frame]) {
    static const auto defaultProps = std::make_shared<const RCTFeedViewProps>();
    _props = defaultProps;
  }

  return self;
}

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

    if (oldViewProps.slotId != newViewProps.slotId) {
        NSLog(@"tzmax: updateProps slotId change old:%s new:%s", &oldViewProps.slotId, &newViewProps.slotId);
    }

    [super updateProps:props oldProps:oldProps];
}

- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
    [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
    NSLog(@"tzmax: updateLayoutMetrics reload:%d state:%ld", &layoutMetrics.frame.size != &oldLayoutMetrics.frame.size ? YES : NO, (long)BUAdSDKManager.state);
}

Class<RCTComponentViewProtocol> RCTFeedViewCls(void)
{
    return RCTFeedView.class;
}

@end

#else
// 旧架构
#import "RCTFeedView.h"
#import <Foundation/Foundation.h>

#import <React/RCTLog.h>
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>

@implementation RCTFeedView {
}

- (instancetype)init:(RNADFeedViewManager*) manager {
    self = [super init];
    return self;
}

- (void)reactSetFrame:(CGRect)frame {
    [super reactSetFrame:frame];
    NSLog(@"tzmax: reactSetFrame Width: %f, Height: %f", frame.size.width , frame.size.height);
}

- (void)setSlotId:(NSString *)slotId {
    // 设置广告 ID
    NSLog(@"tzmax: setSlotId slotId: %s", slotId);
}
@end
#endif

我这里为了能够更加直观对比其中的实现差别,使用了 RCT_NEW_ARCH_ENABLED 宏定义判断新旧架构来使用不同代码部分(当然这会有很多冗余代码,在具体业务实现上可以考虑结合实现)。首先来看一下新旧架构中对组件 props 的获取方面的区别,如上述代码所示 slotId 字段的获取,能够看到上面的代码中在旧架构其实是直接定义一个 setSlotId 函数能感知到参数修改。

在新架构中 props 的变动会统一回调到 updateProps 函数中,然后通过 oldViewProps 和 newViewProps 对比新旧配置字段的更新来调用具体业务实现函数修改 View 参数。

其次就是如何得到 View 组件 style 或者说宽高变化时候的回调或新的宽高呢?在上述代码中能够看到在旧架构的实现中我用到了 reactSetFrame 函数来获取变化的组件宽高属性,而在新架构中提供了 updateLayoutMetrics 函数能够感知到组件位置变化或者宽高变化,和上面 props 的使用方式类似也是可以通过对比 oldLayoutMetrics 和 newLayoutMetrics 的属性变化实现对组件 style 的对比。

当然,一个 View 组件除了参数和属性之外还有一个事件回调需要处理,接下来我们先看看在旧架构中是如何向 JS 层回调事件的,后面接着看看在新架构中又应该如何回调事件,以此来对比两种架构之间有何差别。

// 旧架构

// RNFeedView.h
……
#import "RCTFeedViewManager.h"

@interface RCTFeedView : UIView
- (instancetype)init:(RCTFeedViewManager*) manager;
- (void)setSlotId:(NSString*) slotId; // 设置广告位 ID

// 定义回调事件
@property (nonatomic, copy) RCTBubblingEventBlock onAdViewDidLoad;
@property (nonatomic, copy) RCTBubblingEventBlock onAdViewDidClick;

@end
……

// RCTFeedViewManager.mm
……
RCT_EXPORT_MODULE(RCTFeedView)

// 绑定回调事件
RCT_EXPORT_VIEW_PROPERTY(onAdViewDidLoad, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onAdViewDidClick, RCTBubblingEventBlock)
……

能够看到,在旧架构中是首先在 RCTFeedView 接口声明中定义了两个事件函数 onAdViewDidLoad、onAdViewDidClick 最后在 RCTFeedViewManager 中使用 RCT_EXPORT_VIEW_PROPERTY 宏将这两个事件函数注册上,最后在业务逻辑中就直接调用 onAdViewDidLoad 和 onAdViewDidClick 函数且支持指定类型的参数,最终就能够将事件给回调到 JS 层中了。

// 新架构
……

#import <react/renderer/components/RNADViewSpec/ComponentDescriptors.h>
#import <react/renderer/components/RNADViewSpec/EventEmitters.h>
#import <react/renderer/components/RNADViewSpec/Props.h>
#import <react/renderer/components/RNADViewSpec/RCTComponentViewHelpers.h>

#import "RCTFabricComponentsPlugins.h"
#import "Utils.h"

#import "RCTFeedViewManager.h"

using namespace facebook::react;

@interface RCTFeedView () <RCTFeedViewViewProtocol, NSObject>

@end

@implementation RCTFeedView {
}

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

……

Class<RCTComponentViewProtocol> RCTFeedViewCls(void)
{
    return RCTFeedView.class;
}

// 广告已加载
- (void)adViewDidLoad:(UIView *)view {
    NSLog(@"tzmax: adViewDidLoad");
    if(_eventEmitter == nil) return;
    const auto &defaultEvent = *std::static_pointer_cast<RNADFeedViewEventEmitter const>(_eventEmitter);
    defaultEvent.onAdViewDidLoad({
      .width = static_cast<int>(view.frame.size.width),
      .height = static_cast<int>(view.frame.size.height)
    });
}

// 广告被点击
- (void)adDidClick:(UIView *)view {
    NSLog(@"tzmax: adDidClick");
    if(_eventEmitter == nil) return;
    const auto &defaultEvent = *std::static_pointer_cast<RNADFeedViewEventEmitter const>(_eventEmitter);
    defaultEvent.onAdViewDidClick({});
}

@end

能够看到,在新架构中 RCTFeedView 实际上是实现了 RCTFeedViewViewProtocol 协议,RCTFeedViewViewProtocol 协议是通过 JS 定义文件直接生成在 react/renderer/components/RNADViewSpec/RCTComponentViewHelpers.h 头文件中的。

而在 RCTFeedViewViewProtocol 的实现中是有一个 _eventEmitter 属性的,可以将其类型转换为 RNADFeedViewEventEmitter 就能够调用 JS 中声明且定义好的回调函数了,并且相比之前旧架构的回调函数,这里的函数以及回调参数类型都是定义好并且生成出来的,所以使用起来就非常方便了。

看到这里,相信你对 React Native 新架构的 Fabric Native Components 迁移和适配已经有了个基本的了解。当然如果你遇到了什么问题欢迎给我博客 https://bin.zmide.com/?p=1237 回复留言,我看到之后会尽力帮助你的,另外如果你是 React Native 开源库的贡献者在迁移过程中遇到问题或者需要我们帮助你进行迁移的话也可以通过博客中的联系方式与我取得联系,我会帮助你完成迁移工作。

当然,如果你喜欢这个系列的博客还请转发、点赞、投币一键三连(呸,不好意思走错片场了)支持博主和小伙伴们继续完成后续的 React Native 新架构系列博客。