axios实现文件下载功能

在开发中遇到了需要实现文件下载的功能,起初以为只用<a>标签就能搞定,<a>标签确实能够搞定常见的场景。但是像导出或者在header里面添加了特殊字段的时候,使用<a>标签就搞不定了,又不想去使用原生XMLHttpRequest,因为又一堆的兼容性需求(技术能力不够ε=ε=ε=┏(゜ロ゜;)┛,有现成的兼容方案为啥要自己造轮子呢,说不定还爆胎>逃666),所以萌生基于Axios封装。

Ajax无法下载文件的原因

浏览器的GET(frame、a)和POST(form)请求具有如下特点:

  • response会交由浏览器处理
  • response内容可以为二进制文件、字符串等

Ajax请求具有如下特点:

  • response会交由Javascript处理
  • response内容仅可以为字符串

Ajax本身设计的目标就是用来获取文本数据的,而不是用来搞二进制的。

XMLHttpRequest 2.0新增的数据类型Blob

看张老师的文章 理解DOMString、Document、FormData、Blob、File、ArrayBuffer数据类型

有了Blob类型之后,JavaScript处理二进制进一步增强,可以说以后想怎样就怎样(废话)。

文件下载实现

response header前提条件

服务端返回的头部需要设置

Content-Disposition: “attachment; filename=xxxx.docx;”

<a>标签的直接下载

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
34
import qs from 'qs';
/**
* downloadByUrl
* @param config - 配置参数
* @param config.url - 地址
* @param config.params - querystring参数.
* @param filename 文件名称,包括扩展名部分(不一定生效)
*
* @description
* 原理是使用<a>的href和download属性,所以filename不一定会生效, 浏览器机制问题.
*
* @see https://zhuanlan.zhihu.com/p/58888918
* @see https://github.com/kennethjiang/js-file-download
*/
export function downloadByUrl(
config: {
url: string;
params: any;
},
filename = ''
): void {
var tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href =
config.url + qs.stringify(config.params, { addQueryPrefix: true });
tempLink.setAttribute('download', filename);
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank');
}

document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
}

主要是使用了js-file-download的代码,进行了简单的封装,而且去除了对Blob的依赖,主要为了兼容低版本的浏览器。同时使用了qsquerystring参数进行了简单的处理。

基于axios的实现

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
import fileDownload from 'js-file-download';
import logger from 'js-logger';


/**
* 提取文件名.
* @param response axios的response
* @description 从reponse header的content-disposition中提取文件名.
*/
const extractFilenameFromResponseHeader = (response: AxiosResponse): string => {
// content-disposition: "attachment; filename=xxxx.docx;"
const contentDisposition = response.headers['content-disposition'];
const patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*');
const result = patt.exec(contentDisposition) as RegExpExecArray;
let filename = '';

if (result) {
filename = result.length > 0 ? result[1] : '';
}
// 解码之前尝试去除空格和双引号
// content-disposition: "attachment; filename=\"xxxx.docx\";"
return decodeURIComponent(filename.trim().replace(new RegExp('"', 'g'), ''));
};


const axiosInstance = axios.create({/*
可以传递公共默认的axios配置,但是注意reponse interceptor中默认把reponse.data作为JSON解析的情况
*/});

// https://www.zhihu.com/question/263323250
// https://github.com/axios/axios/issues/815#issuecomment-340972365
const downloadByAxios = async function (
config: AxiosRequestConfig,
filename = ''
): Promise<any | AxiosResponse<any>> {
let response = await axiosInstance({
...config,
responseType: 'blob', // 指定类型
});

let resBlob = response.data; // <--- store the blob if it is
let respData = null;

// 如果确定接口response.data是二进制,所以请求失败时是JSON.
// 这里只对response.data做JSON的尝试解析
try {
let respText = await new Promise((resolve, reject) => {
let reader = new FileReader();
reader.addEventListener('abort', reject);
reader.addEventListener('error', reject);
reader.addEventListener('loadend', () => {
resolve(reader.result as string);
});
reader.readAsText(resBlob);
});
respData = JSON.parse(respText as string); // <--- try to parse as json evantually
} catch (err) {
// ignore
}
// 如果response.data能够确定是二进制,则respData = null说明请求成功
// 否则 respData !== null说明请求失败
if (respData as ResponseData) {
logger.error(respData);

// 方便调用者有进一步的 then().catch()处理
return Promise.reject({
...respData,
});
} else {
// 触发浏览器下载
// 如果没有传递filename尝试从Content-Disposition提取
fileDownload(resBlob, filename || extractFilenameFromResponseHeader(
response
));
// 方便调用者有进一步的 then().catch()处理
return Promise.resolve({
...response,
});
}
};

代码大部分都是参考这个issue实现的,只有少部分的个人代码。

基于axios实现的功能:

  1. 可以使用axios的所有参数,不管请求是GET或者POST
  2. 解决了在header中添加额外的参数的需求
  3. 可以指定filename,如果服务端没有设置content-disposition的情况
  4. 返回Promise方便调用者进一步处理请求

缺点:

  1. 只能使用独立的axios实例,不能公用一个axios

    本来想把下载功能使用axios interceptor拦截器实现,但是返回的response.dataBlob二进制,但是其它的response interceptor默认前提都是把response.data当作JSON处理,导致全部出现异常,所以把下载功能独立出来,更方便维护。

  2. 使用独立的axios实例,所以项目中的axios默认配置需要重新配置一遍

参考链接

Content-Disposition

axios.js实现下载功能

axios.js #815实现

StreamSaver

FileSaver

js-file-download
理解DOMString、Document、FormData、Blob、File、ArrayBuffer数据类型