React Native 原生模块集成Turbo Modules

React Native 0.79 推荐的集成原生模块方式是Turbo Module,使用的是cli生成的项目:

bash 复制代码
# cli 项目代码生成
npx @react-native-community/cli@latest init TurboDemo --version 0.79.0

在尝试了一下文档示例,扩展了自己集成一个 ios 微信登录的功能,下面是ios的集成实践。

Turbo Module 主要是原生和 codegen 结合实现的

RTNCalculator示例模块结构

先简单介绍一下如何实现RTNCalculator(示例演示用),主要参考下面文档 react-native-new-architecture 的步骤。

1.在 app 根目录创建原生模块文件夹RTNCalculator

RTNCalculator 中,创建三个子文件夹: jsiosandroid

2.在 js 文件夹中创建NativeRTNCalculator.ts

代码如下:

ts 复制代码
import { TurboModule, TurboModuleRegistry } from "react-native";
import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes';

export interface Spec extends TurboModule {
  add(a: number, b: number): Promise<number>;
  readonly onValueChanged: EventEmitter<number>
}

export default TurboModuleRegistry.getEnforcing<Spec>("RTNCalculator");

3.模块配置

RTNCalculator 目录的根目录中创建 package.json 文件,主要看 codegenConfig 字段。

Codegen 配置由 codegenConfig 字段指定。它包含一个通过四个字段定义模块的对象:

  • name :库的名称。按照惯例,应该添加 Spec 后缀。
  • type :此包所含模块的类型。在本例中,它是一个 Turbo Native 模块;因此,要使用的值是 modules
  • jsSrcsDir :访问 Codegen 解析的 js 规范的相对路径。
  • android.javaPackageNameCodegen 生成的 Java 文件中使用的包
json 复制代码
{
    "name": "rtn-calculator",
    "version": "0.0.1",
    "description": "Add numbers with Turbo Native Modules",
    "react-native": "js/index",
    "source": "js/index",
    "files": [
      "js",
      "android",
      "ios",
      "rtn-calculator.podspec",
      "!android/build",
      "!ios/build",
      "!**/__tests__",
      "!**/__fixtures__",
      "!**/__mocks__"
    ],
    "keywords": ["react-native", "ios", "android"],
    "repository": "https://github.com/<your_github_handle>/rtn-calculator",
    "author": "<Your Name> <your_email@your_provider.com> (https://github.com/<your_github_handle>)",
    "license": "MIT",
    "bugs": {
      "url": "https://github.com/<your_github_handle>/rtn-calculator/issues"
    },
    "homepage": "https://github.com/<your_github_handle>/rtn-calculator#readme",
    "devDependencies": {},
    "peerDependencies": {
      "react": "*",
      "react-native": "*"
    },
    "codegenConfig": {
      "name": "RTNCalculatorSpec",
      "type": "modules",
      "jsSrcsDir": "js",
      "android": {
        "javaPackageName": "com.rtncalculator"
      }
    }
}

接着在RTNCalculator根目录创建 rtn-calculator.podspec(如果有用到 ios 的一些 cocopods 依赖,写在这个文件内)文件,代码如下:

ruby 复制代码
require "json"

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

Pod::Spec.new do |s|
  s.name            = "rtn-calculator"
  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

4.运行 codegen 自动生成原生代码

切换到app项目根目录,比如 TurboDemo,然后运行下面命令,会自动生成ios的一些代码,生成的代码目录是 RTNCalculator/generated

bash 复制代码
# codegen 生成原生代码
node ./node_modules/react-native/scripts/generate-codegen-artifacts.js \
  --targetPlatform ios \
  --path ./RTNCalculator \
  --outputPath RTNCalculator/generated/

5.编写ios代码

创建 RTNCalculator.h

Objective-C++ 复制代码
#import <RTNCalculatorSpec/RTNCalculatorSpec.h>

NS_ASSUME_NONNULL_BEGIN

@interface RTNCalculator : NativeRTNCalculatorSpecBase <NativeRTNCalculatorSpec>

@end

NS_ASSUME_NONNULL_END

创建 RTNCalculator.mm

Objective-C++ 复制代码
#import "RTNCalculator.h"

@implementation RTNCalculator

RCT_EXPORT_MODULE()

