RN原生模块交互:打通JS与原生的桥梁

RN原生模块交互:打通JS与原生的桥梁

在前几篇文章中,我们掌握了RN的基础组件、路由管理和状态管理,能够开发常规的跨端应用。但在实际业务中,RN的JS层能力有限,很多场景(如调用手机传感器、集成原生SDK、自定义高性能UI)需要依赖原生代码。本文作为RN体系化专栏的第四篇,将深入讲解RN与原生模块的交互原理Android/iOS原生模块开发第三方SDK集成,帮助你打通JS与原生的通信壁垒,实现RN能力的深度扩展。

一、RN与原生通信原理:JSBridge

RN的跨端能力本质是通过JSBridge实现JS层与原生层的双向通信,其核心是一个异步消息传递机制,工作流程可分为三步:

  1. JS层发起调用 :JS通过NativeModules调用原生模块方法,参数经序列化后通过JSBridge发送给原生层;
  2. 原生层处理请求:原生层接收消息后,解析参数并执行对应逻辑,完成后将结果序列化返回;
  3. JS层接收结果:JSBridge将原生返回的结果传递给JS层回调函数,完成一次通信。

RN的原生模块交互分为两种模式:

  • JS调用原生:如调用原生相机、获取设备信息;
  • 原生调用JS:如原生监听硬件事件后通知JS层(如电量变化、推送消息)。

二、JS调用原生模块(基础篇)

RN已内置部分原生模块(如AlertAsyncStorage),可直接通过react-nativeAPI调用,这是最基础的原生交互方式。

1. 调用RN内置原生模块

Alert(弹窗)和Linking(打开链接)为例,无需编写原生代码即可直接使用:

jsx 复制代码
import { View, Text, TouchableOpacity, StyleSheet, Alert, Linking } from 'react-native';

export default function BuiltInModuleDemo() {
  // 调用原生弹窗
  const showAlert = () => {
    Alert.alert(
      '原生弹窗',
      '这是RN内置的原生Alert组件',
      [
        { text: '取消', style: 'cancel' },
        { text: '确定', onPress: () => console.log('确定按钮点击') }
      ]
    );
  };

  // 调用原生打开浏览器
  const openBrowser = () => {
    Linking.openURL('https://reactnative.dev').catch(err => 
      console.error('打开链接失败:', err)
    );
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity style={styles.btn} onPress={showAlert}>
        <Text style={styles.btnText}>显示原生弹窗</Text>
      </TouchableOpacity>
      <TouchableOpacity style={[styles.btn, { marginTop: 15 }]} onPress={openBrowser}>
        <Text style={styles.btnText}>打开浏览器</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  btn: {
    backgroundColor: '#0066cc',
    paddingHorizontal: 30,
    paddingVertical: 12,
    borderRadius: 8,
  },
  btnText: {
    color: '#fff',
    fontSize: 16,
  },
});

2. 自定义原生模块(Android端)

当内置模块无法满足需求时,需自定义原生模块。以下实现一个Android原生模块,提供获取设备型号和自定义Toast的能力。

(1)创建Android原生模块类

在Android工程的app/src/main/java/com/[项目名]/modules目录下,创建DeviceModule.java

java 复制代码
package com.rndemo.modules;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import android.os.Build;
import android.widget.Toast;

// 自定义原生模块,需继承ReactContextBaseJavaModule
public class DeviceModule extends ReactContextBaseJavaModule {
    // 构造方法,接收React上下文
    public DeviceModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    // 重写getName方法,定义模块名称(JS层通过此名称调用)
    @Override
    public String getName() {
        return "DeviceModule";
    }

    // 定义JS可调用的方法,需添加@ReactMethod注解
    // 方法必须为void类型,异步结果通过Promise返回
    @ReactMethod
    public void getDeviceModel(Promise promise) {
        try {
            // 获取设备型号
            String model = Build.MODEL;
            // 成功返回结果
            promise.resolve(model);
        } catch (Exception e) {
            // 失败返回错误
            promise.reject("ERROR", e.getMessage());
        }
    }

    // 自定义Toast提示
    @ReactMethod
    public void showCustomToast(String message) {
        ReactApplicationContext context = getReactApplicationContext();
        // 调用Android原生Toast
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
    }
}
(2)注册原生模块

创建模块注册类CustomPackage.java,将自定义模块注册到RN中:

java 复制代码
package com.rndemo.modules;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CustomPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        // 注册自定义模块
        modules.add(new DeviceModule(reactContext));
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}
(3)在MainApplication中配置Package

修改MainApplication.java,添加自定义Package:

java 复制代码
import com.rndemo.modules.CustomPackage;
// ...
@Override
protected List<ReactPackage> getPackages() {
    @SuppressWarnings("UnnecessaryLocalVariable")
    List<ReactPackage> packages = new PackageList(this).getPackages();
    // 添加自定义Package
    packages.add(new CustomPackage());
    return packages;
}
(4)JS层调用自定义原生模块

