使用axios下载文件

一、介绍

在前后端分离的开发项目中,我们常常有下载文件或者报表的需求。

如果只是简单的下载,我们可以简单使用a标签请求后端就可以了,不过一旦涉及到后端报错的回调、等待动画、进度条这种的,就没有任何办法了。

所以,这里可以使用axios进行请求,获取到后端的文件流后,自己进行生成文件。这样就可以完成上面的那三种情况了。

二、使用

1)下载Excel文件

我们点击下载按钮,将表单内容传入,返回一个对应的excel文件。

前端界面的话,如下所示

image-20220403155847861

定义一下UserDTO.java,用来进行传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.demo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {

private String name;

private String sex;

private Integer age;
}

定义一下ResultData.java,用来统一后端的响应

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
package com.example.demo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResultData<T> {

private Integer errCode;

private String errMsg;

private T data;

public static ResultData success(){
return new ResultData(0, "", null);
}

public static ResultData fail(String errMsg){
return new ResultData(-1, errMsg, null);
}

}

再写一个TestController.java,用来处理下载请求

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
package com.example.demo.controller;

import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.example.demo.dto.ResultData;
import com.example.demo.dto.UserDTO;
import com.example.demo.utils.MyFileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;

@Slf4j
@RestController
@CrossOrigin(exposedHeaders = {"Content-disposition", "Access-Control-Allow-Origin"})
@RequestMapping("/test")
public class TestController {

@RequestMapping("/download")
public ResultData download(@RequestBody UserDTO userDTO, HttpServletResponse response){
if(userDTO.getAge()>18)
return ResultData.fail("愿你永远18岁");
try {
ExcelWriter writer = ExcelUtil.getWriter(true);
writer.writeRow(userDTO, true);
MyFileUtil.downloadFile(response, writer, "用户示例.xlsx");
return null;
} catch (Exception e) {
log.error("出错了");
return ResultData.fail("网络波动,请稍后再试");
}
}

}

还有一个MyFileUtil.java,用来对外输入

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
package com.example.demo.utils;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.poi.excel.ExcelWriter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;

@Slf4j
@Component
public class MyFileUtil {

public static void downloadFile(HttpServletResponse response, ExcelWriter writer, String filename){
ServletOutputStream out = null;
try {
out = response.getOutputStream();
response.setContentType("application/vnd.ms-excel;charset=utf-8");
response.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));

writer.flush(out, true);
} catch (IOException e) {
log.error("io异常", e);
} finally {
writer.close();
IoUtil.close(out);
}
}

public static void downloadFile(HttpServletResponse response, File file, String filename){
OutputStream out = null;
try {
out = response.getOutputStream();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
response.setHeader("Content-Length", String.valueOf(FileUtil.size(file)));

BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file.getPath()));
OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
IoUtil.copy(fis, toClient);
out.flush();
} catch (Exception e) {
log.error("io异常", e);
} finally {
IoUtil.close(out);
}
}

}

这样,后端就准备完成了,接下来看看前端怎么写

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>
<body>
<div id="app">
<h2>下载Excel</h2>
<el-form :model="formData" label-width="80px" style="width: 300px;" size="mini">
<el-form-item label="姓名">
<el-input v-model="formData.name" style="width: 200px;"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="formData.sex">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="年龄">
<el-input-number v-model="formData.age" style="width: 200px;" controls-position="right" :min="1" :max="100"></el-input-number>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="download">下载</el-button>
</el-form-item>
</el-form>
</div>

<script>
const vm = new Vue({
el: "#app",
data: {
formData: {
name: "半月无霜",
sex: "男",
age: 18
},
},
methods: {
download(){
let url = "http://localhost:8080/test/download"
axios.post(url, this.formData, {
responseType: 'arraybuffer'
}).then(res => {
window.downloadExcel(res);
}).catch(error => {

})
}
},
})

// 得到文件流后,前端生成文件,创建出a标签进行点击
var downloadExcel = function (res) {
if (!res) {
return;
}
const fileName = res.headers["content-disposition"].split("=")[1];
const blob = new Blob([res.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8",
});
const url = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.style.display = "none";
aLink.href = url;
aLink.setAttribute("download", decodeURI(fileName));
document.body.appendChild(aLink);
aLink.click();
document.body.removeChild(aLink);
window.URL.revokeObjectURL(url);
}
</script>
</body>
</html>

前端就就是这样的,你说没有异常显示和Loading加载?这很简单,自己加上去吧

2)下载其他文件

在测试的时候,发现了excel文件有一定的特殊性,若是平常的文件,可以这样子做。

这里以gif图片为例,来进行下载。

首先是后端,下载请求controller控制器,

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
package com.example.demo.controller;

