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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 08:47:01