什么情况需要原生模块
React Native 的 JS 层无法直接访问设备硬件(相机、麦克风、蓝牙、NFC)和系统能力(推送通知、健康数据、支付)。这些功能需要通过原生模块暴露给 JS 层调用。
好消息是:你需要的大多数原生能力,Expo SDK 或社区库已经实现好了。自己写原生模块的场景越来越少,主要是:集成特定厂商的原生 SDK(如某银行的支付 SDK、某地图 SDK)、使用尚未有 JS 封装的系统 API、对性能要求极高的场景(需要在 Native 层处理大量数据)。
核心名词解释
原生通信架构(旧 vs 新)
推送通知完整流程
推送通知是移动应用最复杂的功能之一,涉及多个系统和第三方服务的协作。以下是从申请权限到用户点击通知的完整链路:
// 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 访问地址
}
定位服务
import * as Location from 'expo-location';
export async function getCurrentLocation() {
// 1. 申请前台定位权限
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
throw new Error('位置权限被拒绝');
}
// 2. 获取当前位置(精度 vs 速度 权衡)
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced, // High/Low/Balanced
});
return {
lat: location.coords.latitude,
lon: location.coords.longitude,
};
}
// 持续位置更新(实时跟踪场景)
export async function startTracking(
callback: (loc: { lat: number; lon: number }) => void
) {
// 注意:持续定位需要后台定位权限(iOS 更严格)
const subscription = await Location.watchPositionAsync(
{ accuracy: Location.Accuracy.High, timeInterval: 5000 },
(loc) => callback({ lat: loc.coords.latitude, lon: loc.coords.longitude })
);
return subscription; // 调用 subscription.remove() 停止跟踪
}
android.permissions 数组中声明,并在 Managed Workflow 下由 Expo 自动写入 AndroidManifest.xml。
expo-camera(相机拍照/录像/扫码)、expo-location(GPS 定位)、expo-notifications(推送通知)、expo-image-picker(相册选图)、expo-file-system(本地文件读写)、expo-sensors(加速度计/陀螺仪)、expo-av(音频/视频播放)、expo-barcode-scanner(条码扫描)。所有模块都可用 npx expo install 模块名 安装,自动选择与当前 Expo SDK 版本兼容的版本号。
原生模块与设备能力的核心要点:① Expo SDK 已封装大量设备 API(相机、定位、推送通知、传感器),优先使用现成库,减少维护 iOS/Android 双端代码的成本;② 相机和定位权限必须在 app.json 中声明(iOS 的 NSCameraUsageDescription 等),且运行时调用 requestPermissionsAsync() 二次申请,未授权时优雅降级而非崩溃;③ 推送通知需要先获取 Expo Push Token,通过服务器端 Expo Push API 发送;FCM/APNs 凭证分平台独立配置;④ expo-image-picker 选择图片后得到本地 URI,上传到服务器时用 FormData 包装,或通过预签名 URL 直传 S3;⑤ 自定义原生模块(Turbo Module)需要用 C++ 或平台语言实现接口,通过 JSI 桥接调用,适合性能敏感或平台专属功能;⑥ Bare Workflow 才能安装任意原生模块;Managed Workflow 只能使用 Expo 预配置的原生能力。