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

Rails+React多文件上传故障求助:无法向Rails后端传输文件

问题:React前端多文件上传至Rails后端失败及文件URL返回需求

我正在开发家教系统,导师可为学生上传含多文件的作业,学生后续可点击链接下载文件。目前遇到两个核心问题:

  • React前端无法将文件正常传输至Rails后端:要么触发byebug前报错,要么触发后params[:files]为空。
  • 需要实现将文件URL返回至前端的功能,当前系统已使用ActiveStorage存储用户头像。

前端代码

import React, { useState, useContext } from "react";
import { UserContext } from "./App";
import { useLocation, useNavigate } from "react-router-dom";
import { Container, Form, Row, Col, Button } from "react-bootstrap";

function AssignWork() {
  const [error, setError] = useState([]);
  const location = useLocation();
  const navigate = useNavigate();
  const { studentId } = location.state;
  const { currentUser, setStudents } = useContext(UserContext);
  const [assignmentData, setAssignmentData] = useState({
    name: "",
    notes: "",
    subject: "",
    tutor_id: currentUser.id,
    student_id: studentId,
    files: "",
  });

  function handleChange(e) {
    setAssignmentData({ ...assignmentData, [e.target.name]: e.target.value });
  }

  function onFormSubmit(e) {
    e.preventDefault();

    fetch("/assignments", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(assignmentData),
    }).then((resp) => {
      if (resp.ok) {
        resp.json().then((newAssignment) => {
          console.log(newAssignment);
          let studentsList = [
            ...new Map(
              newAssignment.tutor.students.map((student) => [
                student["id"],
                student,
              ])
            ).values(),
          ];
          setStudents(studentsList);
          navigate("/students");
        });
      } else {
        resp.json().then((error) => setError(error.errors));
      }
    });
  }

  return (
    <Container
      style={{
        marginTop: "24px",
        padding: "24px",
        border: ".5px solid grey",
        borderRadius: "8px",
        width: "75%",
      }}
    >
      <h2
        style={{
          border: ".5px solid grey",
          marginBottom: "24px",
          borderRadius: "8px",
          padding: "8px",
        }}
      >
        Assignment
      </h2>
      <Form onSubmit={onFormSubmit}>
        <Row>
          <Col>
            <Form.Label>Name:</Form.Label>
            <Form.Control
              type='text'
              name='name'
              value={assignmentData.name}
              onChange={handleChange}
            />
          </Col>
          <Col>
            <Form.Label>Subject:</Form.Label>
            <Form.Control
              type='text'
              name='subject'
              value={assignmentData.subject}
              onChange={handleChange}
            />
          </Col>
        </Row>
        <Row style={{ marginTop: "16px" }}>
          <Col>
            <Form.Label>Notes:</Form.Label>
            <Form.Control
              as='textarea'
              name='notes'
              value={assignmentData.notes}
              onChange={handleChange}
            />
          </Col>
        </Row>
        <Row style={{ marginTop: "16px" }}>
          <Col>
            <Form.Control
              type='file'
              name='file'
              multiple
              onChange={(e) =>
                setAssignmentData({ ...assignmentData, files: e.target.files })
              }
            />
          </Col>
        </Row>
        <Row
          style={{
            margin: "16px",
            justifyContent: "center",
          }}
        >
          <Button type='submit' variant='success' style={{ maxWidth: "33%" }}>
            Upload
          </Button>
        </Row>
      </Form>
    </Container>
  );
}

export default AssignWork;

后端代码

def create
  byebug
  # assignment = Assignment.create!(new_assignment_params)
  # assignment.update({ files: params[:files] })
  # render json: assignment, include: "tutor.students.assignments", status: :created
end

def destroy
  assignment_to_delete = find_assignment
  if assignment_to_delete.tutor_id == @current_user.id
    assignment_to_delete.destroy
    head :no_content
  else
    render json: { error: "You do not have permission to destroy this assignment." }, status: :unauthorized
  end
end

private

def new_assignment_params
  params.permit(:name, :subject, :notes, :tutor_id, :student_id, :files)
end

解决方案

前端修复

  1. 改用FormData传输文件:JSON无法直接传输二进制文件,必须使用FormData,且不要手动设置Content-Type头(浏览器会自动处理为multipart/form-data)。修改提交函数:
function onFormSubmit(e) {
  e.preventDefault();

  const formData = new FormData();
  // 添加普通表单字段
  formData.append('name', assignmentData.name);
  formData.append('subject', assignmentData.subject);
  formData.append('notes', assignmentData.notes);
  formData.append('tutor_id', assignmentData.tutor_id);
  formData.append('student_id', assignmentData.student_id);
  // 循环添加多文件,字段名用`files[]`匹配后端数组参数
  if (assignmentData.files) {
    Array.from(assignmentData.files).forEach(file => {
      formData.append('files[]', file);
    });
  }

  fetch("/assignments", {
    method: "POST",
    body: formData,
  }).then((resp) => {
    // 原有响应处理逻辑保持不变
    if (resp.ok) {
      resp.json().then((newAssignment) => {
        console.log(newAssignment);
        let studentsList = [
          ...new Map(
            newAssignment.tutor.students.map((student) => [
              student["id"],
              student,
            ])
          ).values(),
        ];
        setStudents(studentsList);
        navigate("/students");
      });
    } else {
      resp.json().then((error) => setError(error.errors));
    }
  });
}
  1. 修正文件输入字段名:将文件输入框的name属性改为files,和后端参数对应:
<Form.Control
  type='file'
  name='files'
  multiple
  onChange={(e) =>
    setAssignmentData({ ...assignmentData, files: e.target.files })
  }
/>

后端修复

  1. 配置ActiveStorage多文件关联:在Assignment模型中添加多附件关联:
# app/models/assignment.rb
class Assignment < ApplicationRecord
  belongs_to :tutor
  belongs_to :student
  has_many_attached :files # 多文件存储关联
end
  1. 更新参数允许规则:允许数组形式的files参数:
private

def new_assignment_params
  params.permit(:name, :subject, :notes, :tutor_id, :student_id, files: [])
end
  1. 完善create动作并返回文件URL:创建Assignment后,通过rails_blob_url生成可访问的文件URL返回给前端:
def create
  assignment = Assignment.create!(new_assignment_params)
  # 生成每个文件的完整访问URL
  file_urls = assignment.files.map { |file| rails_blob_url(file, only_path: false) }
  # 返回包含文件URL的响应
  render json: { 
    assignment: assignment,
    file_urls: file_urls,
    tutor: assignment.tutor.as_json(include: { students: { include: :assignments } })
  }, status: :created
end
  1. 确认CORS配置:若前后端跨域,需在CORS配置中允许multipart/form-data请求:
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*' # 生产环境替换为实际前端域名
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      expose: ['Content-Disposition']
  end
end

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 16:23:07