import cn.hutool.core.io.FileUtil;
import com.example.demo.utils.MyFileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.io.File;

@Slf4j
@RestController
@CrossOrigin(exposedHeaders = {"Content-disposition", "Access-Control-Allow-Origin"})
@RequestMapping("/test")
public class TestController {

@RequestMapping("/downloadImage")
public String downloadImage(@RequestParam String imgPath, HttpServletResponse response){
if(FileUtil.exist(imgPath)){
File file = new File(imgPath);
String suffix = FileUtil.getSuffix(file);
MyFileUtil.downloadFile(response, file, "图片文件测试." + suffix);
return "成功";
}
return "失败";
}

}

MyFileUtil.java就不贴出来了,上面就有

前端代码,这次responseType设置为blob

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>
<body>
<div id="app">
<h2>下载图片</h2>
<form>
图片地址:{{ imgPath }}<br>
<el-button type="primary" @click="downloadImage" size="mini">下载</el-button>
</form>
</div>

<script>
const vm = new Vue({
el: "#app",
data: {
imgPath: "E:\\repository\\aaa.gif"
},
methods: {
downloadImage(){
let url = "http://localhost:8080/test/downloadImage";
axios({
url: url,
method: "post",
params: {
imgPath: this.imgPath
},
responseType: 'blob',
}).then(res => {
window.downloadFile(res);
})
}
},
})

var downloadFile = function (res) {
if (!res) {
return;
}
const fileName = res.headers["content-disposition"].split("=")[1];
const blob = new Blob([res.data], {
type: 'application/zip'
});
const url = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.style.display = "none";
aLink.href = url;
aLink.setAttribute("download", decodeURI(fileName));
document.body.appendChild(aLink);
aLink.click();
document.body.removeChild(aLink);
window.URL.revokeObjectURL(url);
}
</script>
</body>
</html>

界面是这样的,十分简单,点击按钮就可进行下载了

image-20220407232344253

3)下载进度条

如果我们想展示下载的进度条,那该怎么办,UI样式我们就选ElementUI,这次我们需要用到axios中一个叫onDownloadProgress的参数,它允许为下载处理进度事件

修改一下后端,为后端增加一个方法

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
package com.example.demo.controller;

import cn.hutool.core.io.FileUtil;
import com.example.demo.utils.MyFileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.io.File;

@Slf4j
@RestController
@CrossOrigin(exposedHeaders = {"Content-disposition", "Access-Control-Allow-Origin"})
@RequestMapping("/test")
public class TestController {

@RequestMapping("/downloadProgress")
public String downloadProgress(HttpServletResponse response){
// 尽量选择一个比较大的文件,50MB左右
File file = new File("E:\\repository\\123.exe");
String suffix = FileUtil.getSuffix(file);
MyFileUtil.downloadFile(response, file, "进度条下载测试." + suffix);
return "成功";
}

}

前端的样式及请求

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>测试</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>

<body>
<div id="app">
<h2>进度条</h2>
<el-button type="primary" @click="downloadProgress" size="mini">下载</el-button>
<el-progress :percentage="percentage"></el-progress>
</div>

<script>
const vm = new Vue({
el: "#app",
data: {
percentage: 0,
},
methods: {
downloadProgress() {
let url = "http://localhost:8080/test/downloadProgress";
this.percentage = 0
axios({
url: url,
method: "post",
responseType: 'blob',
onDownloadProgress: (e) => {
console.log(e);
this.percentage = Math.round(e.loaded / e.total * 100);
}
}).then(res => {
window.downloadFile(res);
}).catch(error => {

})
}
},
})

var downloadFile = function (res) {
if (!res) {
return;
}
const fileName = res.headers["content-disposition"].split("=")[1];
const blob = new Blob([res.data], {
type: 'application/zip'
});
const url = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.style.display = "none";
aLink.href = url;
aLink.setAttribute("download", decodeURI(fileName));
document.body.appendChild(aLink);
aLink.click();
document.body.removeChild(aLink);
window.URL.revokeObjectURL(url);
}
</script>
</body>

</html>

样式就像这样,当我们点击按钮,根据下载进度展示进度条

download

三、主要代码

1)后端

主要是自己定义的这个MyFileUtil.java

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
package com.example.demo.utils;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.poi.excel.ExcelWriter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;

@Slf4j
@Component
public class MyFileUtil {

public static void downloadFile(HttpServletResponse response, ExcelWriter writer, String filename){
ServletOutputStream out = null;
try {
out = response.getOutputStream();
response.setContentType("application/vnd.ms-excel;charset=utf-8");
response.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));

writer.flush(out, true);
} catch (IOException e) {
log.error("io异常", e);
} finally {
writer.close();
IoUtil.close(out);
}
}

