rn-native-featuresSkill rn-native-features

Expo React Native 应用中的原生特性集成指南,涵盖权限请求、相机使用、推送通知、触觉反馈、位置服务和本地存储等。

移动开发 0 次安装 0 次浏览 更新于 3/3/2026

name: rn-native-features description: Expo React Native 应用中的原生 iOS 功能。在 Expo 中实现相机、推送通知、触觉反馈、权限、设备传感器或其他原生 API 时使用。

原生特性 (Expo)

权限模式

在使用原生特性前总是请求权限:

import * as Camera from 'expo-camera';

async function requestCameraPermission() {
  const { status } = await Camera.requestCameraPermissionsAsync();
  
  if (status !== 'granted') {
    // 如果被拒绝,引导用户去设置
    Alert.alert(
      '需要相机访问权限',
      '请在设置中启用相机访问权限以拍照。',
      [
        { text: '取消', style: 'cancel' },
        { text: '打开设置', onPress: () => Linking.openSettings() },
      ]
    );
    return false;
  }
  return true;
}

先检查权限状态

import * as Camera from 'expo-camera';

function usePermission() {
  const [permission, requestPermission] = Camera.useCameraPermissions();
  
  // permission.granted - 布尔值
  // permission.canAskAgain - 如果用户选择了“不再询问”则为 false
  // permission.status - 'granted' | 'denied' | 'undetermined'
  
  return { permission, requestPermission };
}

相机

基本相机与拍照

import { CameraView, useCameraPermissions } from 'expo-camera';
import { useRef, useState } from 'react';

export function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const [facing, setFacing] = useState<'front' | 'back'>('back');
  const cameraRef = useRef<CameraView>(null);

  if (!permission) return <View />;
  
  if (!permission.granted) {
    return (
      <View>
        <Text>需要相机访问权限</Text>
        <Button title="授权" onPress={requestPermission} />
      </View>
    );
  }

  const takePicture = async () => {
    if (cameraRef.current) {
      const photo = await cameraRef.current.takePictureAsync({
        quality: 0.8,
        base64: false,
        exif: false,
      });
      console.log('照片 URI:', photo.uri);
      // photo.uri 是一个本地文件路径
    }
  };

  return (
    <View style={{ flex: 1 }}>
      <CameraView 
        ref={cameraRef}
        style={{ flex: 1 }} 
        facing={facing}
      >
        <View style={styles.controls}>
          <Button title="翻转" onPress={() => setFacing(f => f === 'back' ? 'front' : 'back')} />
          <Button title="拍照" onPress={takePicture} />
        </View>
      </CameraView>
    </View>
  );
}

图片选择器(相册+相机)

通常比完整的相机 UI 更简单:

import * as ImagePicker from 'expo-image-picker';

async function pickImage() {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  });

  if (!result.canceled) {
    return result.assets[0].uri;
  }
}

async function takePhoto() {
  const permission = await ImagePicker.requestCameraPermissionsAsync();
  if (!permission.granted) return;
  
  const result = await ImagePicker.launchCameraAsync({
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  });

  if (!result.canceled) {
    return result.assets[0].uri;
  }
}

推送通知

使用 expo-notifications 设置

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';

// 配置应用前台时通知的显示方式
Notifications.setNotificationHandler({
  handleNotification: async () =>({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

async function registerForPushNotifications() {
  if (!Device.isDevice) {
    console.log('推送通知需要物理设备');
    return null;
  }

  // 检查现有权限
  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;
  }

  // 获取 Expo 推送令牌
  const token = await Notifications.getExpoPushTokenAsync({
    projectId: 'your-expo-project-id', // 来自 app.json
  });

  return token.data; // "ExponentPushToken[xxxx]"
}

处理接收到的通知

import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';

export function useNotificationHandler() {
  const notificationListener = useRef<Notifications.Subscription>();
  const responseListener = useRef<Notifications.Subscription>();

  useEffect(() => {
    // 应用前台时收到通知
    notificationListener.current = Notifications.addNotificationReceivedListener(
      (notification) => {
        console.log('收到:', notification.request.content);
      }
    );

    // 用户点击通知
    responseListener.current = Notifications.addNotificationResponseReceivedListener(
      (response) => {
        const data = response.notification.request.content.data;
        // 根据通知数据导航
        if (data.screen) {
          router.push(data.screen);
        }
      }
    );

    return () => {
      notificationListener.current?.remove();
      responseListener.current?.remove();
    };
  }, []);
}

本地发送测试通知

async function sendTestNotification() {
  await Notifications.scheduleNotificationAsync({
    content: {
      title: "比赛提醒",
      body: "您的比赛将在 30 分钟后开始!",
      data: { screen: '/match/123' },
    },
    trigger: { seconds: 5 },
  });
}

从后端发送(Expo Push API)

# FastAPI 示例
import httpx

async def send_push_notification(
    expo_push_token: str, 
    title: str, 
    body: str, 
    data: dict = None
):
    message = {
        "to": expo_push_token,
        "title": title,
        "body": body,
        "data": data or {},
    }
    
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://exp.host/--/api/v2/push/send",
            json=message,
            headers={"Content-Type": "application/json"},
        )
        return response.json()

触觉反馈

import * as Haptics from 'expo-haptics';

// 轻触 - 用于 UI 交互
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);

// 中等 - 用于确认
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);

