S3上传进度异常:仅显示100%无连续更新(MERN栈)
解决MERN栈S3文件上传进度不连续更新问题
问题背景
基于MERN栈开发S3文件上传功能时,React端的上传进度仅在完成时显示100%,无法从0到100连续更新。原Node.js和React代码如下:
原Node.js代码
exports.uploadFile2 = async (req, res) => { try { const file = req.files.file; const fileName = file.name; const parts = fileName.split("."); const extension = parts[parts.length - 1]; const params = { Bucket: "buckname", Key: `${uuidv4()}.${extension}`, Body: file.data, ACL: "public-read", ContentEncoding: "7bit", ContentType: file.mimetype, }; s3.upload(params, (err, data) => { if (err) { console.log(err); } console.log(data.Location); res.json(data); }).on("httpUploadProgress", (p) => { console.log(p); }); } catch (error) { console.log(error); res.status(400).json({ error: "No User Found", }); } };
原React代码
import axios from "axios"; import React from "react"; import { fileUploadFunction } from "../function/fileUpload"; import { useSelector } from "react-redux"; import { useRouter } from "next/router"; const index = () => { const { auth } = useSelector((state) => ({ ...state })); const router = useRouter(); const handleUploadFile = async (e) => { if (auth.token) { try { const file = e.target.files[0]; const formData = new FormData(); formData.append("file", file); const { data } = await axios.post( `http://localhost:5000/api/uploadFile2`, formData, { headers: { authtoken: auth && auth.token, }, onUploadProgress: (e) => { const { loaded, total } = e; console.log(Math.round((100 * loaded) / total)); }, } ); // if (data) router.push(`chat/${data}`); } catch (error) { console.log(error); } } }; return ( <div> <h1>Upload file</h1> <input type="file" onChange={handleUploadFile} /> </div> ); }; export default index;
问题原因
当前代码中,后端使用了req.files(依赖express-fileupload这类中间件),该中间件会先把文件完整接收并存储在服务器内存/临时目录,之后才开始向S3上传。因此React端的onUploadProgress监听的是文件从浏览器传到后端服务器的进度,当文件全量传到后端后,前端直接显示100%,但此时后端才刚开始向S3上传,前端无法获取这部分的进度。
解决方案
方案1:后端通过SSE推送S3上传进度
修改后端代码,使用服务器发送事件(SSE) 将S3的上传进度实时推送给前端,前端通过响应流接收进度数据。
修改后的Node.js代码
const { v4: uuidv4 } = require('uuid'); exports.uploadFile2 = async (req, res) => { try { // 设置SSE响应头 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders(); // 立即发送头部 const file = req.files.file; const fileName = file.name; const parts = fileName.split("."); const extension = parts[parts.length - 1]; const params = { Bucket: "buckname", Key: `${uuidv4()}.${extension}`, Body: file.data, ACL: "public-read", ContentEncoding: "7bit", ContentType: file.mimetype, }; const upload = s3.upload(params, (err, data) => { if (err) { console.log(err); res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); res.end(); return; } // 上传完成,发送结果 res.write(`data: ${JSON.stringify({ complete: true, location: data.Location })}\n\n`); res.end(); }); // 监听S3上传进度,推送给前端 upload.on("httpUploadProgress", (p) => { const progress = Math.round((p.loaded / p.total) * 100); res.write(`data: ${JSON.stringify({ progress })}\n\n`); res.flush(); // 立即发送数据 }); // 处理客户端断开连接,终止S3上传 req.on('close', () => { upload.abort(); res.end(); }); } catch (error) { console.log(error); res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); res.end(); } };
修改后的React代码
import React, { useState } from "react"; import { useSelector } from "react-redux"; import { useRouter } from "next/router"; const index = () => { const { auth } = useSelector((state) => ({ ...state })); const router = useRouter(); const [uploadProgress, setUploadProgress] = useState(0); const handleUploadFile = (e) => { if (!auth.token) return; const file = e.target.files[0]; const formData = new FormData(); formData.append("file", file); // 发送文件并接收SSE进度 fetch(`http://localhost:5000/api/uploadFile2`, { method: "POST", headers: { authtoken: auth.token, }, body: formData, }).then(async (res) => { const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); // 解析SSE格式的数据 const dataParts = chunk.split("\n\n").filter(part => part.startsWith("data:")); dataParts.forEach(part => { const dataStr = part.replace("data: ", ""); try { const data = JSON.parse(dataStr); if (data.progress !== undefined) { setUploadProgress(data.progress); console.log(data.progress); } else if (data.complete) { console.log("上传完成", data.location); // router.push(`chat/${data.location}`); } else if (data.error) { console.error("上传失败", data.error); } } catch (e) { console.error("解析进度数据失败", e); } }); } }).catch(err => { console.error("请求失败", err); }); }; return ( <div> <h1>Upload file</h1> <input type="file" onChange={handleUploadFile} /> {uploadProgress > 0 && <p>上传进度:{uploadProgress}%</p>} </div> ); }; export default index;
方案2:前端直传S3(推荐)
跳过后端中转,前端直接向S3上传文件,可直接监听S3的上传进度,更高效且进度准确。
后端生成预签名URL代码
exports.getS3PresignedUrl = async (req, res) => { try { // 验证用户权限(根据业务逻辑调整) const { fileName, fileType } = req.body; const parts = fileName.split("."); const extension = parts[parts.length - 1]; const key = `${uuidv4()}.${extension}`; const params = { Bucket: "buckname", Key: key, Expires: 3600, // URL有效期1小时 ContentType: fileType, ACL: "public-read", }; const url = s3.getSignedUrl('putObject', params); res.json({ url, key }); } catch (error) { console.log(error); res.status(400).json({ error: error.message }); } };
前端直传代码
import axios from "axios"; import React, { useState } from "react"; import { useSelector } from "react-redux"; import { useRouter } from "next/router"; const index = () => { const { auth } = useSelector((state) => ({ ...state })); const router = useRouter(); const [uploadProgress, setUploadProgress] = useState(0); const handleUploadFile = async (e) => { if (!auth.token) return; const file = e.target.files[0]; try { // 第一步:向后端请求预签名URL const { data } = await axios.post( `http://localhost:5000/api/getS3PresignedUrl`, { fileName: file.name, fileType: file.type, }, { headers: { authtoken: auth.token }, } ); // 第二步:用预签名URL直接上传到S3 await axios.put(data.url, file, { headers: { "Content-Type": file.type, }, onUploadProgress: (e) => { const progress = Math.round((100 * e.loaded) / e.total); setUploadProgress(progress); console.log(progress); }, }); // 上传完成,获取文件地址 const fileUrl = `https://buckname.s3.amazonaws.com/${data.key}`; console.log("上传完成", fileUrl); // router.push(`chat/${fileUrl}`); } catch (error) { console.error("上传失败", error); } }; return ( <div> <h1>Upload file</h1> <input type="file" onChange={handleUploadFile} /> {uploadProgress > 0 && <p>上传进度:{uploadProgress}%</p>} </div> ); }; export default index;
关键说明
- 方案1适合必须经过后端处理文件的场景(如验证文件内容、记录上传日志),但需要维护SSE连接。
- 方案2更高效,减少后端压力,进度监听更直接,是S3上传的推荐实现方式。
内容的提问来源于stack exchange,提问作者Venkatesh
相关产品推荐
相关产品推荐

