概要

在前端下载文件是个很通用的需求,一般后端会提供下载的方式有两种:

  1. 直接返回文件的网络地址(一般用在静态文件上,比如图片以及各种音视频资源等)
  2. 返回文件流(一般用在动态文件上,比如根据前端选择,导出不同的统计结果 excel 等)

第一种方式比较简单,但是使用场景有限。
第二种方式通用性更好,最近再使用 antd 开发的过程中,下载文件部分折腾了一下午,于是将关键的部分和遇到的一些问题整理如下。

前端核心代码

我的前端是基于 antd pro 开发的,这里不在详细介绍 antd pro 相关的内容,只说明下封装的下载函数:

import { request } from "umi";
// 这是我在项目中封装的下载函数,有2个参数:
// 一个是文件的id,用来给后端API搜索文件用的
// 一个是文件名filename,这个给前端用的,下载时,默认保存的文件名
export async function DownloadFile(id: string, filename: string) {
  return RestGet<BlobPart>(`/api/v1/file/download/${id}`, "blob").then(
    (res) => {
      let url = URL.createObjectURL(new Blob([res], { type: "octet/stream" }));
      let a = document.createElement("a");
      a.href = url;
      a.download = filename;
      a.click();
      URL.revokeObjectURL(url);
    }
  );
}
// GET 方式请求后端API
export async function RestGet<T>(
  url: string,
  responseType: "json" | "blob" = "json"
) {
  return request<T>(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
    responseType: responseType,
  });
}
// POST 方式请求后端API
export async function RestPost<T>(
  url: string,
  body: any,
  responseType: "json" | "blob" = "json"
) {
  return request<T>(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    data: body,
    responseType: responseType,
  });
}

这个下载函数本质就是创建一个 <a> 元素,然后模拟点击此 <a> 元素来完成下载。
对于 DownloadFile函数,我是根据自己的需要封装的,不一定非得是这两个参数,可以根据自己的后端 API 来定制需要传入的参数。

将下载封装成函数之后看,前端就可以通过一个按钮来下载文件了,比如:

<Button
  type="link"
  key="download"
  icon={<DownloadOutlined />}
  onClick={() =>
    DownloadFile(item.video_file_id as string, (item.name as string) + ".mp4")
  }
>
  下载
</Button>

我这里是 GET 方式下载文件的,如果参数比较多且复杂的话,也可以使用 POST 方式。
只要把 DownloadFileRestGet<T>改成 RestPost<T>,并调整相应的参数即可。

遇到的问题

调试其中的下载方式就不提了,遇到的最大困难,搜索半天也没什么人提到的就是请求中的 responseType问题。

刚开始,我没有设置 responseType,默认值好像是 json或者 text
测试时发现,下载文本类型的文件没有问题,但是下载二进制文件的话,比如视频或者图片,下载之后总是无法正常打开,前后端也没有任何错误提示。

折腾了半天,才试出 responseType传入 blob的话,二进制文件才能正常下载。
而且发现,设置成 blob的话,文本类型的文件也能正常下载打开。