有趣的是,关于浏览器下载行为,在 MDN 上找不到完整的说明文档,只能结合各个相关知识点组合在一起。

基础知识

Blob,File,ObjectURL

  • Blob 对象表示一个不可变、原始数据的类文件对象。
  • 文件(File)接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。File 对象是特殊类型的 Blob。
  • objectURL 对象表示指定的 File 对象或 Blob 对象。

    Fetch,Response

  • Fetch API 提供了一个获取资源的接口(包括跨域请求)。

  • Fetch API 的 Response 接口呈现了对一次请求的响应数据。

    HTTP Headers,Content-Type,Content-Disposition

  • Content-Type 实体头部用于指示资源的MIME类型。

    • MIME 类型现在又称为 media type,有时也被称为 content type,是指示文件类型的字符串。
    • 当 content-type 为 application/octet-stream 时,意味这下载这个文件。
  • Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。

    实现方式

    GET: 标签

    使用 GET 方式获取的内容或者文件,直接使用 标签即可。

需要指定 href 属性和 download 属性。href 属性是内容的超链接,download 表明链接的资源将被下载,而不是显示在浏览器中。该值表示下载文件的建议名称。

有趣的是,在 MDN 文档中对 download 补充了一条说明:

注意: 该值对于下载行为来说不一定是有用的,同时也不能决定下载行为是否发生。 https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLAnchorElement/download

这就要涉及到 Http Headers 中的 Content-Disposition 了。我们用一个对比实验来说明:

Content-Disposition + download + download=a-name.csv
empty 不下载 下载 [href].txt 下载 a-name.csv
inline 不下载 下载 [href].txt 下载 a-name.csv
attachment 下载 [href].txt 下载 [href].txt 下载 a-name.csv
attachment; filename=header-name.csv 下载 header-name.csv 下载 header-name.csv 下载 header-name.csv

从实验结果可以看出,无论是是否下载,还是下载文件名,Content-Disposition 的优先级都高于 标签。

这里继续补充一点 Content-Type 与 Content-Disposition 的关系:

  • Content-Type 应该用于指定文件本身的类型。特别的:application/octet-stream 不应该用于表达下载行为,应该表达的是:我不清楚这个文件的类型是什么,用户可以用于下载。
  • Content-Disposition 应该用于指定文件的下载行为。

POST:Fetch Response -> Blob -> ObjectURL -> 标签

本质上还是 标签下载,但由于 POST 的参数不能直接作为 href 属性,因此如标题所言的一个转换流程。

用代码表示如下:

  1. async function postDownload(url) {
  2. // 获取 POST 接口数据,Response
  3. const res = await fetch(url);
  4. // 从 Response 中获取 filename
  5. const filename = res.headers.get("content-disposition").split("filename=")[1];
  6. // 从 Response 中获取 file content,并转为 Blob
  7. // 也可以写成 const blob = new Blob([await res.text()]);
  8. const blob = await res.blob();
  9. // 将 Blob 转为 ObjectURL
  10. const blobURL = URL.createObjectURL(blob);
  11. // 动态创建一个 <a> 标签,触发 click 事件后删除
  12. const a = document.createElement("a");
  13. a.href = blobURL;
  14. a.download = filename;
  15. document.body.appendChild(a);
  16. a.click();
  17. a.remove();
  18. // 内存管理,释放
  19. URL.revokeObjectURL(blobURL);
  20. }

参考资料

  1. Blob https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
  2. File https://developer.mozilla.org/zh-CN/docs/Web/API/File
  3. URL.createObjectURL https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
  4. Fetch API https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API
  5. Response https://developer.mozilla.org/zh-CN/docs/Web/API/Response
  6. Content-Type https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type
  7. Content-Disposition https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
  8. https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a
  9. HTMLAnchorElement https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLAnchorElement
  10. [Stack Overflow] Do I need Content-Type: application/octet-stream for file download? https://stackoverflow.com/questions/20508788/do-i-need-content-type-application-octet-stream-for-file-download