You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 08:59:52