🚀 前端实战:优雅地实现一个通用Blob文件下载方法

在前端开发中,文件下载是一个常见需求,例如导出 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')
}
相关推荐
zach29 分钟前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
overmind38 分钟前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端
只睡四小时42 分钟前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航
恋猫de小郭1 小时前
Android 原生的 Compose A2UI 也来了,你还抱着 XML 养老吗?
android·前端·flutter
庄园特聘拆椅狂魔1 小时前
从连连看到粒子消散——前端小游戏与视觉特效的技术选型
前端
JudithHuang1 小时前
React 常用 Hooks
前端·react.js·前端框架
sycmancia2 小时前
Qml——Window元素使用
前端
每天都好困2 小时前
03 AR 投影与误差修正:把地图上的点贴到视频画面上
前端
每天都好困2 小时前
04 地图图层封装:像管理 Photoshop 图层一样管理地图元素
前端
去伪存真3 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent