在前端开发中,文件下载是一个常见需求,例如导出 excel(csv) 报表、下载附件等。如果你还在使用传统的 window.open(url)
来下载文件,或者处理 blob
和文件名解析时频频踩坑,那么这篇文章将帮你全面掌握一个通用、健壮且优雅的下载方案。
🧩 下载场景的挑战
下载功能看似简单,但在实际开发中可能会遇到一些让你抓耳挠腮的问题:
- ❓ 请求方式不确定(GET、POST 都可能)
- 📦 响应类型是二进制
blob
,无法直接通过浏览器访问 - 📝 文件名可能被服务端编码(如
filename*=UTF-8''xxx.xlsx
) - 😵 有时候返回的不是文件,而是错误信息(JSON),需要弹出提醒
🧱 技术实践
- Axios:处理 HTTP 请求
- Blob:处理二进制文件
- FileReader:当响应是 JSON 时读取文本
- Ant Design Vue
message
:用于错误提示
🧠 Blob介绍
什么是 Blob?
Blob(Binary Large Object) 是浏览器提供的一个数据类型,用于表示一段不可变的原始二进制数据。
-
不可变:创建后内容不能再直接修改。
-
抽象 :Blob 本身并不知道数据的含义,只保存字节序列以及可选的 MIME 类型(
type
属性)。 -
常见来源
- 表单上传的文件(
<input type="file">
得到的File
对象继承自 Blob) fetch
,axios
,XMLHttpRequest
等请求返回的二进制响应- Canvas、MediaRecorder、WebRTC 等 API 导出的音视频/图片
- 使用
new Blob()
手动拼装的二进制数据
- 表单上传的文件(
Blob 相关的核心属性
属性 | 说明 |
---|---|
size |
数据长度(字节数) |
type |
MIME 类型字符串,例如 image/png 、application/pdf |
isClosed |
(Chrome 113+)判断 Blob 是否已释放 |
##3 常用 API & 场景
1. 构造 Blob / File
go
js
复制编辑
// Blob:任意字节
const blob = new Blob([arrayBuffer, 'hello'], { type: 'text/plain' });
// File:带文件名、修改时间的 Blob
const file = new File([blob], 'report.pdf', { type: 'application/pdf' });
2. 读取 Blob 内容
方法 | 返回值 | 典型用途 |
---|---|---|
arrayBuffer() |
Promise<ArrayBuffer> |
二进制处理、加解密 |
text() |
Promise<string> |
下载文本、CSV、JSON |
stream() |
ReadableStream |
大文件分片或流式上载/下载 |
slice(start, end, contentType) |
新 Blob | 分块上传、断点续传 |
FileReader |
回调式读取 (readAsText , readAsDataURL ...) |
旧写法,兼容早期浏览器 |
csharp
js
复制编辑
// 异步读取文本
const txt = await blob.text();
// 分片读取示例
const chunk = blob.slice(0, 1024 * 1024); // 前 1 MB
3. 创建/释放临时 URL
ini
js
复制编辑
// 生成可在 <img>, <a> 中使用的链接
const url = URL.createObjectURL(blob);
// 用完及时释放,避免内存泄漏
URL.revokeObjectURL(url);
4. 下载文件
ini
js
复制编辑
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'data.xlsx';
link.click();
URL.revokeObjectURL(link.href);
5. 上传文件
php
js
复制编辑
const form = new FormData();
form.append('file', blob, 'avatar.png');
await fetch('/upload', { method: 'POST', body: form });
6. Blob 与其他类型互转
目标 | 方式 |
---|---|
Blob → DataURL | FileReader.readAsDataURL 或 canvas.toDataURL() |
DataURL → Blob | fetch(dataURL).then(r => r.blob()) |
Blob → ObjectURL | URL.createObjectURL(blob) |
Blob → File | new File([blob], 'name.ext', { type: blob.type }) |
File → Blob | 直接使用;File 继承自 Blob |
🧪 文件下载技术实现
js
import axios from 'axios'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/store/module/user'
export function downloadFile(url, data, method = 'get') {
const userStore = useUserStore()
const lowerMethod = method.toLowerCase()
const config = {
method: lowerMethod,
url,
headers: {
charset: 'utf-8',
token: userStore.token || '',
},
responseType: 'blob',
...(lowerMethod === 'get' ? { params: data } : { data }),
}
axios(config).then(res => {
const contentType = res.headers['content-type']
// 若返回 JSON,则表示下载失败,读取错误信息
if (contentType === 'application/json') {
const reader = new FileReader()
reader.onload = () => {
try {
const json = JSON.parse(reader.result)
if (!json.result) {
message.error(json.msg || '文件下载失败')
}
} catch {
message.error('下载失败:无法解析服务器响应')
}
}
reader.readAsText(res.data)
return
}
// 文件下载处理
const blob = new Blob([res.data], { type: contentType || 'application/octet-stream' })
const fileName = getFilenameFromContentDisposition(res.headers['content-disposition']) || 'downloaded-file'
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
}).catch(() => {
message.error('下载失败')
})
}
// 解析 content-disposition 中的文件名
function getFilenameFromContentDisposition(header) {
if (!header) return null
try {
const parts = header.split(';').map(p => p.trim())
// filename*=UTF-8''%E6%96%87%E4%BB%B6.xlsx
const filenameStar = parts.find(p => p.toLowerCase().startsWith('filename*='))
if (filenameStar) {
const encoded = filenameStar.split('=')[1]
const matches = encoded.match(/(?:UTF-8'')?(.+)/i)
if (matches) return decodeURIComponent(matches[1])
}
// filename="文件.xlsx"
const filename = parts.find(p => p.toLowerCase().startsWith('filename='))
if (filename) {
let name = filename.split('=')[1].trim()
if (name.startsWith('"') && name.endsWith('"')) {
name = name.slice(1, -1)
}
return name
}
return null
} catch {
return null
}
}
🎯 代码核心解读
1️⃣ responseType: 'blob'
确保 Axios 接收到的是二进制流,而不是默认的 JSON 格式。
2️⃣ 智能解析文件名getFilenameFromContentDisposition
通过 Content-Disposition
头判断文件名,支持标准 filename
和兼容性更强的 filename*
(用于编码文件名)。在这踩过一个坑,之前没有考虑到filename*utf-8''%E6%84%9F%E5%BA%94%E6%9D%BF.xlsx
这种类型,导致好好的下载突然不能用了,导致前端报错。经过分析是发现后端传回来的文件名变更了。然后就有了getFilenameFromContentDisposition
对返回不同类型进行判断区别处理。
3️⃣ 错误信息处理
部分接口在出错时也返回 blob
,但类型是 application/json
,需用 FileReader
转换为字符串再解析 JSON。
4️⃣ 安全清理 URL 和 DOM
使用 URL.revokeObjectURL
回收内存,并删除临时创建的 <a>
标签,防止内存泄漏。
📘 方法调用
js
import { downloadFile } from '@/utils/download'
async function exportData() {
const url = import.meta.env.PROD ? '/common/material_stock' : import.meta.env.VITE_BASE_URL + '/common/material_stock'
const params = {
...materialStockTakingStore.searchForm,
export_flag: 1
}
downloadFile(url, params, 'post')
}
