• 前端如何实现Blob文件的下载
  • 发布于 1个月前
  • 229 热度
    0 评论
在现代Web开发中,文件下载是一个常见需求。当后端返回Blob格式的数据流时,前端需要正确处理响应并实现文件下载功能,特别是要动态获取后端设置的文件名。本文将详细介绍完整实现方案,涵盖从接口调用到文件下载的全流程。

一.理解Blob和文件下载
1.1 什么是Blob
Blob(Binary Large Object)表示二进制大对象,在前端中常用于处理文件数据。当后端返回文件流时,通常会以Blob格式传输。
1.2 文件下载的基本原理
前端文件下载通常通过以下步骤实现:
.发送请求获取文件数据
.将响应转换为Blob对象
.创建下载链接

.触发下载行为


二.基础实现:Blob文件下载
2.1 基本请求实现
async function downloadFile(url) {
  try {
   // 堆代码 duidaima.com
    const response = await fetch(url);
    const blob = await response.blob();
    const downloadUrl = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = downloadUrl;
    a.download = 'file'; // 默认文件名
    document.body.appendChild(a);
    a.click();  
    window.URL.revokeObjectURL(downloadUrl);
    document.body.removeChild(a);
  } catch (error) {
    console.error('下载失败:', error);
  }
}
2.2 存在的问题
上述实现存在明显缺陷:
.文件名是硬编码的
.无法获取后端设置的实际文件名

.缺乏错误处理


