4. 搭建第一个iOS原生模块
本节的目标不是“看懂一个示例”,而是亲手完成一条最小可用的原生能力通道:在 React Native 工程中创建一个 iOS 原生模块,用 Objective-C 或 Swift 实现方法,通过 RCTBridgeModule 与 RCT_EXPORT_METHOD 把方法暴露给 JavaScript,最后在 JS 侧成功调用并拿到返回值。这条链路一旦跑通,后续接入相机、蓝牙、加密、系统信息等能力都只是替换具体实现。
一、先明确这条链路的四个角色
React Native 的 iOS 原生模块并不是“JS 直接调用 OC 方法”,中间存在明确的桥接层。理解这四个角色,能避免后面出现“方法写了但 JS 找不到”的困惑:
- JS 侧:通过
NativeModules.模块名.方法名(参数)发起调用,返回 Promise 或回调。 - RCTBridge:RN 的桥,负责在 JS 线程与原生线程之间传递调用消息、参数和结果。
- 原生模块类:继承
NSObject并遵循RCTBridgeModule协议,是方法的实际承载者。 - 导出宏:
RCT_EXPORT_MODULE注册模块,RCT_EXPORT_METHOD注册可被 JS 调用的方法。
下面这张图展示了从 JS 调用到原生返回的完整信号链,注意模块名、方法名和参数是如何穿过桥的:
二、创建工程与原生模块文件
先准备一个可运行的 RN 工程。如果还没有,用以下命令创建(示例工程名 NativeDemo):
npx react-native init NativeDemo
cd NativeDemo/ios
pod install
接下来在 iOS 工程目录下新建原生模块文件。推荐放在 ios/NativeDemo/ 下,与 AppDelegate 同级,便于 Xcode 自动纳入编译。这里以 Objective-C 为例,创建 DeviceInfoModule.h 与 DeviceInfoModule.m。
头文件负责声明类与协议遵循:
// DeviceInfoModule.h
#import <React/RCTBridgeModule.h>
@interface DeviceInfoModule : NSObject <RCTBridgeModule>
@end
实现文件负责注册模块、导出方法并返回结果:
// DeviceInfoModule.m
#import "DeviceInfoModule.h"
#import <UIKit/UIKit.h>
@implementation DeviceInfoModule
RCT_EXPORT_MODULE(DeviceInfo);
// 导出同步方法:返回设备名称
RCT_EXPORT_METHOD(getDeviceName:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
@try {
NSString *name = [[UIDevice currentDevice] name];
resolve(name);
} @catch (NSException *e) {
reject(@"device_name_error", e.reason, nil);
}
}
// 导出带参数方法:计算两数之和
RCT_EXPORT_METHOD(addNumber:(double)a
withB:(double)b
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
resolve(@(a + b));
}
@end
这里有两个关键点。第一,RCT_EXPORT_MODULE(DeviceInfo) 中的 DeviceInfo 就是 JS 侧访问的模块名,若省略参数则默认取类名去掉 Module 后缀。第二,RCT_EXPORT_METHOD 的方法名由选择器决定,冒号前的片段拼接即为 JS 方法名,例如 addNumber:withB:resolver:rejecter: 对应 JS 的 addNumber(a, b),其中 resolver 与 rejecter 是 RN 约定的回调参数,不参与 JS 传参。
#import <React/RCTBridgeModule.h>,Swift 类用 @objc(DeviceInfo) 标注并遵循 RCTBridgeModule,方法用 @objc 暴露。混编时最容易出错的是桥接头文件路径未在 Build Settings 中正确配置。三、在 JS 侧调用并验证
原生模块注册完成后,无需手动 import,RN 会在启动时自动扫描并挂载。JS 侧通过 NativeModules 访问:
import { NativeModules } from 'react-native';
const { DeviceInfo } = NativeModules;
async function demo() {
try {
const name = await DeviceInfo.getDeviceName();
console.log('设备名称:', name);
const sum = await DeviceInfo.addNumber(3.5, 6.5);
console.log('计算结果:', sum); // 10
} catch (e) {
console.warn('调用失败:', e.code, e.message);
}
}
demo();
运行 npx react-native run-ios 后,在 Metro 日志或 Xcode 控制台中应能看到输出。判断链路是否真正打通,可以按以下顺序排查:
- 模块是否为 undefined:说明模块未注册成功,检查类是否遵循协议、文件是否加入 Xcode 编译目标。
- 方法是否为 undefined:说明
RCT_EXPORT_METHOD未生效,检查宏是否写在@implementation内、方法签名是否正确。 - Promise 一直 pending:说明原生方法没有调用 resolve 或 reject,检查分支是否遗漏回调。
- 参数类型不匹配:RN 支持 NSNumber、NSString、NSArray、NSDictionary 等,double 需用
@(a+b)包装为 NSNumber 返回。
四、一个可直接复用的最小验证清单
把上面的过程整理成可执行检查项,每次新增原生模块时逐条核对,能显著减少“写了但调不通”的时间:
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 模块注册 | RCT_EXPORT_MODULE(DeviceInfo) | 宏写在 @interface 外 |
| 协议遵循 | 类声明 <RCTBridgeModule> | 忘记引入 React/RCTBridgeModule.h |
| 方法导出 | 宏位于 @implementation 内 | 方法名与 JS 调用不一致 |
| 回调参数 | resolve/reject 成对出现 | 只写 resolve,异常时无返回 |
| 编译目标 | 文件加入 Build Phases | 新建文件未勾选 target |