使用FormData上传图片失败求助(附React+Redux代码)
问题:FormData上传图片失败
我尝试通过POST请求用FormData上传图片,但一直无法正常工作。
以下是Profile.jsx中的代码,功能是从本地选择图片后,点击提交按钮将文件发送至updateCurrentUserAvatar接口完成上传:
import React, { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getCurrentUserAvatar, setImage, updateCurrentUserAvatar } from '~/features/superAdminSlice'; import Spinner from '~/layouts/components/Spinner'; const SuperAdminAccountProfile = () => { const dispatch = useDispatch(); const userAvatar = useSelector((state) => state.superadmin.avatar); const [imageProfile, setImageProfile] = useState(userAvatar); const [loading, setLoading] = useState(true); useEffect(() => { dispatch(getCurrentUserAvatar()); setImageProfile(userAvatar); setLoading(false); }, [dispatch, userAvatar]); const handleButtonClick = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.style.opacity = 0; input.style.height = 0; input.onchange = (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = () => { setImageProfile(reader.result); }; reader.readAsDataURL(file); setImage(file); }; document.body.appendChild(input); input.click(); document.body.removeChild(input); }; const handleSubmit = (event) => { event.preventDefault(); if (imageProfile) { dispatch(updateCurrentUserAvatar(imageProfile)); } }; return ( <div style={{ position: 'relative', width: '200px', height: '200px' }}> {loading ? ( <Spinner /> ) : ( <> <img src={imageProfile} alt="" style={{ width: '100%', height: '100%' }} /> <button type="button" style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', visibility: 'visible', }} onClick={handleButtonClick} > Change Image </button> </> )} <button type="submit" onClick={handleSubmit}> Submit </button> </div> ); }; export default SuperAdminAccountProfile;
以下是包含updateCurrentUserAvatar方法的Slice.js代码:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import axios from '~/api/axios'; import { getAdmins, register, updateStatus } from '~/services/superAdminService'; const initialState = { data: [], admins: [], isLocked: false, currentPage: 1, pageSize: 5, search: '', adminId: '', count: '', avatar: null, }; export const getAdminData = createAsyncThunk('superadmin/adminList', async ({ currentPage, pageSize, search }) => { try { const response = await getAdmins(currentPage, pageSize, search); return response; } catch (error) { console.log(error); } }); export const updateAdminStatus = createAsyncThunk('superadmin/updateData', async (adminId) => { try { const response = await updateStatus(adminId); return response; } catch (error) { console.log(error); } }); export const createAdmin = createAsyncThunk('superadmin/createAdmin', async (payload) => { try { const response = await register({ email: payload.email, fullname: payload.fullname, username: payload.username, password: payload.password, confirmPassword: payload.confirmPassword, }); return response; } catch (error) { console.log(error); } }); export const getCurrentUserAvatar = createAsyncThunk('superadmin/getAvatar', async () => { try { const response = await axios.get('User/avatar'); return response.url; } catch (error) { console.log(error); } }); export const updateCurrentUserAvatar = createAsyncThunk('superadmin/updateAvatar', async (avatar) => { try { const formData = new FormData(); formData.append('avatar', avatar); const response = await axios.post('User/avatar', formData, { headers: { 'Content-type': 'multipart/form-data' }, }); console.log(response); return response; } catch (error) { console.log(error); } }); export const superAdminSlice = createSlice({ name: 'superadmin', initialState, reducers: { setPage: (state, action) => { state.currentPage = action.payload; }, setSearch: (state, action) => { state.search = action.payload; state.currentPage = 1; }, setLock: (state, action) => { state.isLocked = action.payload; }, setImage: (state, action) => { console.log(action); state.avatar = action.payload; }, }, extraReducers: (builder) => { builder .addCase(getAdminData.fulfilled, (state, action) => { state.data = action.payload.data; state.count = action.payload.count; }) .addCase(updateAdminStatus.fulfilled, (state, action) => { state.adminId = action.payload.adminId; state.isLocked = action.payload.isLocked; }) .addCase(createAdmin.fulfilled, (state, action) => { state.admins.push(action.payload); }) .addCase(getCurrentUserAvatar.fulfilled, (state, action) => { state.avatar = action.payload; }) .addCase(updateCurrentUserAvatar.fulfilled, (state, action) => { state.avatar = action.payload; }); }, }); export const { setPage, setSearch, setLock, setImage } = superAdminSlice.actions; export default superAdminSlice.reducer;
问题分析与修复
核心问题是上传时传递的是DataURL字符串,而非实际的File对象:
handleSubmit中传给updateCurrentUserAvatar的imageProfile是FileReader.readAsDataURL生成的base64字符串,不是用户选择的File文件。- 虽然用
setImage(file)把File对象存在了redux状态,但提交时并未使用该值。
修复步骤:
1. 修改Profile.jsx的提交逻辑
直接使用redux中存储的File对象提交:
const handleSubmit = (event) => { event.preventDefault(); const file = useSelector((state) => state.superadmin.avatar); if (file) { dispatch(updateCurrentUserAvatar(file)); } };
或者在组件内维护File状态,更直观:
// 新增组件内状态 const [selectedFile, setSelectedFile] = useState(null); // 修改handleButtonClick const handleButtonClick = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.style.opacity = 0; input.style.height = 0; input.onchange = (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = () => { setImageProfile(reader.result); }; reader.readAsDataURL(file); setSelectedFile(file); // 存储File对象 }; document.body.appendChild(input); input.click(); document.body.removeChild(input); }; // 修改提交方法 const handleSubmit = (event) => { event.preventDefault(); if (selectedFile) { dispatch(updateCurrentUserAvatar(selectedFile)); } };
2. 优化axios请求(推荐)
传递FormData时,无需手动设置Content-type: multipart/form-data,axios会自动生成包含boundary的正确请求头,手动设置反而可能导致格式错误:
export const updateCurrentUserAvatar = createAsyncThunk('superadmin/updateAvatar', async (avatar) => { try { const formData = new FormData(); formData.append('avatar', avatar, avatar.name); // 可选:指定文件名 const response = await axios.post('User/avatar', formData); console.log(response); // 后端若返回图片URL,用该URL更新状态 return response.data.url; } catch (error) { console.log(error); } });
3. 修正useEffect逻辑
原逻辑可能在请求完成前执行setImageProfile,导致初始值为null,拆分后更可靠:
useEffect(() => { const fetchAvatar = async () => { await dispatch(getCurrentUserAvatar()); setLoading(false); }; fetchAvatar(); }, [dispatch]); // 监听userAvatar变化自动更新预览图 useEffect(() => { setImageProfile(userAvatar); }, [userAvatar]);
内容的提问来源于stack exchange,提问作者Quang Phú Nguyễn Hoàng
相关产品推荐
相关产品推荐