- (void)add:(double)a b:(double)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    NSNumber *result = [[NSNumber alloc] initWithDouble:a+b];
    resolve(result);
    [self emitOnValueChanged:result];
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeRTNCalculatorSpecJSI>(params);
}

@end

6.本地安装RTNCalculator模块

bash 复制代码
# app 项目根目录下运行
npm install ./RTNCalculator

7.安装 pod 依赖

切换到 app(app项目根目录)/ios 目录下,运行 pod install

8.给App.tsx添加ui操作示例

项目根目录下的App.tsx

tsx 复制代码
import React, {useState} from 'react';
import {
  Alert,
  Button,
  EventSubscription,
  StyleSheet,
  Text,
  TextInput,
  View,
} from 'react-native';

import RTNCalculator from 'rtn-calculator/js/NativeRTNCalculator';

function App(): React.JSX.Element {
  const [num1, setNum1] = useState<string>('3');
  const [num2, setNum2] = useState<string>('7');
  const [result, setResult] = useState<number | null>(null);
  const listenerSubscription = React.useRef<null | EventSubscription>(null);

  React.useEffect(() => {
    listenerSubscription.current = RTNCalculator.onValueChanged(data => {
      Alert.alert(`Result: ${data}`);
    });

    return () => {
      listenerSubscription.current?.remove();
      listenerSubscription.current = null;
    };
  }, []);

  return (
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <View style={styles.inputContainer}>
        <TextInput
          style={styles.input}
          value={num1}
          onChangeText={setNum1}
          keyboardType="numeric"
          placeholder="Enter first number"
        />
        <Text style={styles.operator}>+</Text>
        <TextInput
          style={styles.input}
          value={num2}
          onChangeText={setNum2}
          keyboardType="numeric"
          placeholder="Enter second number"
        />
        <Text style={styles.operator}>={result}</Text>
      </View>

      <Button
        title="Calculate"
        onPress={async () => {
          const value1 = parseFloat(num1) || 0;
          const value2 = parseFloat(num2) || 0;
          const value = await RTNCalculator.add(value1, value2);
          setResult(value ?? null);
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  inputContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 5,
    padding: 10,
    width: 100,
    textAlign: 'center',
  },
  operator: {
    fontSize: 24,
    marginHorizontal: 10,
  },
  result: {
    fontSize: 18,
    marginBottom: 20,
  },
});

export default App;

9.编译、运行

打开 ios 目录下 TurboDemo.xcworkspace,配置好红框内的 bundle 签名信息,然后运行,成功以后截图如下:

ios 微信 sdk 集成示例

操作步骤同上面RTNCalculator一样,创建模块文件夹为RTNWechat,下面贴出文件代码

1.RTNWechat/js/NativeRTNWechat.ts文件:

ts 复制代码
import { TurboModule, TurboModuleRegistry } from "react-native";
import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';

export interface Spec extends TurboModule {
  // 检查微信是否已安装
  isWXAppInstalled(): Promise<boolean>;
  
  // 微信登录
  login(): Promise<{
    code: string;
    state: string;
  }>;
  
  // 微信支付
  pay(params: {
    partnerId: string;
    prepayId: string;
    nonceStr: string;
    timeStamp: string;
    package: string;
    sign: string;
  }): Promise<{
    errCode: number;
    errStr: string;
  }>;
  
  // 微信分享(链接)
  shareLink(params: {
    title: string;
    description: string;
    thumbUrl: string;
    webpageUrl: string;
    scene: number; // 0: 会话 1: 朋友圈 2: 收藏
  }): Promise<{
    errCode: number;
    errStr: string;
  }>;
  
  // 微信分享(图片)
  shareImage(params: {
    image: string;
    scene: number; // 0: 会话 1: 朋友圈 2: 收藏
  }): Promise<{
    errCode: number;
    errStr: string;
  }>;
  
  // 事件监听器 - 处理从原生端返回的事件
  readonly onAuthResponse: EventEmitter<{
    errCode: number;
    errStr: string;
    code?: string;
    state?: string;
  }>;
}

export default TurboModuleRegistry.get<Spec>("RTNWechat") as Spec | null; 

2.RTNWechat/ios/RTNWechat.h文件:

Objective-C++ 复制代码
#import <RTNWechatSpec/RTNWechatSpec.h>

NS_ASSUME_NONNULL_BEGIN

@interface RTNWechat : NativeRTNWechatSpecBase <NativeRTNWechatSpec>

@end

NS_ASSUME_NONNULL_END 

3.RTNWechat/ios/RTNWechat.mm文件:

Objective-C++ 复制代码
#import "RTNWechat.h"
#import <WXApi.h>

@interface RTNWechat() <WXApiDelegate>
@end

@implementation RTNWechat

RCT_EXPORT_MODULE()

// 在这里初始化微信SDK
-(instancetype)init {
  self = [super init];
  if (self) {
    NSLog(@"RTNWechat模块初始化");
    
    BOOL result = [WXApi registerApp:@"wxd477edab60670232" universalLink:@"https://www.baidu.com"];

    NSLog(@"微信SDK自动注册结果: %@", result ? @"成功" : @"失败");
    
    // 检查微信是否已安装
    BOOL isInstalled = [WXApi isWXAppInstalled];
    NSLog(@"微信是否已安装: %@", isInstalled ? @"是" : @"否");
    
    // 检查微信是否支持API
    BOOL isSupport = [WXApi isWXAppSupportApi];
    NSLog(@"微信是否支持API: %@", isSupport ? @"是" : @"否");
  
  }
  return self;
}

// 确保在主线程执行UI操作
- (dispatch_queue_t)methodQueue {
  return dispatch_get_main_queue();
}

// 检查微信是否已安装
RCT_EXPORT_METHOD(isWXAppInstalled:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject) {
  BOOL isInstalled = [WXApi isWXAppInstalled];
  NSLog(@"RTNWechat isWXAppInstalled: %@", isInstalled ? @"已安装" : @"未安装");
  resolve(@(isInstalled));
}

// 登录
- (void)login:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  // 检查微信是否已安装
  if (![WXApi isWXAppInstalled]) {
    reject(@"login_error", @"WeChat is not installed", nil);
    return;
  }
  
  // 打印当前注册状态
  NSLog(@"WXApi isWXAppSupport: %d", [WXApi isWXAppInstalled]);
  
  SendAuthReq *req = [[SendAuthReq alloc] init];
  req.scope = @"snsapi_userinfo";
  
  req.state = @"wechat_login";
  
  [WXApi sendReq:req completion:^(BOOL success) {
    NSLog(@"WXApi sendReq result: %d", success);
    if (success) {
      // 请求发送成功,等待回调
      resolve(nil); // 注意:我们应该在这里resolve,否则promise会一直pending
    } else {
      reject(@"login_error", @"Failed to send login request", nil);
    }
  }];
}

