React Native使用state传Base64上传图片时出现JSON解析错误
问题
手动将Base64字符串定义为const常量时,代码可正常运行;但改用React Native的state存储并传递Base64数据至Node.js后端时,出现错误:SyntaxError: JSON Parse error: Unexpected token: <。技术栈为前端Expo(React Native)、后端Express+PostgreSQL。
前端代码
import React, { Component } from "react"; import { StyleSheet, Text, View, PixelRatio, TouchableOpacity, Image, TextInput, } from "react-native"; import { Camera } from "expo-camera"; import * as ImagePicker from "expo-image-picker"; export default class Project extends Component { constructor() { super(); this.state = { ImageSource: null, data: null, Image_TAG: "", ImageData: null, imageInfo: null, ServerImage: null, }; } getPermissionAsync = async () => { // Camera roll Permission if (Platform.OS === "android") { const { status } = await Permissions.askAsync(Permissions.CAMERA_ROLL); if (status !== "granted") { alert("Sorry, we need camera roll permissions to make this work!"); } } // Camera Permission const { status } = await Permissions.askAsync( Permissions.CAMERA, Permissions.AUDIO_RECORDING ); this.setState({ hasPermission: status === "granted" }); }; handleCameraType = () => { const { cameraType } = this.state; this.setState({ cameraType: cameraType === Camera.Constants.Type.back ? Camera.Constants.Type.front : Camera.Constants.Type.back, }); }; pickImage = async () => { let response = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 1, base64: true, }); if (!response.canceled) { this.setState({ ImageSource: response.uri, data: response.data, ImageData: response.assets[0].base64, imageInfo: response.assets[0], }); } }; getImage = async () => { const mybody = {}; const response = await fetch( `https://coral-app-n96bn.ondigitalocean.app/groupchat/getimage`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", }, body: mybody, } ); const jsonData = await response.json(); this.setState({ ServerImage: jsonData[0].image_base64, }); }; uploadImageToServer = async () => { let apiUrl = `https://coral-app-n96bn.ondigitalocean.app/groupchat/uploadimage`; const uri = this.state.ImageSource; const myBase64 = this.state.ImageData; let options = { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ uri: uri, base64: myBase64 }), }; return fetch(apiUrl, options) .then((response) => response.json()) .then((response) => { console.log("status: " + response.status); console.log("body: " + JSON.stringify(response, null, 4)); }) .catch((err) => { console.log("ERROR: " + err); }); }; render() { return ( <View style={styles.container}> <TouchableOpacity onPress={this.pickImage.bind(this)}> <View style={styles.ImageContainer}> {this.state.ImageSource === null ? ( <Text>Select a Photo</Text> ) : ( <Image style={styles.ImageContainer} source={{ uri: this.state.ImageSource }} /> )} </View> </TouchableOpacity> <TextInput placeholder="Enter Image Name " onChangeText={(data) => this.setState({ Image_TAG: data })} underlineColorAndroid="transparent" style={styles.TextInputStyle} /> <TouchableOpacity onPress={this.uploadImageToServer} activeOpacity={0.6} style={styles.button} > <Text style={styles.TextStyle}> UPLOAD IMAGE TO SERVER </Text> </TouchableOpacity> <TouchableOpacity onPress={this.getImage} activeOpacity={0.6} style={styles.button} > <Text style={styles.TextStyle}> Get Image from SERVER </Text> </TouchableOpacity> <View style={styles.ImageContainer}> {this.state.ServerImage === null ? ( <Text>Server photo will appear here</Text> ) : ( <Image style={styles.ImageContainer} source={{ uri: `data:image/jpeg;base64,${this.state.ServerImage}`, }} /> )} </View> <View style={styles.ImageContainer}> {this.state.ImageData === null ? ( <Text>Selected photo will appear here</Text> ) : ( <Image style={styles.ImageContainer} source={{ uri: this.state.ImageData }} /> )} </View> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", backgroundColor: "#FFF8E1", paddingTop: 20, }, ImageContainer: { borderRadius: 10, width: 250, height: 250, borderColor: "#9B9B9B", borderWidth: 1 / PixelRatio.get(), justifyContent: "center", alignItems: "center", backgroundColor: "#CDDC39", }, TextInputStyle: { textAlign: "center", height: 40, width: "80%", borderRadius: 10, borderWidth: 1, borderColor: "#028b53", marginTop: 20, }, button: { width: "80%", backgroundColor: "#00BCD4", borderRadius: 7, marginTop: 20, }, TextStyle: { color: "#fff", textAlign: "center", padding: 10, }, });
后端控制器代码
module.exports.uploadImage = async (req, res) => { const uploadImageQuery = await pool.query( "INSERT INTO practice_pics(image, image_base64) values($1, $2) RETURNING id", [req.body.uri, req.body.base64] ); const newImage = uploadImageQuery.rows[0].id; res.json({ ...req.body, body: req.body, status: newImage, }); };
问题分析与解决
核心原因
SyntaxError: JSON Parse error: Unexpected token: < 本质是后端返回的不是JSON格式数据,而是HTML内容(比如错误页面、404/500响应),导致前端response.json()解析失败。以下是具体排查和修复步骤:
1. 确认后端返回的原始内容
修改前端uploadImageToServer方法,先打印后端返回的原始文本,定位具体错误:
uploadImageToServer = async () => { let apiUrl = `https://coral-app-n96bn.ondigitalocean.app/groupchat/uploadimage`; const uri = this.state.ImageSource; const myBase64 = this.state.ImageData; let options = { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ uri: uri, base64: myBase64 }), }; return fetch(apiUrl, options) .then((response) => { console.log('响应状态码:', response.status); return response.text(); // 先获取原始文本 }) .then((rawText) => { console.log('后端返回原始内容:', rawText); try { const jsonData = JSON.parse(rawText); console.log('解析后的JSON:', jsonData); } catch (e) { console.log('解析错误:', e); } }) .catch((err) => { console.log("请求错误: " + err); }); };
打印的HTML内容会直接暴露错误原因(比如数据库连接失败、字段长度不够、CORS跨域错误等)。
2. 检查后端数据库配置
- 数据库连接:确认
pool已正确初始化并连接到PostgreSQL,若连接失败,Express会返回默认的HTML错误页面。 - 字段类型:
practice_pics表的image_base64字段需设置为TEXT类型(Base64字符串长度远超VARCHAR的默认上限),若用BYTEA则需额外处理二进制转换,建议直接用TEXT存储原始Base64字符串。
3. 修复前端权限模块问题
代码中Permissions未导入,且Expo已更新权限获取方式,修改getPermissionAsync:
// 先导入权限模块 import * as Permissions from 'expo-permissions'; getPermissionAsync = async () => { // 相册权限 if (Platform.OS === "android") { const { status } = await Permissions.askAsync(Permissions.CAMERA_ROLL); if (status !== "granted") { alert("需要相册权限才能使用此功能!"); } } // 相机权限 const { status } = await Permissions.askAsync(Permissions.CAMERA); this.setState({ hasPermission: status === "granted" }); };
或使用新版ImagePicker自带的权限请求:
// 替换为这个 const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
4. 配置后端CORS跨域
若前端和后端域名不同,需在Express中配置CORS允许跨域请求及凭证:
const cors = require('cors'); // 替换为你的前端实际地址(比如Expo调试地址或正式域名) const allowedOrigin = 'http://localhost:19006'; app.use(cors({ origin: allowedOrigin, credentials: true }));
未配置CORS会导致请求被拦截,后端返回HTML格式的跨域错误页面。
5. 验证Base64数据完整性
在pickImage和uploadImageToServer中分别打印Base64字符串的前100个字符,确认state存储后数据未被篡改:
// pickImage中添加 console.log('选中图片的Base64前缀:', response.assets[0].base64.slice(0, 100)); // uploadImageToServer中添加 console.log('state中的Base64前缀:', myBase64.slice(0, 100));
内容的提问来源于stack exchange,提问作者JohnBradens
相关产品推荐
相关产品推荐

