什么情况需要原生模块
React Native 的 JS 层无法直接访问设备硬件(相机、麦克风、蓝牙、NFC)和系统能力(推送通知、健康数据、支付)。这些功能需要通过原生模块暴露给 JS 层调用。
好消息是:你需要的大多数原生能力,Expo SDK 或社区库已经实现好了。自己写原生模块的场景越来越少,主要是:集成特定厂商的原生 SDK(如某银行的支付 SDK、某地图 SDK)、使用尚未有 JS 封装的系统 API、对性能要求极高的场景(需要在 Native 层处理大量数据)。
核心名词解释
Expo SDK
Expo 官方提供的一套原生模块集合,封装了几十个常用的设备能力 API,包括相机、位置、文件系统、推送通知、传感器、日历等。使用 npx expo install 安装,自动匹配兼容版本。
expo-camera
相机模块,提供预览、拍照、录像功能。新版(v14+)基于 Camera2 API(Android)和 AVFoundation(iOS),支持二维码扫描、面部检测等。
expo-location
位置服务,支持前台/后台定位、地理围栏、持续位置更新。需要在 app.json 中声明权限,iOS 还需在 Info.plist 说明使用原因。
expo-notifications
推送通知模块,支持本地通知和远程推送。获取 Expo Push Token,通过 Expo 的推送服务转发到 APNs(iOS)和 FCM(Android)。
Native Module(原生模块)
用 Objective-C/Swift(iOS)或 Java/Kotlin(Android)编写,通过 Bridge 或 JSI 暴露给 JS 调用的模块。
JSI(JavaScript Interface)
新架构的 JS-Native 通信层,JS 引擎直接持有 C++ 对象引用,调用原生方法无需 JSON 序列化,实现同步调用,延迟接近零。
Turbo Module
基于 JSI 实现的新一代原生模块。与旧的 Native Module 的区别:懒加载(按需初始化,启动更快)、类型安全(通过 Codegen 自动生成 C++ 接口)、支持同步调用。
Codegen
新架构的代码生成工具,根据 TypeScript/Flow 类型定义自动生成 C++ 胶水代码,确保 JS 和 Native 层的类型安全,消除接口不一致导致的崩溃。
原生通信架构(旧 vs 新)
旧架构:Bridge(异步,有序列化开销)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JS Thread Bridge Native
│ │ │
│─── JSON 消息 ────────▶│ │
│ {module:"Camera", │ │
│ method:"takePic"} │ │
│ │──── 反序列化 ──────▶│
│ │ │ 执行原生代码
│ │◀─── JSON 结果 ─────│
│◀─── 回调 ────────────│ │
│ │ │
新架构:JSI + Turbo Module(同步,无序列化)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JS Thread C++ Layer Native
│ │ │
│ camera.takePic() │ │
│──── 直接 C++ 调用 ──▶│ │
│ (同步,无队列) │──── C++ 调用 ─────▶│
│ │ │ 执行原生代码
│◀──── 同步返回 ────────│◀─── 返回值 ────────│
│ │ │
优势:
• 无 JSON 序列化,性能提升 ~3-5x
• 支持同步调用(旧架构只能异步)
• 懒加载,启动时间减少
推送通知完整流程
推送通知是移动应用最复杂的功能之一,涉及多个系统和第三方服务的协作。以下是从申请权限到用户点击通知的完整链路:
// hooks/useNotifications.ts
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
import { useEffect, useRef } from 'react';
// 通知显示方式(前台时也显示)
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotifications(): Promise<string | null> {
// 1. 检查并申请权限
const { status: existingStatus } =
await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('推送通知权限被拒绝');
return null;
}
// 2. Android 需要创建通知渠道
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
});
}
// 3. 获取 Expo Push Token(后端用此 token 发送通知)
const { data: pushToken } = await Notifications.getExpoPushTokenAsync({
projectId: 'your-expo-project-id', // app.json 中的 projectId
});
return pushToken; // 发给后端保存
}
export function useNotificationObserver() {
const responseListener = useRef<Notifications.Subscription>();
useEffect(() => {
// 监听用户点击通知(App 在后台或关闭状态)
responseListener.current =
Notifications.addNotificationResponseReceivedListener(response => {
const { postId } = response.notification.request.content.data;
if (postId) {
// 跳转到对应帖子页面(深度链接)
navigation.navigate('PostDetail', { id: postId });
}
});
return () => {
if (responseListener.current) {
Notifications.removeNotificationSubscription(responseListener.current);
}
};
}, []);
}
相机拍照与上传
import * as ImagePicker from 'expo-image-picker';
import { api } from '../lib/api';
export async function pickAndUploadImage(): Promise<string | null> {
// 1. 申请相册权限
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') return null;
// 2. 打开图片选择器
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 0.8, // 压缩到 80% 质量
});
if (result.canceled) return null;
// 3. 获取预签名上传 URL(安全做法,避免前端持有 S3 密钥)
const { uploadUrl, publicUrl } = await api.post('/upload/presign', {
filename: result.assets[0].fileName,
contentType: 'image/jpeg',
});
// 4. 直传 S3(绕过后端,减轻服务器压力)
const imageData = await fetch(result.assets[0].uri);
const blob = await imageData.blob();
await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': 'image/jpeg' },
});
return publicUrl; // CDN 访问地址
}
优先用 Expo SDK
在决定自己写原生模块之前,先查看 Expo SDK 文档(docs.expo.dev)和 React Native Directory(reactnative.directory)。90% 的常见需求都有现成的库。自己写原生模块意味着需要维护 iOS 和 Android 两套代码,升级 RN 版本时也要同步适配,成本很高。
权限说明文字要诚实
iOS 审核会严格检查权限申请的说明文字(NSCameraUsageDescription 等)。说明必须清楚告诉用户为什么需要这个权限,且与实际功能一致。模糊或欺骗性的说明会导致应用被拒绝。