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 的方式项目。

相关推荐
BBB努力学习程序设计13 分钟前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html
BBB努力学习程序设计19 分钟前
CSS3渐变:用代码描绘色彩的流动之美
前端·html
冰暮流星28 分钟前
css之动画
前端·css
jump6801 小时前
axios
前端
spionbo1 小时前
前端解构赋值避坑指南基础到高阶深度解析技巧
前端
用户4099322502121 小时前
Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗
前端·ai编程·trae
开发者小天1 小时前
React中的componentWillUnmount 使用
前端·javascript·vue.js·react.js
永远的个初学者2 小时前
图片优化 上传图片压缩 npm包支持vue(react)框架开源插件 支持在线与本地
前端·vue.js·react.js
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
npm i / npm install 卡死不动解决方法
前端·npm·node.js
Kratzdisteln2 小时前
【Cursor _RubicsCube Diary 1】Node.js;npm;Vite
前端·npm·node.js