使用axios下载文件
一、介绍
在前后端分离的开发项目中,我们常常有下载文件或者报表的需求。
如果只是简单的下载,我们可以简单使用a标签请求后端就可以了,不过一旦涉及到后端报错的回调、等待动画、进度条这种的,就没有任何办法了。
所以,这里可以使用axios进行请求,获取到后端的文件流后,自己进行生成文件。这样就可以完成上面的那三种情况了。
二、使用
1)下载Excel文件
我们点击下载按钮,将表单内容传入,返回一个对应的excel文件。
前端界面的话,如下所示
定义一下UserDTO.java
,用来进行传参
package com.example.demo.dto; | |
import lombok.AllArgsConstructor; | |
import lombok.Data; | |
import lombok.NoArgsConstructor; | |
public class UserDTO { | |
private String name; | |
private String sex; | |
private Integer age; | |
} |
定义一下ResultData.java
,用来统一后端的响应
package com.example.demo.dto; | |
import lombok.AllArgsConstructor; | |
import lombok.Data; | |
import lombok.NoArgsConstructor; | |
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
,用来处理下载请求
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; | |
public class TestController { | |
public ResultData download( 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
,用来对外输入
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; | |
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); | |
} | |
} | |
} |
这样,后端就准备完成了,接下来看看前端怎么写
<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
控制器,
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; | |
public class TestController { | |
public String downloadImage( 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
<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> |
界面是这样的,十分简单,点击按钮就可进行下载了
3)下载进度条
如果我们想展示下载的进度条,那该怎么办,UI样式我们就选ElementUI,这次我们需要用到axios
中一个叫onDownloadProgress
的参数,它允许为下载处理进度事件
修改一下后端,为后端增加一个方法
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; | |
public class TestController { | |
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 "成功"; | |
} | |
} |
前端的样式及请求
<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> |
样式就像这样,当我们点击按钮,根据下载进度展示进度条
三、主要代码
1)后端
主要是自己定义的这个MyFileUtil.java
中
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; | |
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)前端
<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> |
四、总结
基本上来说,上面的方法步骤都是一样的,只是流的类型不同。
- 后端返回流,类型设置为
application/vnd.ms-excel;charset=utf-8
或者application/octet-stream
- 前端axios请求,responseType设置为
arraybuffer
或者blob
- 得到文件流后,前端生成文件,创建出模拟a标签进行点击
需要注意的点:
- 后端如果成功生成流并返回,
controller
上直接返回null
即可 - 由于是前后端分离项目,必定会有前后端跨域的问题,所以请注意跨域问题
千万不要等用到的时候,才到处翻博客
我是半月,祝你幸福!!!