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

React Native Expo多文件上传异常求助:FormData与fetch调用问题

React Native (Expo) 文件上传问题:FormData 提交至 Node.js/Express 后端异常

需求目标

  • 允许用户通过expo-document-picker选择文件,可为每个文件添加可选描述
  • 通过FormData以POST请求发送文件及对应描述到服务器
  • 确保后端能正确处理并完成文件上传

当前前端实现代码

if (!files || files.length === 0) {
  alert("No files selected. Please select at least one file to upload.");
  return;
}

if (Object.keys(descriptions).length !== files.length) {
  alert("Each file must have a corresponding description.");
  return;
}

setLoading(true);

try {
  const formData = new FormData();

  files.forEach((file, index) => {
    formData.append("files", {
      uri: file.uri.startsWith("file://") ? file.uri : `file://${file.uri}`, // Ensure proper URI
      name: file.name || `file_${index}`, // Assign name
      type: file.type || "application/octet-stream", // Default MIME type
    });

    formData.append("descriptions", descriptions[index] || ""); // Append descriptions
  });

  // Debugging FormData
  console.log("FormData before sending:");
  for (const pair of formData.entries()) {
    console.log(pair[0], pair[1]);
  }

  // API Request
  const response = await fetch(
    `${process.env.EXPO_PUBLIC_API_URL}/v1/assets/assignments/${assignment_id}`,
    {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${user.token}`, // Add auth token only
      },
      body: formData, // FormData automatically includes the boundary
    }
  );

  if (!response.ok) {
    const errorText = await response.text();
    console.error("Backend Response Error:", errorText);
    alert(`Error from server: ${errorText}`);
    return;
  }

  alert("Files uploaded successfully!");
  navigation.goBack();
} catch (error) {
  console.error("Error uploading files:", error);
  alert(`Error uploading files: ${error.message}`);
} finally {
  setLoading(false);
}

遇到的问题

  • 文件未被服务器接收,或fetch请求直接失败
  • 服务器偶尔报告文件数据缺失或损坏
  • FormData未按预期传递数据

已完成的调试步骤

  • 确认expo-document-picker能正确返回文件元数据(uri、name、type等)
  • 通过console.log验证FormData已正确追加文件及描述项
  • 尝试手动设置Content-Type: multipart/form-data头,但因fetch自动生成带boundary的头导致冲突,问题更严重

环境信息

  • Expo SDK: ~51.0.28
  • React Native: 0.74.5
  • 后端:Node.js/Express

核心疑问

  1. React Native(Expo)中使用fetch和FormData上传文件的正确姿势是什么?
  2. 文件URI(如file://或base64格式)有哪些需要注意的坑?
  3. 如何确保后端能同时正确接收文件数据和对应的描述信息?

解决方案

1. Expo中FormData上传文件的正确写法

  • 禁止手动添加Content-Type头:fetch会自动根据FormData生成包含boundary的multipart/form-data头,手动添加会丢失boundary,导致后端无法解析。
  • 优化FormData追加逻辑:当前代码中文件和描述分开追加,容易出现顺序匹配问题,建议用下标关联每组文件和描述:
    import { Platform } from 'react-native';
    
    files.forEach((file, index) => {
      // 处理安卓/iOS URI差异
      const fileUri = Platform.OS === 'android' 
        ? file.uri 
        : file.uri.replace('file://', '');
      
      // 用下标绑定文件和描述,后端可直接通过下标对应
      formData.append(`files[${index}]`, {
        uri: fileUri,
        name: file.name || `file_${index}`,
        type: file.type || 'application/octet-stream',
      });
      formData.append(`descriptions[${index}]`, descriptions[index] || '');
    });
    

2. 文件URI的注意事项

  • 平台差异处理:iOS返回的URI带file://前缀,上传时需要去掉;安卓直接使用返回的URI即可。
  • 优先使用原生URI:避免将文件转成base64上传,会增加33%左右的数据体积,且处理速度慢。
  • 配置文件权限:在app.json中添加必要权限,确保应用能访问文件:
    {
      "expo": {
        "ios": {
          "infoPlist": {
            "NSDocumentPickerUsageDescription": "需要访问您的文件以上传"
          }
        },
        "android": {
          "permissions": ["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]
        }
      }
    }
    

3. 后端(Express)正确接收文件和描述

  • 使用multer中间件:Express处理multipart/form-data必须依赖multer,先安装依赖:
    npm install multer
    
    后端配置示例:
    const express = require('express');
    const multer = require('multer');
    const router = express.Router();
    const fs = require('fs');
    
    // 确保上传目录存在
    const uploadDir = './uploads';
    if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);
    
    // 配置文件存储规则
    const storage = multer.diskStorage({
      destination: (req, file, cb) => cb(null, uploadDir),
      filename: (req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`)
    });
    
    const upload = multer({ storage });
    
    // 接收多文件上传接口
    router.post('/v1/assets/assignments/:id', upload.array('files'), (req, res) => {
      const files = req.files;
      const descriptions = req.body.descriptions;
    
      if (!files || files.length === 0) {
        return res.status(400).send('No files uploaded');
      }
    
      // 匹配文件和对应描述
      files.forEach((file, index) => {
        console.log(`文件:${file.filename},描述:${descriptions[index] || '无描述'}`);
        // 这里可以添加文件信息入库逻辑
      });
    
      res.status(200).send('文件上传成功');
    });
    
    module.exports = router;
    
  • 参数名匹配:前端FormData中文件的参数名是files[${index}],后端upload.array('files')会自动解析为数组;描述参数descriptions[${index}]会被解析为req.body.descriptions数组,顺序与文件一一对应。

内容的提问来源于stack exchange,提问作者Abdella Munir

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 05:29:51