在RN组件中,通过NativeModules调用上述原生模块:

jsx 复制代码
import { View, Text, TouchableOpacity, StyleSheet, NativeModules } from 'react-native';
import { useState } from 'react';

// 获取自定义原生模块
const { DeviceModule } = NativeModules;

export default function CustomNativeModuleDemo() {
  const [deviceModel, setDeviceModel] = useState('');

  // 获取设备型号(异步调用)
  const getModel = async () => {
    try {
      const model = await DeviceModule.getDeviceModel();
      setDeviceModel(`设备型号:${model}`);
    } catch (error) {
      console.error('获取设备型号失败:', error);
    }
  };

  // 显示自定义Toast
  const showToast = () => {
    DeviceModule.showCustomToast('这是原生Toast提示!');
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity style={styles.btn} onPress={getModel}>
        <Text style={styles.btnText}>获取设备型号</Text>
      </TouchableOpacity>
      <TouchableOpacity style={[styles.btn, { marginTop: 15 }]} onPress={showToast}>
        <Text style={styles.btnText}>显示原生Toast</Text>
      </TouchableOpacity>
      {deviceModel ? <Text style={styles.modelText}>{deviceModel}</Text> : null}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  btn: {
    backgroundColor: '#0066cc',
    paddingHorizontal: 30,
    paddingVertical: 12,
    borderRadius: 8,
  },
  btnText: {
    color: '#fff',
    fontSize: 16,
  },
  modelText: {
    marginTop: 20,
    fontSize: 16,
    color: '#333',
  },
});

3. 自定义原生模块(iOS端)

iOS端自定义原生模块的流程与Android类似,以下实现相同功能的iOS模块。

(1)创建iOS原生模块类

在iOS工程的[项目名]目录下,创建DeviceModule.hDeviceModule.m文件:

objc 复制代码
// DeviceModule.h
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(DeviceModule, NSObject)

// 暴露给JS的方法
RCT_EXTERN_METHOD(getDeviceModel:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(showCustomToast:(NSString *)message)

@end
objc 复制代码
// DeviceModule.m
#import "DeviceModule.h"
#import <UIKit/UIKit.h>

@implementation DeviceModule

// 模块名称(需与头文件一致)
RCT_EXPORT_MODULE(DeviceModule);

// 获取设备型号
RCT_EXPORT_METHOD(getDeviceModel:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
    @try {
        // 获取设备型号
        NSString *model = [UIDevice currentDevice].model;
        resolve(model);
    } @catch (NSException *e) {
        reject(@"ERROR", e.reason, nil);
    }
}

// 自定义Toast(iOS无原生Toast,需手动实现)
RCT_EXPORT_METHOD(showCustomToast:(NSString *)message) {
    UIViewController *rootVC = [UIApplication sharedApplication].delegate.window.rootViewController;
    
    // 创建Toast标签
    UILabel *toast = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 40)];
    toast.center = CGPointMake(rootVC.view.bounds.size.width / 2, rootVC.view.bounds.size.height * 0.8);
    toast.backgroundColor = [UIColor blackColor];
    toast.textColor = [UIColor whiteColor];
    toast.textAlignment = NSTextAlignmentCenter;
    toast.layer.cornerRadius = 20;
    toast.clipsToBounds = YES;
    toast.text = message;
    [rootVC.view addSubview:toast];
    
    // 自动消失
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [toast removeFromSuperview];
    });
}

@end
(2)JS层调用(跨端统一)

iOS端无需额外注册,JS层代码与Android端完全一致,RN会自动识别iOS原生模块,实现跨端统一调用。

三、原生UI组件封装(进阶篇)

除了原生方法调用,RN还支持封装自定义原生UI组件(如Android的TextView、iOS的UILabel),实现高性能的原生UI展示。

1. Android原生UI组件封装

(1)创建原生ViewManager
java 复制代码
package com.rndemo.views;

import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import android.widget.TextView;

// 自定义原生TextView组件
public class CustomTextViewManager extends SimpleViewManager<TextView> {
    public static final String REACT_CLASS = "CustomTextView";

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    // 创建原生View
    @Override
    protected TextView createViewInstance(ThemedReactContext reactContext) {
        TextView textView = new TextView(reactContext);
        textView.setTextSize(16);
        return textView;
    }

    // 定义JS可设置的属性(如text、color)
    @ReactProp(name = "text")
    public void setText(TextView view, String text) {
        view.setText(text);
    }

    @ReactProp(name = "textColor")
    public void setTextColor(TextView view, String color) {
        view.setTextColor(android.graphics.Color.parseColor(color));
    }
}
(2)注册ViewManager

CustomPackagecreateViewManagers方法中注册:

java 复制代码
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    List<ViewManager> managers = new ArrayList<>();
    managers.add(new CustomTextViewManager());
    return managers;
}
(3)JS层封装并使用原生UI组件
jsx 复制代码
// components/CustomTextView.js
import { requireNativeComponent } from 'react-native';