// 支付
- (void)pay:(NSDictionary *)params resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  // 检查参数
  if (!params[@"partnerId"] || !params[@"prepayId"] || !params[@"nonceStr"] || 
      !params[@"timeStamp"] || !params[@"package"] || !params[@"sign"]) {
    reject(@"pay_error", @"Missing required payment parameters", nil);
    return;
  }
  
  // 检查微信是否已安装
  if (![WXApi isWXAppInstalled]) {
    reject(@"pay_error", @"WeChat is not installed", nil);
    return;
  }
  
  PayReq *req = [[PayReq alloc] init];
  req.partnerId = params[@"partnerId"];
  req.prepayId = params[@"prepayId"];
  req.nonceStr = params[@"nonceStr"];
  req.timeStamp = [params[@"timeStamp"] intValue];
  req.package = params[@"package"];
  req.sign = params[@"sign"];
  
  [WXApi sendReq:req completion:^(BOOL success) {
    if (success) {
      // 请求发送成功,等待回调
      resolve(nil); // 添加resolve解决Promise
    } else {
      reject(@"pay_error", @"Failed to send payment request", nil);
    }
  }];
}

// 分享链接
- (void)shareLink:(NSDictionary *)params resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  // 检查参数
  if (!params[@"webpageUrl"] || !params[@"title"]) {
    reject(@"share_error", @"Missing required share link parameters", nil);
    return;
  }
  
  // 检查微信是否已安装
  if (![WXApi isWXAppInstalled]) {
    reject(@"share_error", @"WeChat is not installed", nil);
    return;
  }
  
  WXWebpageObject *webpage = [WXWebpageObject object];
  webpage.webpageUrl = params[@"webpageUrl"];
  
  WXMediaMessage *message = [WXMediaMessage message];
  message.title = params[@"title"];
  message.description = params[@"description"] ?: @"";
  
  // 处理缩略图 - 异步加载以避免阻塞主线程
  if (params[@"thumbUrl"]) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:params[@"thumbUrl"]]];
      UIImage *thumbImage = imageData ? [UIImage imageWithData:imageData] : nil;
      
      dispatch_async(dispatch_get_main_queue(), ^{
        if (thumbImage) {
          [message setThumbImage:thumbImage];
        }
        
        message.mediaObject = webpage;
        
        SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init];
        req.bText = NO;
        req.message = message;
        req.scene = [params[@"scene"] intValue];
        
        [WXApi sendReq:req completion:^(BOOL success) {
          if (success) {
            // 请求发送成功,等待回调
            resolve(nil); // 添加resolve解决Promise
          } else {
            reject(@"share_error", @"Failed to send share request", nil);
          }
        }];
      });
    });
  } else {
    // 无缩略图的情况
    message.mediaObject = webpage;
    
    SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init];
    req.bText = NO;
    req.message = message;
    req.scene = [params[@"scene"] intValue];
    
    [WXApi sendReq:req completion:^(BOOL success) {
      if (success) {
        // 请求发送成功,等待回调
        resolve(nil); // 添加resolve解决Promise
      } else {
        reject(@"share_error", @"Failed to send share request", nil);
      }
    }];
  }
}

