Next.js 14:客户端组件如何向服务器动作传递File对象?
问题:通过预签名URL上传文件到AWS S3的客户端-服务器组件交互问题
我正尝试通过预签名URL将图片/视频上传至AWS S3存储。从后端获取URL后,需将URL与File对象发送到服务器动作以执行AWS S3的PUT请求,但遇到以下问题:
- 无法直接从客户端向服务器组件发送File对象
- 用
JSON.stringify处理File对象,得到空对象 - 改用FormData发送,报错:
"Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream" - 将数据转为ArrayBuffer再
JSON.stringify,仍得到空对象
相关代码如下:
page.tsx(点击处理函数)
const clickHandler = async () => { const imageName = selected?.name; const imageType = selected?.type; const result = await createPreSignedObject({ variables: { name: imageName, contentType: imageType }, }); const resultedResult = result.data.createPreSignedObject; console.log(resultedResult); //preSignedURL if (resultedResult && typeof resultedResult === "string") { if (selected) { //const formData = new FormData(); //formData.append("file", selected); const fileToArrayBuffer = (file: File) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(reader.error); reader.readAsArrayBuffer(file); }); }; const fileBufferResult = await fileToArrayBuffer(selected); console.log(fileBufferResult); console.log(JSON.stringify(fileBufferResult)); //giving empty object await postObjectToS3(resultedResult, JSON.stringify(fileToArrayBuffer(selected))); } } else { console.log("no result found"); } };
preSignedObjects.ts
"use server" export const postObjectToS3 = async (url: string, selectedFile: string) => { const newResult = JSON.parse(selectedFile); console.log(newResult); const result = await axios.put(url, newResult, { headers: { "Content-Type": "multipart/form-data", }, }); };
解决方案
核心思路
服务器组件不需要接收完整的File对象或其序列化形式,直接在客户端用预签名URL发起PUT请求是更简单的方案。如果必须通过服务器组件中转,需要正确传递文件的二进制数据,而非序列化后的空对象。
方案1:客户端直接发起PUT请求(推荐)
无需经过服务器组件中转,直接让客户端与S3交互,减少网络请求且避免File对象传递问题:
// page.tsx 点击处理函数修改 const clickHandler = async () => { const imageName = selected?.name; const imageType = selected?.type; const result = await createPreSignedObject({ variables: { name: imageName, contentType: imageType }, }); const preSignedURL = result.data.createPreSignedObject; if (preSignedURL && selected) { try { const response = await fetch(preSignedURL, { method: 'PUT', body: selected, // 直接传入File对象 headers: { 'Content-Type': imageType || '', // 必须匹配预签名URL生成时指定的Content-Type }, }); response.ok ? console.log('文件上传成功') : console.error('上传失败', await response.text()); } catch (err) { console.error('请求出错', err); } } else { console.log('未获取到预签名URL或选中文件'); } };
方案2:通过服务器组件中转(若业务必须)
如果必须经过服务器组件,需将文件转为Base64字符串传递,再在服务器端转回二进制数据:
客户端代码修改(page.tsx)
const clickHandler = async () => { const imageName = selected?.name; const imageType = selected?.type; const result = await createPreSignedObject({ variables: { name: imageName, contentType: imageType }, }); const preSignedURL = result.data.createPreSignedObject; if (preSignedURL && selected) { // 将File转为Base64字符串 const fileToBase64 = (file: File) => { return new Promise<string>((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(reader.error); reader.readAsDataURL(file); }); }; const base64Data = await fileToBase64(selected); // 移除data URL前缀,仅保留Base64内容 const pureBase64 = base64Data.split(',')[1]; await postObjectToS3(preSignedURL, pureBase64, imageType || ''); } else { console.log('未获取到预签名URL或选中文件'); } };
服务器组件代码修改(preSignedObjects.ts)
"use server" import axios from 'axios'; export const postObjectToS3 = async (url: string, base64File: string, contentType: string) => { // 将Base64转为Buffer const fileBuffer = Buffer.from(base64File, 'base64'); try { const result = await axios.put(url, fileBuffer, { headers: { 'Content-Type': contentType, // 必须和预签名URL生成时的Content-Type一致 }, }); console.log('上传成功', result); } catch (err) { console.error('上传失败', err); } };
关键注意事项
- Content-Type必须严格匹配:预签名URL生成时指定的
contentType,必须与PUT请求的Content-Typeheader一致,否则S3会返回403错误 - 禁止序列化File对象:File是浏览器特有类型,无法通过JSON序列化传递,必须转为二进制或Base64格式
- 优先选择客户端直接上传:减少中转能提升性能,同时避免数据传递的各类问题
内容的提问来源于stack exchange,提问作者Krish
相关产品推荐
相关产品推荐