// 重触 - 用于重要操作
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);

// 成功/错误/警告 - 语义反馈
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);

// 选择 - 用于选择器/滚动
Haptics.selectionAsync();

良好的触觉模式

// 按钮按压
<Pressable 
  onPress={() => {
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    handlePress();
  }}
/>

// 表单提交成功
await submitForm();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);

// 错误状态
if (error) {
  Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}

// 选择器/滚动选择
<Picker 
  onValueChange={(value) => {
    Haptics.selectionAsync();
    setValue(value);
  }}
/>

位置

import * as Location from 'expo-location';

async function getCurrentLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== 'granted') {
    return null;
  }

  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.Balanced,
  });
  
  return {
    latitude: location.coords.latitude,
    longitude: location.coords.longitude,
  };
}

// 监听位置变化
function useLocationTracking() {
  const [location, setLocation] = useState(null);

  useEffect(() => {
    let subscription: Location.LocationSubscription;
    
    (async () => {
      const { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') return;
      
      subscription = await Location.watchPositionAsync(
        {
          accuracy: Location.Accuracy.High,
          distanceInterval: 10, // 米
        },
        (loc) => setLocation(loc.coords)
      );
    })();
    
    return () => subscription?.remove();
  }, []);
  
  return location;
}

本地存储

AsyncStorage(简单的键值存储)

import AsyncStorage from '@react-native-async-storage/async-storage';

// 存储
await AsyncStorage.setItem('user_preferences', JSON.stringify(prefs));

// 检索
const prefs = JSON.parse(await AsyncStorage.getItem('user_preferences') || '{}');

// 删除
await AsyncStorage.removeItem('user_preferences');

SecureStore(敏感数据)

import * as SecureStore from 'expo-secure-store';

// 用于令牌、凭证 - 加密存储
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_token');

应用状态与生命周期

import { useEffect } from 'react';
import { AppState, AppStateStatus } from 'react-native';

function useAppState(callback: (state: AppStateStatus) => void) {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', callback);
    return () => subscription.remove();
  }, [callback]);
}

// 使用
useAppState((state) => {
  if (state === 'active') {
    // 应用进入前台 - 刷新数据
    refreshData();
  } else if (state === 'background') {
    // 应用进入后台 - 保存状态
    saveState();
  }
});

键盘处理

import { KeyboardAvoidingView, Platform } from 'react-native';

// 包装表单以避免键盘
<KeyboardAvoidingView 
  behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
  style={{ flex: 1 }}
>
  <YourForm />
</KeyboardAvoidingView>

// 收起键盘
import { Keyboard } from 'react-native';
Keyboard.dismiss();

// 监听键盘事件
import { useEffect } from 'react';
import { Keyboard } from 'react-native';

function useKeyboardVisible() {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const showSub = Keyboard.addListener('keyboardDidShow', () => setVisible(true));
    const hideSub = Keyboard.addListener('keyboardDidHide', () => setVisible(false));
    return () => {
      showSub.remove();
      hideSub.remove();
    };
  }, []);

  return visible;
}

常见问题

问题 解决方案
相机黑屏 检查权限,确保 CameraView 有明确的尺寸
通知未接收 验证物理设备,检查推送令牌注册
位置不准确 使用 Accuracy.High,检查设备位置服务是否启用
触觉反馈不工作 只在物理设备上工作,不在模拟器上
SecureStore 大小限制 每项最大 ~2KB,用于令牌不用于大数据