三.进阶实现:动态获取文件名
3.1 从Content-Disposition获取文件名
后端通常会在响应头中设置Content-Disposition格式如下:
Content-Disposition: attachment; filename="example.pdf"
中文名时需编码下:
const filename = 'duidaima.xlsx';
// 设置响应头
ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
const encodedFilename = encodeURIComponent(filename);
ctx.set('Content-Disposition', `attachment; filename*=UTF-8''${encodedFilename}`);
说明:
filename* 是 RFC 5987 标准定义的扩展语法
UTF-8'' 指定编码方式
encodeURIComponent 确保特殊字符被正确处理
我们可以从该头部的ContentDisposition提取文件名:
// 通过contentDisposition 获取文件名
function getFilenameFromContentDisposition(contentDisposition) {
  if (!contentDisposition) {
    return null
  }
  const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
  const matches = filenameRegex.exec(contentDisposition)
  if (matches && matches[1]) {
    return matches[1].replace(/['"]/g, "").replace("UTF-8", "")
  }
  return null
}
四.处理特殊情况
4.1 处理中文文件名
当文件名包含中文时,可能出现乱码问题。解决方案是对文件名进行解码:
function decodeFilename(filename) {
  // 处理UTF-8编码的文件名
  if (filename.includes("%")) {
    try {
      return decodeURIComponent(filename)
    } catch (e) {
      return filename
    }
  }
  // 处理ISO-8859-1编码的情况
  if (/=\?/.test(filename)) {
    try {
      return decodeURIComponent(escape(filename))
    } catch (e) {
      return filename
    }
  }
  return filename
}
五.完整示例
5.1 新增/utils/download.js文件
/**
 * 获取 blob
 * @param  {String} url 目标文件地址
 * @return {Promise} 
 */
export function getBlob(url) {
  return new Promise(resolve => {
    const xhr = new XMLHttpRequest()
    url = url + `?r=${Math.random()}`
    xhr.open("GET", url, true)
    xhr.responseType = "blob"
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.response)
      }
    }
    xhr.send()
  })
}
/**
* 保存
* @param  {Blob} blob     
* @param  {String} fileName 想要保存的文件名称
* @param  {String} type 想要保存的文件名称
*/
export function downloadFileByBlob(data, fileName, type) {
  let options = {}
  if (type) {
    options = { type: `application/${type}` }
  }
  const blob = new Blob([data], options)
  if ("download" in document.createElement("a")) { // 非IE下载
    const alink = document.createElement("a")
    alink.download = fileName
    alink.style.display = "none"
    alink.href = URL.createObjectURL(blob)
    document.body.appendChild(alink)
    alink.click()
    URL.revokeObjectURL(alink.href) // 释放URL 对象
    document.body.removeChild(alink)
  } else { // IE10+下载
    navigator.msSaveBlob(blob, fileName)
  }
}
/**
* 下载
* @param  {String} url 目标文件地址
* @param  {String} filename 想要保存的文件名称
*/
export function downloadFileByUrl(url, filename, type) {
  getBlob(url).then(blob => {
    downloadFileByBlob(blob, filename, type)
  })
}
// 通过contentDisposition 获取文件名
export function getFilenameFromContentDisposition(contentDisposition) {
  if (!contentDisposition) {
    return null
  }
  const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
  const matches = filenameRegex.exec(contentDisposition)
  if (matches && matches[1]) {
    return decodeFilename(matches[1].replace(/['"]/g, "").replace("UTF-8", ""))
  }
  return null
}
function decodeFilename(filename) {
  // 处理UTF-8编码的文件名
  if (filename.includes("%")) {
    try {
      return decodeURIComponent(filename)
    } catch (e) {
      return filename
    }
  }
  // 处理ISO-8859-1编码的情况
  if (/=\?/.test(filename)) {
    try {
      return decodeURIComponent(escape(filename))
    } catch (e) {
      return filename
    }
  }
  return filename
}
5.2 axios.js响应拦截中设置文件名
import { getFilenameFromContentDisposition } from "@/utils/download"
.....

// respone拦截器
service.interceptors.response.use(
  async(response) => {
    const { data, config, status, request } = response
    // 如果自定义代码不是200,则判断为错误。
    if (status !== 200) {
      modal.msgError(data.message || "Error")
      return { ...data, config, responseErr: true }
    } else {
      // 二进制数据则直接返回
      if (
        request.responseType === "blob" ||
        request.responseType === "arraybuffer"
      ) {
        // 获取 Content-Disposition 头
        const contentDisposition = response.headers["content-disposition"]
        // 解析文件名
        const fileName = getFilenameFromContentDisposition(contentDisposition) // 默认文件名 为空
        if (fileName) {
          // 存储文件名 用于设置和后端一样的文件名
          localStorage.setItem("downloadFileName", fileName)
        }
        return response
      }
      if (data.code === 200) {
        return { ...data, config }
      }
      if (data.code === 401 || data.code === 403) {
        return { ...data, config, responseErr: true }
      }
      // 用户权限变更
      if (data.code === 402) {
        if (isShowingModal) {
          return { ...data, config, responseErr: true }
        }
        isShowingModal = true
        modal
          .confirm(data.message, {
            type: "warning",
            showClose: false
          })
          .then(async() => {
            // 刷新去首页吧
            window.location.href =
              window.location.origin + window.location.pathname
          })
          .catch(async(err) => {
            // 关闭 MessageBox 后,手动移除 不能关闭的遮罩层
            const maskModal = document.querySelector(".v-modal")
            if (maskModal) {
              maskModal.remove()
            }
            await store.dispatch("user/getUserInfo")
            isShowingModal = false
          })
        return { ...data, config, responseErr: true }
      }
      modal.msgError(data.message || "Error")
      return { ...data, config, responseErr: true }
    }
  },
  (error) => {
    // 加入try 否则 会导致 取消重复请求时 因为无config等字段导致报错 导致取消重复请求失效
    try {
      const { config, headers, status } = error.response
      if (status === 520) {
        // 导出文件接口报错时与后端约定 返回状态码为 520
        // 约定好响应头添加 x-custom-header 且值为原本报错的 code
        const code = Number(headers["x-custom-header"])
        console.log("🚀 ~ code:", code)
        // 当code为401 或者 403时 触发刷新token接口或者弹框提示过期重新登录
        if (code === 401 || code === 403) {
          return {
            code,
            message: "凭证已过期,请重新登录!",
            config,
            responseErr: true
          }
        } else {
          modal.msgError(code === 409 ? "无权限" : error.message || "Error")
          return Promise.reject(error)
        }
      }
    } catch (err) {
      console.log("🚀 ~ err:", err)
    }
    if (error.name !== "CanceledError") {
      modal.msgError(error.message || "Error")
    }
    return Promise.reject(error)
  }
)
代码解释,如果返回内容 是blob文件,则获取其响应头设置的文件名,将文件名存储到浏览器缓存。

5.3 配置接口处需设置响应类型(重要)
export function exportUserByQuery(data = {}, config = {
  preventDuplicateRequestsType: "prevent",
  responseType: "blob" // blob 或 arraybuffer
}) {
  return axiosRequest.post(`url`, data, config)
}
5.4 页面中下载
import { downloadFileByBlob } from "@/utils/download"
......
userApi.exportUserByQuery(this.queryParams).then((data) => {
  // 从缓存中获取文件名 没有则设置 默认文件名
  const fileName = localStorage.getItem("downloadFileName") || "用户数据.xlsx"
  downloadFileByBlob(data, fileName, "octet-stream")
  localStorage.removeItem("downloadFileName") // 清除缓存
  this.$modal.msgSuccess("导出用户数据成功")
  this.openExportWay = false
})
总结
本文详细介绍了前端如何处理Blob格式的文件下载,并动态获取后端设置的文件名。关键点包括:
1. 正确解析Content-Disposition头部获取文件名
2. 处理中文文件名编码问题
3. 实现axios拦截处获取文件名,下载文件处设置文件名
上述axios响应拦截代码采用了vue-axios-optimize(点击阅读),轻松实现防止重复请求,全局加载动画精准控制,无感知续签TOKEN,接口缓存等功能。

用户评论