fetch请求后端API并将响应body保存为文件

太长不看版

基于类npm方式管理依赖库的写法

下载依赖库

1
2
yarn add file-saver
yarn @types/file-saver -D

代码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import * as FileSaver from 'file-saver';

const req = {
method: 'get',
headers: {
// 如果后端要求在请求API的时候头部带上token, 可以在这里添加
token: 'token_something'
}
};

// 假设后端提供以json格式导出配置文件的接口为"/get/setting/file?type=json"
fetch('/get/setting/file?type=json', req).then(response => {
/**
* 获取响应的头部content-disposition内容, 例子内容为"attachment; filename="CD.json""
* "attachment"标识: 代表后端期望前端把响应的body当做文件处理并弹出文件下载框
* "filename"标识: 代表后端期望前端在保存文件时显示的默认文件名
* 在"RFC 6266", "RFC 2183"规范中规定了content-disposition的意义
* 一般的浏览器内部会按照规范实现触发下载框的功能
* 比如在浏览器地址栏中直接输入请求url获得的响应, content-disposition的内容带有attachment标识时就会当做下载文件来处理
* 当filename标识不存在时设一个默认的文件名, 比如Chrome设的默认文件名就是"file"
* 但是fetch并没有对这个规范做相应的实现, 那么就需要我们自行判断并进行实现.
*/
const disposition = response.headers.get('content-disposition');
if (disposition && disposition.match(/attachment/)) {
let filename = disposition.replace(/attachment;.*filename=/, '').replace(/"/g, '');
filename = filename && filename !== '' ? filename : 'file';
response.blob().then(blob => {
FileSaver.saveAs(blob, filename);
});
}
});

直接写在HTML中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/FileSaver.min.js"></script>
<script>
let saveTest = () => {
const req = {
method: 'get',
headers: {
token: 'token_something'
}
};
fetch('/get/setting/file?type=json', req).then(response => {
const disposition = response.headers.get('content-disposition');
if (disposition && disposition.match(/attachment;/)) {
let filename = disposition.replace(/attachment;.*filename=/, '').replace(/"/g, '');
filename = filename && filename !== '' ? filename : 'default.txt';
response.blob().then(blob => {
saveAs(blob, filename);
});
}
});
}
saveTest();
</script>
<body>
</body>
</html>

深入分析

关于怎么用fetch去触发浏览器的下载框界面和什么时候该触发下载框界面已经在代码部分进行了详细注释
这里深入分析一下为什么以下代码会触发下载框界面

1
FileSaver.saveAs(blob, filename);

先上一段无需任何依赖实现下载框弹窗的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const req = {
method: 'get',
headers: {
token: 'token_something'
}
};

fetch('/get/setting/file?type=json', req).then(response => {
const disposition = response.headers.get('content-disposition');
if (disposition && disposition.match(/attachment/)) {
let filename = disposition.replace(/attachment;.*filename=/, '').replace(/"/g, '');
filename = filename && filename !== '' ? filename : 'file';
response.blob().then(blob => {
/**
* 不依赖任何库后的代码变动区
* 可以看出原来blob这个对象可以使用createObjectURL方法创建出一个url
* 这个url是"blob:"开头, 形式如下
* blob:http://127.0.0.1:3000/b1b07221-dd60-46bd-a33f-59c2f416c87e
* 这个url是属于浏览器本地的blob存储区
* 即相当于将response返回的body部分以blob对象形式保存到了浏览器本地的blob存储区
* 这时再创建一个a标签, 并把url赋给a标签的href, 并触发"click"事件即可弹出浏览器的下载框了.
*/
const fileUrl = URL.createObjectURL(blob);
const saveLink = document.createElement('a');
saveLink.href = fileUrl;
saveLink.download = filename;
let e = new MouseEvent('click');
saveLink.dispatchEvent(e);
// 使用完ObjectURL后需要及时释放, 否则会浪费浏览器存储区资源.
URL.revokeObjectURL(fileUrl);
});
}
});

代码注释部分已详细说明了”file-saver”库的基本实现原理

为什么还要选择file-saver库

从”file-saver”源码中可以看到,

此库基于模拟a标签点击的原理对各种浏览器下的创建元素, 事件处理做兼容性处理,

这样可以更好的保证触发弹出浏览器的下载框.

blob对象

从代码中可以看到一个很重要的对象, 即blob对象.

blob的主体内容就是将可文本化的对象或文本数据二进制化,

相当于后端工程师在开发中读取磁盘上一个文件后得到的stream对象.

所以实际上不管后端有没有在响应头部申明content-disposition并包含attachment标记.

我们都可以使用response.blob方法将响应的body内容blob化,

并使用createObjectURL方法将blob对象临时保存到浏览器本地的blob存储区.

值得注意的是, 生成的这个blob url在被销毁之前其对应内容可以在同进程的浏览器的任何新标签中被直接访问到.

如果想自定义blob数据, 可以使用以下方式:

1
2
3
const data = 'something'
// Blob中传入的数据参数必须为数组
let blob = new Blob([data])

blob参考

其它参考

显示 Gitment 评论