如何在Nextjs中点击按钮下载AWS S3预签名URL的外部图片
方案总览
你的核心痛点是后端中转大文件导致超时,最优解就是避免后端中转,直接让前端从S3预签名URL下载,可落地的方案分为以下几个部分:
一、后端改造
原有同步中转逻辑需要在单次请求中完成S3下载、TIFF编解码、返回流全流程,1GB文件必然超时,且会大量占用服务器带宽和算力,按业务需求选择对应改造方案:
场景1:不需要实时调整TIFF压缩参数、DPI
直接返回原文件的预签名URL即可,改造后的Go代码示例:
// GetDownloadBatchFile handles GET requests to get s3 presigned download url func GetDownloadBatchFile(ctx *atreugo.RequestCtx) error { // Parse Batch ID batchIntfID := ctx.UserValue("batch_id") batchStrID, isStr := batchIntfID.(string) if !isStr { return ctx.TextResponse("User error. No batch ID provided", 400) } batchIntID, err := strconv.Atoi(batchStrID) if err != nil { return ctx.TextResponse("User error. Invalid batch ID provided", 400) } // Get Batch & Batch Setting batch, err := models.GetBatchByID(batchIntID) if err != nil { return ctx.TextResponse("User Error. No batch associated with provided batch ID", 404) } batch.BatchSetting, err = models.GetBatchSettingByID(batch.BatchSettingID) if err != nil { return ctx.TextResponse("User Error. No batch setting associated with provided batch setting ID", 404) } // Generate presigned url with custom response headers req, _ := util.AWSS3Svc.GetObjectRequest(&s3.GetObjectInput{ Bucket: aws.String(batch.BatchSetting.AWSBucket), Key: aws.String(batch.OutputFileKey), // 直接指定响应头,前端下载时自动触发保存,文件名自定义 ResponseContentDisposition: aws.String("attachment; filename=custom-output.tiff"), ResponseContentType: aws.String("image/tiff"), }) // 过期时间按文件大小设置,1GB文件建议设2小时以上 URL, err := req.Presign(time.Hour * 3) if err != nil { return ctx.TextResponse("System Error. Failed to generate download link", 500) } // 直接返回JSON格式的预签名URL ctx.Response.Header.SetContentType("application/json") return ctx.JSONResponse(map[string]string{ "presignedUrl": URL, }) }
场景2:必须实时生成指定DPI、无压缩的TIFF
实时处理1GB文件必然超时,必须改成异步方案:
- 用户点击下载时,后端先返回「任务已提交」的响应,同时提交转码任务到异步队列(可以用RabbitMQ或者Go原生channel实现轻量队列)
- 后台Worker异步下载S3原文件,转码完成后把新文件上传到S3
- 转码完成后通过站内信、邮件通知用户,或者前端轮询任务状态,完成后返回新文件的预签名URL供下载
二、Next.js前端实现
前置要求:先配置S3桶的CORS规则,允许你的前端域名发起GET请求,避免跨域拦截。
方式1:原生a标签下载(优先推荐,适合大文件)
调用浏览器原生下载能力,支持进度显示、断点续传,不会占用前端页面内存,1GB文件无压力。代码示例:
'use client' // Next.js 13+ App Router需要添加该标记 import { useState } from 'react' export default function DownloadButton({ batchId }: { batchId: number }) { const [loading, setLoading] = useState(false) const handleDownload = async () => { setLoading(true) try { // 调用后端接口获取预签名URL const res = await fetch(`/你的后端接口路径/batch/${batchId}/download-url`) const { presignedUrl } = await res.json() // 动态创建a标签触发下载 const a = document.createElement('a') a.href = presignedUrl // 如果后端生成预签名时已经设置了Content-Disposition,可省略download属性 a.download = 'custom-output.tiff' document.body.appendChild(a) a.click() document.body.removeChild(a) } catch (err) { console.error('下载失败', err) } finally { setLoading(false) } } return ( <button onClick={handleDownload} disabled={loading}> {loading ? '获取下载地址中...' : '下载图片'} </button> ) }
方式2:流式下载(需要自定义页面内下载进度时使用)
如果需要在页面内嵌入下载进度条,可以用fetch的ReadableStream处理,注意1GB以上文件不建议转Blob存入内存,仅适合中等大小的文件:
const handleDownloadWithProgress = async () => { setLoading(true) try { const res = await fetch(`/你的后端接口路径/batch/${batchId}/download-url`) const { presignedUrl } = await res.json() const response = await fetch(presignedUrl) const contentLength = +response.headers.get('Content-Length')! let receivedLength = 0 const chunks = [] const reader = response.body?.getReader() if (!reader) return while(true) { const { done, value } = await reader.read() if (done) break chunks.push(value) receivedLength += value.length // 这里可以更新自定义进度条的状态 console.log(`下载进度: ${(receivedLength / contentLength * 100).toFixed(2)}%`) } // 生成Blob触发下载 const blob = new Blob(chunks) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'custom-output.tiff' a.click() URL.revokeObjectURL(url) } catch (err) { console.error('下载失败', err) } finally { setLoading(false) } }
注意事项
- S3预签名URL的过期时间要根据文件大小设置足够长,1GB文件建议设置2小时以上,避免下载到一半URL过期
- 如果用CloudFront加速S3的话,要单独配置CloudFront的缓存策略和CORS,避免预签名URL被缓存
- 大文件下载建议开启S3的分段下载支持,浏览器原生会自动处理,不需要额外开发
内容的提问来源于stack exchange,提问作者Brady Agranoff
相关产品推荐
相关产品推荐