// 分享图片
- (void)shareImage:(NSDictionary *)params resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  // 检查参数
  if (!params[@"image"]) {
    reject(@"share_error", @"Missing image URL", nil);
    return;
  }
  
  // 检查微信是否已安装
  if (![WXApi isWXAppInstalled]) {
    reject(@"share_error", @"WeChat is not installed", nil);
    return;
  }
  
  // 异步加载图片
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:params[@"image"]]];
    
    if (!imageData) {
      dispatch_async(dispatch_get_main_queue(), ^{
        reject(@"share_error", @"Failed to load image", nil);
      });
      return;
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
      WXImageObject *imageObject = [WXImageObject object];
      imageObject.imageData = imageData;
      
      WXMediaMessage *message = [WXMediaMessage message];
      message.mediaObject = imageObject;
      
      // 创建缩略图
      UIImage *image = [UIImage imageWithData:imageData];
      UIImage *thumbImage = [self thumbnailWithImage:image size:CGSizeMake(100, 100)];
      [message setThumbImage:thumbImage];
      
      SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init];
      req.bText = NO;
      req.message = message;
      req.scene = [params[@"scene"] intValue];
      
      [WXApi sendReq:req completion:^(BOOL success) {
        if (success) {
          // 请求发送成功,等待回调
          resolve(nil); // 添加resolve解决Promise
        } else {
          reject(@"share_error", @"Failed to send share request", nil);
        }
      }];
    });
  });
}

// 创建缩略图方法
- (UIImage *)thumbnailWithImage:(UIImage *)image size:(CGSize)size {
  UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
  [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
  UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();
  return newImage;
}

// 处理微信回调
- (void)onResp:(BaseResp *)resp {
  if ([resp isKindOfClass:[SendAuthResp class]]) {
    // 登录回调
    SendAuthResp *authResp = (SendAuthResp *)resp;
    [self emitOnAuthResponse:@{
      @"errCode": @(authResp.errCode),
      @"errStr": authResp.errStr ?: @"",
      @"code": authResp.code ?: @"",
      @"state": authResp.state ?: @""
    }];
  } else if ([resp isKindOfClass:[PayResp class]]) {
    // 支付回调
    PayResp *payResp = (PayResp *)resp;
    [self emitOnAuthResponse:@{
      @"errCode": @(payResp.errCode),
      @"errStr": payResp.errStr ?: @""
    }];
  } else if ([resp isKindOfClass:[SendMessageToWXResp class]]) {
    // 分享回调
    SendMessageToWXResp *shareResp = (SendMessageToWXResp *)resp;
    [self emitOnAuthResponse:@{
      @"errCode": @(shareResp.errCode),
      @"errStr": shareResp.errStr ?: @""
    }];
  }
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeRTNWechatSpecJSI>(params);
}

@end 

4.rtn-wechat.podspec文件

ruby 复制代码
require "json"

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

Pod::Spec.new do |s|
  s.name            = "rtn-wechat"
  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)

  # 微信SDK依赖
  s.dependency "WechatOpenSDK", "~> 2.0.2"