public static void downloadFile(HttpServletResponse response, File file, String filename){
OutputStream out = null;
try {
out = response.getOutputStream();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
response.setHeader("Content-Length", String.valueOf(FileUtil.size(file)));

BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file.getPath()));
OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
IoUtil.copy(fis, toClient);
out.flush();
} catch (Exception e) {
log.error("io异常", e);
} finally {
IoUtil.close(out);
}
}

}

2)前端

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>测试</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>

<body>
<div id="app">
<h2>下载Excel</h2>
<el-form :model="formData" label-width="80px" style="width: 300px;" size="mini">
<el-form-item label="姓名">
<el-input v-model="formData.name" style="width: 200px;"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="formData.sex">
<el-radio label="男"></el-radio>
<el-radio label="女"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="年龄">
<el-input-number v-model="formData.age" style="width: 200px;" controls-position="right" :min="1"
:max="100"></el-input-number>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="download">下载</el-button>
</el-form-item>
</el-form>

<hr>
<h2>下载图片</h2>
<form>
图片地址:{{ imgPath }}<br>
<el-button type="primary" @click="downloadImage" size="mini">下载</el-button>
</form>

<hr>
<h2>进度条</h2>
<el-button type="primary" @click="downloadProgress" size="mini">下载</el-button>
<el-progress :percentage="percentage"></el-progress>
</div>

<script>
const vm = new Vue({
el: "#app",
data: {
formData: {
name: "半月无霜",
sex: "男",
age: 18
},
imgPath: "E:\\repository\\aaa.jpg",
percentage: 0,
},
methods: {
download() {
let url = "http://localhost:8080/test/download";
let loading = this.$loading({
text: "正在下载"
});
axios.post(url, this.formData, {
responseType: 'arraybuffer'
}).then(res => {
console.log(res);
if (res.headers["content-type"] == "application/json") {
let resjson = JSON.parse(ab2str(res.data));
this.$message.error(resjson.errMsg);
} else {
window.downloadExcel(res);
}
loading.close();
}).catch(error => {
this.$message.error(error);
})
},
downloadImage() {
let url = "http://localhost:8080/test/downloadImage";
axios({
url: url,
method: "post",
params: {
imgPath: this.imgPath
},
responseType: 'blob',
}).then(res => {
window.downloadFile(res);
})
},
downloadProgress() {
let url = "http://localhost:8080/test/downloadProgress";
this.percentage = 0
axios({
url: url,
method: "post",
responseType: 'blob',
onDownloadProgress: (e) => {
console.log(e);
this.percentage = Math.round(e.loaded / e.total * 100);
}
}).then(res => {
window.downloadFile(res);
}).catch(error => {

})
}
},
})

var downloadExcel = function (res) {
if (!res) {
return;
}
const fileName = res.headers["content-disposition"].split("=")[1];
const blob = new Blob([res.data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8",
});
const url = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.style.display = "none";
aLink.href = url;
aLink.setAttribute("download", decodeURI(fileName));
document.body.appendChild(aLink);
aLink.click();
document.body.removeChild(aLink);
window.URL.revokeObjectURL(url);
}

var downloadFile = function (res) {
if (!res) {
return;
}
const fileName = res.headers["content-disposition"].split("=")[1];
const blob = new Blob([res.data], {
type: 'application/zip'
});
const url = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.style.display = "none";
aLink.href = url;
aLink.setAttribute("download", decodeURI(fileName));
document.body.appendChild(aLink);
aLink.click();
document.body.removeChild(aLink);
window.URL.revokeObjectURL(url);
}

function ab2str(buf) {
let encodedString = String.fromCharCode.apply(null, new Uint8Array(buf));
let decodedString = decodeURIComponent(escape(encodedString));
return decodedString;
}

function ab2hex(buffer) {
const hexArr = Array.prototype.map.call(new Uint8Array(buffer), function (bit) {
return ('00' + bit.toString(16)).slice(-2)
})
return hexArr.join('');
}
</script>
</body>

</html>

四、总结

基本上来说,上面的方法步骤都是一样的,只是流的类型不同。

  1. 后端返回流,类型设置为application/vnd.ms-excel;charset=utf-8或者application/octet-stream

  2. 前端axios请求,responseType设置为arraybuffer或者blob

  3. 得到文件流后,前端生成文件,创建出模拟a标签进行点击

需要注意的点:

  1. 后端如果成功生成流并返回,controller上直接返回null即可
  2. 由于是前后端分离项目,必定会有前后端跨域的问题,所以请注意跨域问题

千万不要等用到的时候,才到处翻博客

我是半月,祝你幸福!!!