记录导出下载zip压缩包问题

背景:

点击 " 导出 " 按钮,需要调用后端接口,下载 zip 压缩包。( "application/zip" )


Blob 文件下载函数报错解决

TypeScript 复制代码
/**
 * 通用的 Blob 文件下载函数
 * @param blob要下载的 Blob 对象
 * @param name下载文件的名称
 */
function downloadBlob(blob: Blob, name: string): void {
  // 1. 创建 Blob 对象的 URL
  const url = URL.createObjectURL(blob);
  
  // 2. 创建隐藏的 <a> 标签
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  
  // 3. 触发点击事件开始下载
  a.click();
  
  // 4. 释放 URL 对象,避免内存泄漏
  URL.revokeObjectURL(url);
}

导出报错:

Failed to execute 'createObjectURL' on 'URL': Overload resolution failed.

报错原因

URL.createObjectURL 参数必须严格是 Blob / File / MediaSource ,报错 Overload resolution failed 只有两种情况:

  1. 传入的 blob 根本不是 Blob 实例(接口返回二进制、 base64、普通对象、null / undefined)
  2. 异步时序问题:还没拿到完整 blob 就执行下载,blob 为空

修复完整版 TS 函数(加类型校验、容错、延迟释放 URL)

TypeScript 复制代码
/**
 * 通用的 Blob 文件下载函数
 * @param blob 要下载的 Blob 对象
 * @param name 下载文件的名称
 */
function downloadBlob(blob: Blob, name: string): void {
  // 关键1:强校验 blob 类型,拦截非法参数
  if (!(blob instanceof Blob)) {
    console.error('downloadBlob 参数不是合法 Blob 对象', blob);
    throw new Error('传入的文件数据不是Blob类型,无法下载');
  }

  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  // 兼容部分浏览器样式隐藏
  a.style.display = 'none';
  document.body.appendChild(a);

  a.click();

  // 关键2:延迟释放URL,部分浏览器同步 revoke 会下载失效
  setTimeout(() => {
    URL.revokeObjectURL(url);
    document.body.removeChild(a);
  }, 100);
}

export default downloadBlob;

配套调用排查方案(90% 报错根源在这里)

场景 1:后端返回二进制流(axios 示例)

TypeScript 复制代码
// 请求必须加 responseType: 'blob',否则拿到的是普通JSON/字符串
axios.get('/api/export', {
  responseType: 'blob' // 缺一不可
}).then(res => {
  // res.data 才是 Blob
  downloadBlob(res.data, '导出文件.xlsx');
})

场景 2:接口返回 base64 字符串,手动转 Blob 再下载

TypeScript 复制代码
function base64ToBlob(base64: string, mimeType = 'application/octet-stream') {
  const byteString = atob(base64.split(',')[1]);
  const ab = new ArrayBuffer(byteString.length);
  const ia = new Uint8Array(ab);
  for (let i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i);
  }
  return new Blob([ab], { type: mimeType });
}

