React + Next.js SSR环境下点击按钮下载服务端PDF文件的最优方案是什么
Next.js SSR项目实现服务端PDF下载的最佳实践
以下方案覆盖公开文件、私有鉴权文件两种主流场景,可根据自身需求选择:
场景1:PDF为公开无权限要求的文件
这种情况直接用原生标签实现即可,零额外开发成本:
- 实现步骤:
- 把PDF文件放入项目
public目录下,例如存放路径为public/assets/report.pdf - 前端按钮直接嵌套带download属性的a标签:
const DownloadPdfBtn = () => { return ( <a href="/assets/report.pdf" download="自定义导出文件名.pdf"> <button type="button">下载PDF</button> </a> ) } - 把PDF文件放入项目
- 优势:无需服务端代码,SSR/CSR模式下均生效,不会触发Next.js路由拦截,性能最优。
场景2:PDF为私有文件,需要鉴权后才可下载(生产环境常用场景)
这种情况不能把文件放在公开的public目录,需要走API接口做权限校验后返回文件流,分两种路由模式的实现:
Pages Router 实现
- 先编写服务端API接口,路径为
pages/api/download-pdf.js:
import fs from 'fs' import path from 'path' export default function handler(req, res) { // 此处替换为项目自身的鉴权逻辑,例如校验请求头的token、用户权限 const token = req.headers.authorization?.split(' ')[1] if (!token || !validateUserToken(token)) { return res.status(401).send('无下载权限') } // 服务端私有PDF存放路径,不要放在public目录 const filePath = path.join(process.cwd(), 'private-storage', 'user-report.pdf') const fileBuffer = fs.readFileSync(filePath) // 设置响应头强制触发下载 res.setHeader('Content-Type', 'application/pdf') res.setHeader('Content-Disposition', `attachment; filename="用户自定义文件名.pdf"`) res.send(fileBuffer) }
- 前端按钮调用逻辑:
const DownloadPdfBtn = () => { const handleDownload = async () => { try { const res = await fetch('/api/download-pdf', { headers: { // 携带鉴权token Authorization: `Bearer ${localStorage.getItem('user_token')}` } }) if (!res.ok) throw new Error('下载请求失败') // 把响应流转为blob对象触发下载 const blob = await res.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = '自定义导出文件名.pdf' a.click() // 释放内存避免泄漏 window.URL.revokeObjectURL(url) } catch (err) { console.error('下载出错:', err) } } return <button onClick={handleDownload}>下载PDF</button> }
App Router 实现
- API路由写在
app/api/download-pdf/route.js:
import fs from 'fs/promises' import path from 'path' import { NextResponse } from 'next/server' export async function GET(request) { // 替换为自身项目的鉴权逻辑 const token = request.headers.get('authorization')?.split(' ')[1] if (!token || !validateUserToken(token)) { return NextResponse.json({ error: '无下载权限' }, { status: 401 }) } const filePath = path.join(process.cwd(), 'private-storage', 'user-report.pdf') const fileBuffer = await fs.readFile(filePath) return new NextResponse(fileBuffer, { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="自定义导出文件名.pdf"` } }) }
- 前端调用逻辑和Pages Router完全一致,直接复用上面的组件代码即可。
注意事项:
- 若PDF文件体积超过10M,不要一次性读入内存,改成流式读取返回,避免服务端OOM
- 生产环境如果PDF存在云存储(OSS/COS等),建议服务端直接生成带过期时间的签名下载地址返回给前端,无需走服务端中转文件流,性能更高
- 无需引入第三方下载库,原生API已经可以覆盖所有场景,可减少项目包体积
内容的提问来源于stack exchange,提问作者Forshank
相关产品推荐
相关产品推荐

