React Native原生模块开发实战:从创建到通信详解

Viewed 0

实战:准备与原生代码编写

在React Native开发中,原生模块扩展允许开发者调用平台原生功能。以下是创建和集成原生模块的完整步骤。

项目初始化与静态库配置

首先,使用react-native init nativeTest创建一个新的React Native项目。然后,在Xcode中打开项目文件nativeTest/ios/nativeTest.xcodeproj

接下来,创建静态库并链接到项目:

  1. nativeTest/node_modules目录下创建文件夹react-native-nativeModuleTest,并在其中创建ios子目录。
  2. 在Xcode中新建一个Cocoa Touch Static Library,命名为nativeModuleTest
  3. 将生成的静态库文件拷贝到nativeTest/node_modules/react-native-nativeModuleTest/ios目录中。
  4. 在Xcode中打开nativeModuleTest.xcodeproj,配置Header Search Paths为$(SRCROOT)/../../react-native/React,并设置为recursive。
  5. nativeModuleTest.xcodeproj拖拽到主项目的Library中。
  6. 在Build Phases的Link Binary With Libraries部分添加libnativeModuleTest.a静态库。

编写原生模块代码

在React Native中,原生模块是一个实现RCTBridgeModule协议的Objective-C类。RCT是ReaCT的缩写。

创建头文件nativeModuleTest.h

#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

@interface nativeModuleTest : NSObject <RCTBridgeModule>
@end

实现文件nativeModuleTest.m

#import "nativeModuleTest.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>

@implementation nativeModuleTest

RCT_EXPORT_MODULE(nativeModuleTest);

// 暴露一个打印方法
RCT_EXPORT_METHOD(testPrint:(NSString *)name info:(NSDictionary *)info) {
    RCTLogInfo(@"%@: %@", name, info);
}

// 获取屏幕尺寸的辅助函数
static NSDictionary *DynamicDimensions() {
    CGFloat width = MIN(RCTScreenSize().width, RCTScreenSize().height);
    CGFloat height = MAX(RCTScreenSize().width, RCTScreenSize().height);
    CGFloat scale = RCTScreenScale();
    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) {
        width = MAX(RCTScreenSize().width, RCTScreenSize().height);
        height = MIN(RCTScreenSize().width, RCTScreenSize().height);
    }
    return @{@"width": @(width), @"height": @(height), @"scale": @(scale)};
}

// 通过回调返回屏幕尺寸
RCT_EXPORT_METHOD(getDynamicDimensions:(RCTResponseSenderBlock)callback) {
    callback(@[[NSNull null], DynamicDimensions()]);
}

// 通过Promise返回屏幕尺寸
RCT_REMAP_METHOD(getDynamicDimensionsByPromise,
                 resolver:(RCTPromiseResolveBlock)resolve
                 rejecter:(RCTPromiseRejectBlock)reject) {
    NSDictionary *dimensions = DynamicDimensions();
    if (dimensions) {
        resolve(dimensions);
    } else {
        reject(@"-1001", @"not respond this method", nil);
    }
}

// 初始化并监听屏幕方向变化
- (instancetype)init {
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(orientationDidChange:)
                                                     name:UIDeviceOrientationDidChangeNotification
                                                   object:nil];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@synthesize bridge = _bridge;

// 发送方向变化事件到JavaScript
- (void)orientationDidChange:(id)noti {
    [_bridge.eventDispatcher sendDeviceEventWithName:@"orientationDidChange"
                                                body:@{@"Orientation": UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation) ? @"Landscape" : @"Portrait",
                                                       @"Dimensions": DynamicDimensions()}];
}

// 导出常量供JavaScript使用
- (NSDictionary *)constantsToExport {
    return @{@"EVENT_ORIENTATION": @"orientationDidChange"};
}

@end

JavaScript调用示例

在JavaScript中,可以这样使用原生模块:

import { NativeModules, DeviceEventEmitter } from 'react-native';
const nativeModuleTest = NativeModules.nativeModuleTest;

// 调用打印方法
nativeModuleTest.testPrint("Jack", { height: '1.78m', weight: '7kg' });

// 通过回调获取屏幕尺寸
nativeModuleTest.getDynamicDimensions((error, dimensions) => {
    console.log("屏幕尺寸:", dimensions);
});

// 通过Promise获取屏幕尺寸
async function fetchDimensions() {
    try {
        const dimensions = await nativeModuleTest.getDynamicDimensionsByPromise();
        console.log("屏幕尺寸(Promise):", dimensions);
    } catch (e) {
        console.error(e);
    }
}
fetchDimensions();

// 监听屏幕方向变化事件
const subscription = DeviceEventEmitter.addListener('orientationDidChange', (dimensions) => {
    console.log("屏幕旋转,新尺寸:", dimensions);
});

// 在组件卸载时取消订阅
// componentWillUnmount() { subscription.remove(); }

通信方式详解

React Native中,JavaScript与原生模块之间的通信主要有三种方式,各有优缺点。

JavaScript向原生模块传输数据

通过直接调用原生模块暴露的方法,并传递参数。这种方式只能调用一次。
示例:在JavaScript中调用nativeModuleTest.testPrint("Jack", info),原生模块通过RCT_EXPORT_METHOD接收参数并处理。

原生模块向JavaScript传输数据

  1. 回调函数(Callbacks):原生方法可以接受一个RCTResponseSenderBlock参数,用于将数据传回JavaScript。回调函数第一个参数是错误对象(无错误时为null),其余为返回值。注意:回调通常只能调用一次,且必须调用以避免内存泄漏。
    示例:getDynamicDimensions方法使用回调返回屏幕尺寸。

  2. Promise:如果原生方法的最后两个参数是RCTPromiseResolveBlockRCTPromiseRejectBlock,则对应的JavaScript方法会返回Promise对象,便于使用async/await语法。
    示例:getDynamicDimensionsByPromise方法通过Promise解析屏幕尺寸或拒绝错误。

发送事件(多次数据传输)

原生模块可以通过RCTEventDispatcher主动向JavaScript发送事件,即使未被调用。适用于持续数据流,如屏幕方向监听。
示例:在orientationDidChange方法中,使用_bridge.eventDispatcher发送方向变化事件,JavaScript通过DeviceEventEmitter订阅。

发布上线

完成开发后,可以将原生模块发布为npm包供他人使用。

创建GitHub仓库

在模块目录nativeTest/node_modules/react-native-nativeModuleTest中初始化Git仓库,并关联到远程GitHub仓库。

创建入口文件

创建index.js文件作为模块入口:

import { NativeModules } from 'react-native';
module.exports = NativeModules.nativeModuleTest;

发布到npm

  1. 运行npm init生成package.json文件,确保包含基本信息如名称、版本、入口文件和GitHub仓库链接。如果模块依赖其他包,需在dependencies中声明。
  2. 注册npm账号(如果尚未注册),使用npm adduser命令。
  3. 执行npm publish发布模块。发布成功后,其他开发者可通过npm install react-native-nativeModuleTest安装使用。

通过以上步骤,你可以完整地创建、测试和发布一个React Native原生模块,实现JavaScript与原生平台的高效通信。

0 Answers