在前后端分离的开发项目中,我们常常有下载文件或者报表的需求。
如果只是简单的下载,我们可以简单使用a标签请求后端就可以了,不过一旦涉及到后端报错的回调、等待动画、进度条这种的,就没有任何办法了。
所以,这里可以使用axios进行请求,获取到后端的文件流后,自己进行生成文件。这样就可以完成上面的那三种情况了。
我们点击下载按钮,将表单内容传入,返回一个对应的excel文件。
前端界面的话,如下所示
定义一下UserDTO.java
,用来进行传参
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
,用来统一后端的响应
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
,用来处理下载请求
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
,用来对外输入
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);
}
}
}
这样,后端就准备完成了,接下来看看前端怎么写
<!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加载?这很简单,自己加上去吧
在测试的时候,发现了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;
@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
<!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>
界面是这样的,十分简单,点击按钮就可进行下载了
如果我们想展示下载的进度条,那该怎么办,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;
@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 "成功";
}
}
前端的样式及请求
<!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>
样式就像这样,当我们点击按钮,根据下载进度展示进度条
主要是自己定义的这个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;
@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);
}
}
}
<!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>
基本上来说,上面的方法步骤都是一样的,只是流的类型不同。
application/vnd.ms-excel;charset=utf-8
或者application/octet-stream
arraybuffer
或者blob
需要注意的点:
controller
上直接返回null
即可千万不要等用到的时候,才到处翻博客
我是半月,祝你幸福!!!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。