// 引入原生UI组件
const CustomTextView = requireNativeComponent('CustomTextView');

export default CustomTextView;
jsx 复制代码
// 页面中使用
import CustomTextView from '../components/CustomTextView';

export default function NativeUIComponentDemo() {
  return (
    <View style={styles.container}>
      <CustomTextView
        style={{ width: 200, height: 40 }}
        text="原生TextView组件"
        textColor="#ff6600"
      />
    </View>
  );
}

四、第三方原生SDK集成实战

实际开发中,常需集成第三方原生SDK(如高德地图、微信支付),以下以高德地图Android SDK为例,演示RN与第三方SDK的集成流程。

1. 集成高德地图Android SDK

(1)添加SDK依赖

在Android工程的build.gradle中添加依赖:

gradle 复制代码
// 项目根目录build.gradle
allprojects {
    repositories {
        // 添加高德仓库
        maven { url 'https://repo.amap.com/'}
    }
}

// app/build.gradle
dependencies {
    // 高德地图SDK
    implementation 'com.amap.api:3dmap:9.8.0'
    implementation 'com.amap.api:location:6.15.1'
}
(2)配置权限和Key

AndroidManifest.xml中添加权限和高德Key:

xml 复制代码
<!-- 权限声明 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />

<!-- 高德Key配置 -->
<meta-data
    android:name="com.amap.api.v2.apikey"
    android:value="你的高德Key" />
(3)封装地图原生模块

创建AMapModule.java,实现地图初始化和定位功能,再通过JSBridge暴露给RN层调用。

(4)JS层调用地图SDK

通过NativeModules调用封装的地图模块,实现地图展示和定位功能。

2. 集成注意事项

  • 权限申请 :原生SDK所需权限需在AndroidManifest/iOS Info.plist中声明,动态权限需在JS层通过PermissionsAndroid/PermissionsIOS申请;
  • 生命周期管理:原生SDK的初始化和销毁需与RN组件生命周期绑定,避免内存泄漏;
  • 版本兼容:需确保第三方SDK版本与RN版本、系统版本兼容,避免出现兼容性问题。

五、原生调用JS方法

除了JS调用原生,原生也可主动调用JS方法,实现原生事件向JS层的传递(如硬件传感器数据、推送消息)。

1. Android原生调用JS

java 复制代码
// 获取ReactInstanceManager
ReactInstanceManager manager = getReactNativeHost().getReactInstanceManager();
ReactContext context = manager.getCurrentReactContext();

// 调用JS全局方法
if (context != null) {
    context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
        .emit("nativeEvent", "原生传递给JS的消息");
}

2. iOS原生调用JS

objc 复制代码
// 获取bridge
RCTBridge *bridge = [self.bridge get];
// 调用JS方法
[bridge enqueueJSCall:@"RCTDeviceEventEmitter" method:@"emit" args:@[@"nativeEvent", @"原生传递给JS的消息"]];

3. JS层监听原生事件

jsx 复制代码
import { NativeEventEmitter, NativeModules } from 'react-native';

// 创建事件发射器
const eventEmitter = new NativeEventEmitter(NativeModules.DeviceModule);

// 监听原生事件
const subscription = eventEmitter.addListener('nativeEvent', (message) => {
  console.log('原生事件消息:', message);
});

// 组件卸载时取消监听
useEffect(() => {
  return () => {
    subscription.remove();
  };
}, []);

六、小结与下一阶段预告

本文系统讲解了RN与原生模块的交互原理和实战方案,从内置原生模块调用,到自定义原生模块/UI组件开发,再到第三方SDK集成,你已具备RN与原生深度融合的开发能力,能够应对复杂的原生能力扩展需求。

下一篇文章《RN跨端适配与沉浸式体验:适配不同设备与系统》,将聚焦RN应用的多设备适配平台差异化处理,解决不同屏幕、不同系统的兼容性问题,打造更优质的跨端用户体验。

如果你在原生模块集成中遇到兼容性或通信问题,可随时留言,我会为你提供针对性的解决方案!

相关推荐
hoiii1872 小时前
MATLAB中离散傅里叶变换(DFT)的实现与分析
开发语言·matlab
进击的荆棘2 小时前
C++起始之路——类和对象(中)
开发语言·c++
梦想的旅途22 小时前
非官方接口下企业微信外部群主动交互:数据传输稳定性优化方案摘要
开发语言·php
沐知全栈开发2 小时前
Linux 系统目录结构
开发语言
小画家~2 小时前
第三十七:类型断言
开发语言·c++·算法·golang
编织幻境的妖2 小时前
Python读写CSV与JSON文件方法
开发语言·python·json
weixin_307779132 小时前
Jenkins jQuery3 API 插件详解:赋能插件前端开发的利器
运维·开发语言·前端·jenkins·jquery
世转神风-2 小时前
QEventLoop-qt阻塞异步操作
开发语言·qt
Hard but lovely2 小时前
C++ 11--》初始化
开发语言·c++