React Native:上传相机拍摄照片至API时应用崩溃问题
问题描述
React Native应用在Android模拟器中运行正常,但在Galaxy A30真机上,用户尝试上传相机拍摄的照片到API时会崩溃。从相册选择照片提交无异常,模拟器中无报错输出,仅相机上传场景出现崩溃。
相关代码
import React, {useState, useContext} from 'react'; import {ScrollView, View, Text, TextInput, TouchableOpacity, Alert} from 'react-native'; import { AuthContext } from '../Context/AuthContext'; import { URLs } from '../constants/links'; import * as ImagePicker from 'expo-image-picker'; import axios from 'axios'; import * as Permissions from "expo-permissions"; import { CAMERA } from "expo-permissions"; const MyScreen = ({navigation}) => { const { myToken } = useContext(AuthContext) const [allImage, setAllImage] = React.useState([]); const [pickedImage, setPickedImage] = useState(""); const [fileName, setFileName] = React.useState(""); const formdata = new FormData(); const cameraPermission = async () => { const result = await Permissions.askAsync(CAMERA); if (result.status != "granted") { Alert.alert( "Insufficient Permission", "You need to grant camera permission to use this app", [{ text: "Okay" }] ); return true; } return true; }; const useCamera = async () => { const hasPermissions = await cameraPermission(); if (!hasPermissions) { return; } if(allImage.length < 4){ let result = await ImagePicker.launchCameraAsync({ allowsEditing: true, quality: 0.5, }); if (!result.cancelled) { const name = result.uri.split('/').pop(); let match = /\.(\w+)$/.exec(name); let type = match ? `image/${match[1]}` : `image`; let newFile = { uri: result.uri, type: type, name: name } setAllImage(newFile) setPickedImage(result.uri) if (!pickedImage && allImage.length === 0) { setAllImage([newFile]); setFileName("Photo 1") }else { setAllImage([...allImage, newFile]); setFileName(fileName + ", Photo " + (allImage.length + 1)) } } } else { Alert.alert("Image", "You have reach the image upload limit"); } }; const fetchData = () => { const abortCont = new AbortController(); allImage.forEach((file) => { formdata.append('files[]', file); }); axios({ method: 'post', url: URLs, headers: { Accept: "application/json", Authorization: myToken, 'Content-Type': "multipart/form-data", }, data: formdata, signal: abortCont.signal, }).then(function (result) { if(result.data.message === "Successfully added") { Alert.alert("Upload Successufull", result.data.message); navigation.goBack() } }).catch(function (error) { Alert.alert("Error", error); formdata = new FormData(); }); return () => abortCont.abort(); } return ( <ScrollView> <View> <View> <Text>Attach Receipt File</Text> <View> <TextInput editable={false} placeholder="Select files.." value={fileName} /> </View> <View> <TouchableOpacity activeOpacity={0.8} onPress={useCamera}> <Text>Camera</Text> </TouchableOpacity> </View> <View> <TouchableOpacity activeOpacity={0.9} onPress={fetchData}> <Text>Submit</Text> </TouchableOpacity> </View> </View> </View> </ScrollView> ); } export default MyScreen;
问题排查与修复方案
核心问题分析
- FormData实例复用异常:
formdata定义在组件顶层,每次提交都会往同一实例追加文件,导致数据混乱;catch块中重新赋值formdata会引发变量绑定异常,无法清空原有数据。 - 数组状态被覆盖为对象:
useCamera中存在setAllImage(newFile),直接将数组状态改为单个文件对象,后续allImage.forEach会因对象无此方法抛出错误,真机环境下直接触发崩溃。 - 废弃权限API兼容性问题:
expo-permissions已被废弃,旧权限请求方法在部分Android真机上可能存在权限获取不完整的情况。 - Android相机URI格式问题:相机返回的
content://格式URI在FormData中无法被正确解析,导致上传失败崩溃。
修复步骤
在fetchData内部创建FormData
每次提交时新建FormData实例,避免数据复用:const fetchData = () => { const abortCont = new AbortController(); const formdata = new FormData(); // 移到内部创建 allImage.forEach((file) => { formdata.append('files[]', file); }); // 后续请求代码不变 }修复useCamera中的状态错误
删除错误的setAllImage(newFile)行,确保allImage始终保持数组类型:const useCamera = async () => { // 权限判断代码不变 if (!result.cancelled) { // newFile创建代码不变 // 删除 setAllImage(newFile) 这一行 setPickedImage(result.uri) if (allImage.length === 0) { setAllImage([newFile]); setFileName("Photo 1") }else { setAllImage([...allImage, newFile]); setFileName(fileName + ", Photo " + (allImage.length + 1)) } } };替换为最新的权限请求API
使用expo-image-picker自带的权限方法,替代已废弃的expo-permissions:// 删除 import * as Permissions from "expo-permissions"; 和 import { CAMERA } from "expo-permissions"; const cameraPermission = async () => { const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== "granted") { Alert.alert( "权限不足", "需要授予相机权限才能使用此功能", [{ text: "确定" }] ); return false; // 权限未授予返回false } return true; };处理Android相机URI格式
安装expo-file-system,将content://URI转换为应用可访问的文件路径:npx expo install expo-file-system在
useCamera中处理URI:import * as FileSystem from 'expo-file-system'; import { Platform } from 'react-native'; const useCamera = async () => { // 权限判断和相机调用代码不变 if (!result.cancelled) { let uri = result.uri; // 处理Android的content:// URI if (Platform.OS === 'android' && uri.startsWith('content://')) { const fileInfo = await FileSystem.getInfoAsync(uri); uri = fileInfo.uri; // 转换为file://格式 } // newFile创建和状态更新代码不变 } };优化状态逻辑(可选)
移除冗余的pickedImage状态,直接通过allImage数组管理选中图片,简化逻辑:// 移除 const [pickedImage, setPickedImage] = useState(""); const useCamera = async () => { // 代码不变,仅删除与pickedImage相关的判断和赋值 if (!result.cancelled) { // newFile创建代码不变 if (allImage.length === 0) { setAllImage([newFile]); setFileName("Photo 1") }else { setAllImage([...allImage, newFile]); setFileName(fileName + ", Photo " + (allImage.length + 1)) } } };
修复后完整代码
import React, {useState, useContext} from 'react'; import {ScrollView, View, Text, TextInput, TouchableOpacity, Alert, Platform} from 'react-native'; import { AuthContext } from '../Context/AuthContext'; import { URLs } from '../constants/links'; import * as ImagePicker from 'expo-image-picker'; import axios from 'axios'; import * as FileSystem from 'expo-file-system'; const MyScreen = ({navigation}) => { const { myToken } = useContext(AuthContext) const [allImage, setAllImage] = React.useState([]); const [fileName, setFileName] = React.useState(""); const cameraPermission = async () => { const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== "granted") { Alert.alert( "权限不足", "需要授予相机权限才能使用此功能", [{ text: "确定" }] ); return false; } return true; }; const useCamera = async () => { const hasPermissions = await cameraPermission(); if (!hasPermissions) { return; } if(allImage.length < 4){ let result = await ImagePicker.launchCameraAsync({ allowsEditing: true, quality: 0.5, }); if (!result.cancelled) { let uri = result.uri; if (Platform.OS === 'android' && uri.startsWith('content://')) { const fileInfo = await FileSystem.getInfoAsync(uri); uri = fileInfo.uri; } const name = uri.split('/').pop(); let match = /\.(\w+)$/.exec(name); let type = match ? `image/${match[1]}` : `image`; let newFile = { uri: uri, type: type, name: name } if (allImage.length === 0) { setAllImage([newFile]); setFileName("Photo 1") }else { setAllImage([...allImage, newFile]); setFileName(fileName + ", Photo " + (allImage.length + 1)) } } } else { Alert.alert("提示", "已达到图片上传上限"); } }; const fetchData = () => { const abortCont = new AbortController(); const formdata = new FormData(); allImage.forEach((file) => { formdata.append('files[]', file); }); axios({ method: 'post', url: URLs, headers: { Accept: "application/json", Authorization: myToken, 'Content-Type': "multipart/form-data", }, data: formdata, signal: abortCont.signal, }).then(function (result) { if(result.data.message === "Successfully added") { Alert.alert("上传成功", result.data.message); navigation.goBack() } }).catch(function (error) { Alert.alert("错误", error.message || "上传失败"); }); return () => abortCont.abort(); } return ( <ScrollView> <View style={{padding: 16}}> <View> <Text style={{fontSize: 16, marginBottom: 8}}>Attach Receipt File</Text> <View style={{borderWidth: 1, borderColor: '#ccc', borderRadius: 4, padding: 8, marginBottom: 16}}> <TextInput editable={false} placeholder="Select files.." value={fileName} /> </View> <View style={{marginBottom: 16}}> <TouchableOpacity activeOpacity={0.8} onPress={useCamera} style={{backgroundColor: '#e0e0e0', padding: 12, borderRadius: 4}}> <Text style={{textAlign: 'center'}}>Camera</Text> </TouchableOpacity> </View> <View> <TouchableOpacity activeOpacity={0.9} onPress={fetchData} style={{backgroundColor: '#2196F3', padding: 12, borderRadius: 4}}> <Text style={{textAlign: 'center', color: 'white'}}>Submit</Text> </TouchableOpacity> </View> </View> </View> </ScrollView> ); } export default MyScreen;
内容的提问来源于stack exchange,提问作者Nomel
相关产品推荐
相关产品推荐

