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
解决方案
前端修复
- 改用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)); } }); }
- 修正文件输入字段名:将文件输入框的
name属性改为files,和后端参数对应:
<Form.Control type='file' name='files' multiple onChange={(e) => setAssignmentData({ ...assignmentData, files: e.target.files }) } />
后端修复
- 配置ActiveStorage多文件关联:在
Assignment模型中添加多附件关联:
# app/models/assignment.rb class Assignment < ApplicationRecord belongs_to :tutor belongs_to :student has_many_attached :files # 多文件存储关联 end
- 更新参数允许规则:允许数组形式的
files参数:
private def new_assignment_params params.permit(:name, :subject, :notes, :tutor_id, :student_id, files: []) end
- 完善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
- 确认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
相关产品推荐
相关产品推荐