end

5.package.json文件

json 复制代码
{
  "name": "rtn-wechat",
  "version": "0.0.1",
  "description": "WeChat login, payment and sharing with Turbo Native Modules",
  "react-native": "js/index",
  "source": "js/index",
  "files": [
    "js",
    "android",
    "ios",
    "rtn-wechat.podspec",
    "!android/build",
    "!ios/build",
    "!**/__tests__",
    "!**/__fixtures__",
    "!**/__mocks__"
  ],
  "keywords": [
    "react-native",
    "ios",
    "android"
  ],
  "repository": "https://github.com/<your_github_handle>/rtn-wechat",
  "author": "<Your Name> <your_email@your_provider.com> (https://github.com/<your_github_handle>)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/<your_github_handle>/rtn-wechat/issues"
  },
  "homepage": "https://github.com/<your_github_handle>/rtn-wechat#readme",
  "devDependencies": {},
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  },
  "codegenConfig": {
    "name": "RTNWechatSpec",
    "type": "modules",
    "jsSrcsDir": "js",
    "android": {
      "javaPackageName": "com.rtnwechat"
    }
  }
}

6.增加 info.plist 配置

xml 复制代码
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>weixin</string>
    <string>weixinULAPI</string>
</array>

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>weixin</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>您的微信AppID</string>
        </array>
    </dict>
</array>

需要注意的点:

使用 Codegen 生成脚手架代码时,iOS 不会自动清理 build 文件夹。例如,如果您更改了 Spec 名称,然后再次运行 Codegen ,旧文件将会被保留。如果发生这种情况,请记住在再次运行 Codegen 之前删除 build 文件夹。

如果修改了 ios 原生代码,需要用 xcode 重新编译运行

实机演示

示例仓库代码

github.com/Infiee/Reac...

文档参考

react-native-new-architecture

reactnative.dev/docs/turbo-...

dev.to/amazonappde...

developers.weixin.qq.com/doc/oplatfo...

扩展文档

iOS配置Universal Links

题外话

也尝试使用过 Expo 集成原生模块功能,但是需要 prebuild 出 ios 和 Android 目录来,和 cli 创建的模块方法类似,也需要遵守定义的那一套规则。

一般企业用的 app 大多都会需要第三方登录(如微信登录、QQ 登录、微博登录、fb 登录登功能)、第三方支付功能(微信支付、支付宝支付)、统计类(友盟统计)、极光推送、穿山甲等一些 sdk 的集成。这些功能都需要自己集成,如果是需要这种第三方 sdk 比较多的,还是建议使用 cli 的方式项目。

相关推荐
samllplum7 分钟前
在 master 分支上进行了 commit 但还没有 push,怎么安全地切到新分支并保留这些更改
前端·git
万叶学编程23 分钟前
鸿蒙移动应用开发--渲染控制实验
前端·华为·harmonyos
艾恩小灰灰44 分钟前
深入理解CSS中的`transform-origin`属性
前端·javascript·css·html·web开发·origin·transform
ohMyGod_1231 小时前
Vue如何获取Dom
前端·javascript·vue.js
蓉妹妹1 小时前
React项目添加react-quill富文本编辑器,遇到的问题,比如hr标签丢失
前端·react.js·前端框架
码客前端1 小时前
css图片设为灰色
前端·javascript·css
艾恩小灰灰2 小时前
CSS中的`transform-style`属性:3D变换的秘密武器
前端·css·3d·css3·html5·web开发·transform-style
Captaincc2 小时前
AI coding的隐藏王者,悄悄融了2亿美金
前端·后端·ai编程
天天扭码2 小时前
一分钟解决一道算法题——矩阵置零
前端·算法·面试
抹茶san2 小时前
el-tabs频繁切换tab引发的数据渲染混淆
前端·vue.js·element