NextJS 15中Axios调用Server Action实现文件上传进度展示问题
问题分析与解决方案
核心错误原因
你当前的代码存在两个关键问题:
- Axios请求路径错误:你把Server Action函数
uploadFileToS3直接传给了Axios的post方法第一个参数,而Axios期望的是请求URL。uploadFileToS3是异步函数,调用后返回Promise对象,被转成字符串[object Promise]作为请求路径,因此出现POST https://DEVSERVER/PAGE/[object%20Promise] 404错误。 - 进度监控逻辑错位:原Server Action是在服务器端完成文件到S3的上传,客户端仅负责把文件发送给Server Action——此时你能监控的是客户端到服务器的上传进度,而非服务器到S3的真实上传进度,且原写法无法触发Axios的
onUploadProgress回调。
解决方案一:用Axios调用API路由监控客户端到服务器进度
将Server Action逻辑迁移到API路由,用Axios正常调用并监控上传进度:
1. 创建API路由(替代Server Action)
// app/api/upload-to-s3/route.ts import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3" import { revalidatePath } from "next/cache" import { NextResponse } from "next/server" type S3Payload = { uploadDestination: string bucket: string } export async function POST(request: Request) { try { const formData = await request.formData() const payload = JSON.parse(formData.get("payload") as string) as S3Payload const s3 = new S3Client({ credentials: { accessKeyId: process.env.S3_ACCESS_KEY_ID!, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, }, region: process.env.S3_REGION!, }) const { uploadDestination, bucket } = payload const files = formData.getAll("file") as File[] const response = await Promise.all( files.map(async (file) => { const { type: fileType, size: fileSize } = file const fileExtension = fileType.split("/")[1] const fileID = `${uploadDestination}_${Date.now()}` const filePath = `${uploadDestination}/${fileID}.${fileExtension}` const arrayBuffer = await file.arrayBuffer() const buffer = Buffer.from(arrayBuffer) const uploadFileObject = { Bucket: bucket, Key: filePath, Body: buffer, ContentType: fileType, ContentLength: fileSize, } const uploadFile = new PutObjectCommand(uploadFileObject) return await s3.send(uploadFile) }) ) revalidatePath("/") return NextResponse.json({ success: true, data: response }) } catch (error) { console.error(error) return NextResponse.json({ success: false, error: "上传失败" }, { status: 500 }) } }
2. 客户端组件修改
// uploadPage/component.tsx import { useState } from 'react'; import { Dropzone } from '@mantine/dropzone'; import axios from 'axios'; const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setUploading] = useState(false); async function handleOnSubmit(files: File[]) { setUploading(true); setUploadProgress(0); const uploadDestination = mediaType; const s3Payload = { uploadDestination, bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME!, }; try { const formData = new FormData(); files.forEach((file) => formData.append("file", file)); formData.append("payload", JSON.stringify(s3Payload)); await axios.post("/api/upload-to-s3", formData, { headers: { "Content-Type": "multipart/form-data" }, maxContentLength: 2e10, maxBodyLength: 2e10, onUploadProgress: (event) => { if (event.total) { const progress = Math.round((event.loaded / event.total) * 100); setUploadProgress(progress); } }, }); } catch (error) { console.error("文件上传失败", error); } finally { setUploading(false); setUploadProgress(0); } } // 保留原Dropzone渲染代码
解决方案二:客户端直接用预签名URL上传到S3(推荐)
这种方式让客户端直接和S3交互,能监控真实的S3上传进度,同时减轻服务器压力:
1. 创建预签名URL生成API
// app/api/get-presigned-url/route.ts import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { NextResponse } from "next/server"; export async function POST(request: Request) { try { const { fileType, uploadDestination, bucket } = await request.json(); const s3 = new S3Client({ credentials: { accessKeyId: process.env.S3_ACCESS_KEY_ID!, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, }, region: process.env.S3_REGION!, }); const fileExtension = fileType.split("/")[1]; const fileID = `${uploadDestination}_${Date.now()}`; const filePath = `${uploadDestination}/${fileID}.${fileExtension}`; const command = new PutObjectCommand({ Bucket: bucket, Key: filePath, ContentType: fileType, }); const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); return NextResponse.json({ success: true, signedUrl }); } catch (error) { console.error(error); return NextResponse.json({ success: false, error: "生成预签名URL失败" }, { status: 500 }); } }
2. 客户端组件修改
// uploadPage/component.tsx import { useState } from 'react'; import { Dropzone } from '@mantine/dropzone'; import axios from 'axios'; const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setUploading] = useState(false); async function handleOnSubmit(files: File[]) { setUploading(true); setUploadProgress(0); const uploadDestination = mediaType; const bucket = process.env.NEXT_PUBLIC_S3_BUCKET_NAME!; try { await Promise.all(files.map(async (file, index) => { // 获取预签名URL const presignedRes = await axios.post("/api/get-presigned-url", { fileType: file.type, uploadDestination, bucket, }); const { signedUrl } = presignedRes.data; // 上传到S3并监控进度 await axios.put(signedUrl, file, { headers: { "Content-Type": file.type }, onUploadProgress: (event) => { if (event.total) { const fileProgress = Math.round((event.loaded / event.total) * 100); // 计算总进度(多文件平均分配) const totalProgress = Math.round((index * 100 + fileProgress) / files.length); setUploadProgress(totalProgress); } }, }); })); // 上传完成后刷新页面数据 await fetch("/api/revalidate", { method: "POST" }); } catch (error) { console.error("文件上传失败", error); } finally { setUploading(false); setUploadProgress(0); } } // 可选:创建刷新缓存的API // app/api/revalidate/route.ts import { revalidatePath } from "next/cache"; import { NextResponse } from "next/server"; export async function POST() { revalidatePath("/"); return NextResponse.json({ success: true }); } // 保留原Dropzone渲染代码
内容的提问来源于stack exchange,提问作者Donald Louch
相关产品推荐
相关产品推荐