// 使用
const blob = base64ToBlob(base64Str, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
downloadBlob(blob, '文件.xlsx');

场景 3:blob 为 null / undefined 兜底

调用前增加判断:

TypeScript 复制代码
if (!blob || !(blob instanceof Blob)) return;
downloadBlob(blob, '文件名.pdf');

补充报错高频坑

  1. 同步 revokeObjectURL :你原来代码创建完立刻释放,部分浏览器下载还没完成 URL 就失效,不仅下载空白,还会偶发类型报错,必须用 setTimeout 延迟清理
  2. 忘记 responseType: blob :axios 默认解析为文本 /json,拿到的不是 Blob,直接传参必报 Overload resolution failed
  3. 文件流报错返回 JSON :后端异常时返回 {code:500} 文本,不是二进制,校验 blob.type 可区分:
TypeScript 复制代码
// 拦截后端报错返回的json blob
if (blob.type.includes('application/json')) {
  const reader = new FileReader();
  reader.readAsText(blob);
  reader.onload = () => {
    console.error('导出失败', reader.result);
  };
  return;
}

问题:

场景2:后端 createExcel 接口是生成压缩包,接口返回的 contentType:"application/zip"(压缩包 zip 格式),file:文件,STRING 类型, base64 编码格式,需要解析。


一、Base64 转 Zip Blob 工具函数

TypeScript 复制代码
/**
 * base64字符串转zip压缩包Blob
 * @param base64Str 后端返回的base64文件字符串
 * @returns Blob(zip)
 */
function base64ToZipBlob(base64Str: string): Blob {
  // 去掉base64前缀 data:xxx;base64,
  const pureBase64 = base64Str.includes(',') 
    ? base64Str.split(',')[1] 
    : base64Str;

  // base64解码
  const binary = atob(pureBase64);
  const arrBuf = new ArrayBuffer(binary.length);
  const uint8 = new Uint8Array(arrBuf);

  for (let i = 0; i < binary.length; i++) {
    uint8[i] = binary.charCodeAt(i);
  }

  // 指定zip标准mime类型
  return new Blob([arrBuf], { type: 'application/zip' });
}

二、整合下载(复用之前修复好的 downloadBlob)

TypeScript 复制代码
/**
 * 通用的 Blob 文件下载函数
 * @param blob 要下载的 Blob 对象
 * @param name 下载文件的名称(建议后缀 .zip)
 */
function downloadBlob(blob: Blob, name: string): void {
  if (!(blob instanceof Blob)) {
    console.error('文件数据非合法Blob', blob);
    throw new Error('文件解析失败,无法下载');
  }

  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  a.style.display = 'none';
  document.body.appendChild(a);

  a.click();

  // 延迟释放url,防止下载失效
  setTimeout(() => {
    URL.revokeObjectURL(url);
    document.body.removeChild(a);
  }, 150);
}

三、接口调用完整示例

假设后端返回结构:{ code: 200, data: { file: "base64字符串" } }

TypeScript 复制代码
// 请求后端生成zip压缩包
async function exportZip() {
  try {
    const res = await axios.post('/api/createExcel');
    const base64Str = res.data.data.file;

    // base64转zip blob
    const zipBlob = base64ToZipBlob(base64Str);
    
    // 下载,文件名后缀必须 .zip
    downloadBlob(zipBlob, `批量文件_${new Date().getTime()}.zip`);
  } catch (err) {
    console.error('导出压缩包失败', err);
  }
}

四、关键坑点说明

1、MIME 类型必须填 application/zip

不指定或填错会导致解压工具识别异常、下载文件损坏。

2、区分带前缀 / 纯 base64

后端部分场景会直接返回纯 base64 无 data:xxx;base64, ,代码做了兼容处理。

3、文件名强制后缀 .zip

否则浏览器下载后无后缀,双击无法解压。

4、不会再报 createObjectURL Overload resolution failed

因为转换后一定是标准 Blob 实例,传入 downloadBlob 会先做类型校验拦截非法值。

五、兜底校验(防止后端返回空串 / 报错 base64)

调用前增加判断:

TypeScript 复制代码
if (!base64Str || base64Str.length < 10) {
  alert('服务端未返回有效文件数据');
  return;
}
const zipBlob = base64ToZipBlob(base64Str);
downloadBlob(zipBlob, '导出文件.zip');

后端接口返回的 file 值:

file:"UEsDBBQACAgxxxxxxxx"

适配纯 Base64(无 data:xxx, 前缀,以 UEs 开头标准 zip base64)完整代码

1. base64 转 zip Blob 工具

TypeScript 复制代码
/**
 * 纯base64(UEs开头zip)转Blob
 * @param base64Str 后端返回纯base64字符串 UEsDBBQACAgxxxxxxxx
 * @returns zip Blob
 */
function pureBase64ToZipBlob(base64Str: string): Blob {
  // 无逗号分割前缀,直接解码
  const binaryStr = atob(base64Str);
  const len = binaryStr.length;
  const buffer = new ArrayBuffer(len);
  const uint8Arr = new Uint8Array(buffer);

  for (let i = 0; i < len; i++) {
    uint8Arr[i] = binaryStr.charCodeAt(i);
  }

  // zip标准mime
  return new Blob([buffer], { type: "application/zip" });
}

2. 下载函数(修复内存释放问题)

TypeScript 复制代码
function downloadBlob(blob: Blob, fileName: string): void {
  if (!(blob instanceof Blob)) {
    console.error("非法Blob数据", blob);
    throw new Error("文件数据解析失败");
  }

  const objectUrl = URL.createObjectURL(blob);
  const aTag = document.createElement("a");
  aTag.href = objectUrl;
  aTag.download = fileName;
  aTag.style.display = "none";
  document.body.appendChild(aTag);

  aTag.click();

  // 延迟销毁URL,避免下载空白/报错
  setTimeout(() => {
    URL.revokeObjectURL(objectUrl);
    document.body.removeChild(aTag);
  }, 100);
}

3. 接口调用示例

后端返回格式:{ file: "UEsDBBQACAgxxxxxxxx" }

TypeScript 复制代码
async function exportZipFile() {
  const res = await axios.post("/api/createExcel");
  const base64Str = res.data.file;

  // 空值拦截
  if (!base64Str || !base64Str.startsWith("UEs")) {
    alert("未获取到有效压缩包数据");
    return;
  }

  const zipBlob = pureBase64ToZipBlob(base64Str);
  // 文件名后缀必须 .zip
  downloadBlob(zipBlob, `导出压缩包_${Date.now()}.zip`);
}

关键说明

  1. UEs 是 zip 文件 base64 固定开头,代码增加前置校验,快速判断是否为合法压缩包;
  2. 纯 base64 无 data:application/zip;base64, 前缀,不再执行 split(',')[1],避免截断错误;
  3. 修复同步 revokeObjectURL 导致的下载失效、createObjectURL 参数报错问题;
  4. MIME 固定 application/zip,下载后可直接解压,不会损坏。

问题:

接口是不是不能写 responseType:"blob"了?

结论先说

当前场景绝对不能加 responseType: 'blob'

原因:后端返回的是 JSON 对象,里面的 file 字段是一段 base64 字符串,不是二进制流。

两种接口返回模式区分清楚

模式 1(二进制流 → 必须 responseType: 'blob')

后端直接返回文件二进制,响应体整个就是 zip 文件,无外层 JSON。

TypeScript 复制代码
axios.get('/api/export', {
  responseType: 'blob' // 正确
})
// res.data 直接是 Blob
downloadBlob(res.data, 'xxx.zip')

模式 2(你现在的场景:JSON 包裹 base64 → 不能加 blob)

后端返回结构:

TypeScript 复制代码
{
  "code": 200,
  "file": "UEsDBBQACAgIAxxxxxx"
}
  • 整个响应是普通 JSON,file 只是字符串字段
  • 如果加 responseType: 'blob',axios 会把整个响应体转成 Blob,你拿不到 json 里的 base64,完全解析失败

正确请求写法:

TypeScript 复制代码
// 不要写 responseType
const res = await axios.post('/api/createExcel')
const base64Str = res.data.file

补充踩坑点

  1. 如果你强行加 responseType: 'blob' res.data 会是一个包含完整 json 文本的 Blob,你还要额外读 FileReader 转字符串再解析 json,多一层无用逻辑,纯属添乱。

  2. 什么时候该用 blob? 只有后端直接输出文件流(响应头 Content-Type: application/zip,无 json 包装)时才配置。 只要后端包了一层 json 返回 base64,一律去掉 responseType。