🚀 前端实战:优雅地实现一个通用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/pngapplication/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.readAsDataURLcanvas.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')
}
相关推荐
gnip10 分钟前
低代码平台自定义组件实现思路
前端·低代码
实习生小黄21 分钟前
基于扫描算法获取psd图层轮廓
前端·javascript·算法
青松学前端27 分钟前
你不知道的秘密-axios源码
前端·javascript
GISer_Jing28 分钟前
IntersectionObserver API&应用场景&示例代码详解
前端·javascript
未来之窗软件服务30 分钟前
学校住宿缴费系统h5-——东方仙盟——仙盟创梦IDE
前端·javascript·ide·仙盟创梦ide·东方仙盟
markyankee10140 分钟前
JavaScript 作用域与闭包详解
前端·javascript
gufs镜像40 分钟前
Swift学习总结——使用Playground
前端·ios·面试
高冷的小明40 分钟前
React-Find 一款能快速在网页定位到源码的工具,支持React19.x/next 15
前端·javascript·react.js
parade岁月41 分钟前
从浏览器存储到web项目中鉴权的简单分析
前端·后端
默默地写代码1 小时前
微信小程序 新版canvas绘制名片
前端·javascript·微信小程序