请求库为fetch
export const exportDataByPost = async (url, params) => {
console.log('------exportDataByPost------');
let AFFTK = localStorage.getItem('AFFTK')
const token = `Bearer ${AFFTK}`
const merchant = localStorage.getItem('MID') ? localStorage.getItem('MID').split('_')[1] : ''
try{
const response = await fetch(baseUrl + prefix + url, {
mode: 'cors',
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": token,
merchant,
},
body: JSON.stringify(params),
})
if (response.headers.get('content-type') !== 'application/json') {
response.blob().then((blob) => {
const a = window.document.createElement('a');
const downUrl = window.URL.createObjectURL(blob);// 获取 blob 本地文件连接 (blob 为纯二进制对象,不能够直接保存到磁盘上)
let filename = "download.xls";
if (response.headers.get('content-disposition') && response.headers.get('content-disposition').indexOf("filename=") !== -1) {
filename = response.headers.get('content-disposition').split('filename=')[1];
a.href = downUrl;
a.download = `${decodeURI(filename.split('"')[1])}` || "download.xls";
a.click();
window.URL.revokeObjectURL(downUrl);
}
}).catch(error =>{
message.error(error);
});
} else {
let res = await response.json();
message.error(res.msg);
}
}catch(err){
message.error('下载超时');
}
}
请求库采用fetch,导出方法封装的思路
- 封装的方法为异步方法,返回一个Promise
- 如果响应头content-type参数为application/json,返回的响应为json对象,需要通过response.json()转化为前端可以解析的json对象,转化后的json对象中有code和msg字段,用于向用户提示错误信息
- 如果返回的响应为文件流,从原始响应到可下载到浏览器的文件的过程中,需要经历以下过程
- 从响应头content-disposition中获取到文件名称
- 通过response.blob()将原生响应转化为前端可解析的blob
- 在response.blob的回调函数中,以特定方法实现浏览器下载文件
注意点
通过fetch发起的请求,无法直接通过诸如response.headers['content-disposition']获取content-disposition参数,这一点与axios不同。
遇到的问题
本地测试可以实现正常下载,但是部署到测试环境出现 ”无法获取文件名“ 的问题。经追踪,发现在测试环境是因为无法从响应头获取 content-disposition 字段信息,从而导致无法获取文件名。
分析原因
代码部署到测试环境之后,页面访问的是远程测试服务器的前端文件目录,而从前端文件目录发起请求后需要经过nginx反向代理,这时后端下载请求响应头中未暴露出 content-disposition 字段信息。
解决方案
服务端代码设置header暴露出 content-disposition 字段信息(在nginx配置文件中设置无效,只能在服务端代码中添加相关代码)。
参考文章
前端axios获取二进制流下载excel并解决无法获header问题
Fetch / ajax 不能获取response中的所有headers的解决方法(适用nginx)
请求库为axios
/**
* 对应导出请求,返回的响应为json对象,不是文件流的情况,
* 需要重新请求一遍
* 不过不带responseType: 'blob'
* @param url 请求地址
* @param params 请求表单参数
*/
function exportDataByJson (url, params) {
axios({
url: baseUrl + url,
method: 'post',
data: params
}).then((res) => {
Message({
type: 'error',
message: res.msg
})
}).catch((err) => {
console.error(err)
})
}
/**
* 导出文件post请求
* @param url 请求地址
* @param params 请求表单参数
*/
export async function exportDataByPost (url, params) {
console.log('------exportDataByPost------')
try {
const response = await axios({
url: baseUrl + url,
method: 'post',
responseType: 'blob', // 这句话很重要
data: params
})
// 当导出请求,返回的响应为json对象时,
// 根本就不会走到这里,直接走到后面catch里面了
// 所以下面console.log(response)根本不会打印出来
console.log(response)
if (response.status !== 200) {
console.log('网络或服务器异常!')
return
}
let blob = new Blob([response.data], { type: response.headers['content-type'] })
const a = window.document.createElement('a')
const downUrl = window.URL.createObjectURL(blob)// 获取 blob 本地文件连接 (blob 为纯二进制对象,不能够直接保存到磁盘上)
let filename = 'download.xls'
if (response.headers['content-disposition'] && response.headers['content-disposition'].indexOf('filename=') !== -1) {
filename = response.headers['content-disposition'].split('filename=')[1]
a.href = downUrl
a.download = `${decodeURI(filename.split('"')[1])}` || 'download.xls'
a.click()
window.URL.revokeObjectURL(downUrl)
}
} catch (err) {
console.error(err.type)
if (err.type === 'application/json') {
console.log('导出请求返回的是json数据啊啊啊啊啊')
// 再次请求接口,获取错误提示信息,不过要去掉responseType: 'blob',
exportDataByJson(url, params)
}
}
}
axios实现导出与fetch实现导出的不同在于:
- 前者的请求参数中需要加
responseType: 'blob'
- 原生响应转化为blob的方法不一样
其中,
responseType: 'blob'
,这句代码很重要。如果没有这句代码,虽然可以成功将文件下载下来,不过下载下来的文件内容乱码,如下图所示:
如果遇到导出请求返回响应为json对象数据,axios相对于fetch需要再请求一次接口获取错误信息,提示给用户,网络消耗增大。
除了以上方法之外,下述postExportFile方法也是一种解决方案,该方法是通过构建表单的方式实现下载。但不是所有post请求导出都可以通过下述postExportFile方法正确导出文件,因此遇到导出,最好还是采用上述方案。
postExportFile (params, url) {
// params是post请求需要的参数,url是请求url地址
let form = document.createElement('form')
form.style.display = 'none'
form.action = url
form.method = 'post'
document.body.appendChild(form)
for (let key in params) {
let input = document.createElement('input')
input.type = 'hidden'
input.name = key
input.value = params[key]
form.appendChild(input)
}
let inputToken = document.createElement('input')
inputToken.type = 'hidden'
inputToken.name = 'token'
inputToken.value = localStorage.getItem('token')
form.appendChild(inputToken)
form.submit()
form.remove()
},
exportData () {
let startDate = ''
let endDate = ''
if (this.queryValues.date) {
let [sDate, eDate] = this.queryValues.date
startDate = moment(sDate).format('YYYY-MM-DD')
endDate = moment(eDate).format('YYYY-MM-DD')
}
let qDate = deepCopy(this.queryValues)
delete qDate.date
let params = Object.assign(
{},
{
start_date: startDate,
end_date: endDate,
page: this.currentPageNo,
page_size: this.pageSize
},
qDate
)
exportDataByPost('/report/order/export', params)
/* this.$store.dispatch('reportOrderExport', params).then(res => {
if (res.data) {
this.postExportFile(params, baseUrl + '/report/order/export')
}
}) */
}
timeout支持
由于fetch无法像axios一样通过timeout配置项实现前端请求超时控制,需要自己封装,以下为自己封装的带超时控制的导出功能。
参考文章
ES6 fetch(input, init) 设置超时(timeout)
/**
* 导出文件公共函数
* 由于fecth请求,没有超时timeout配置项,
* 需要封装实现类似于axios timeout的功能
* 在此背景下,特封装了一个带前端请求超时控制的fecth导出文件公共函数
*/
import fetch from 'dva/fetch'
import { message } from 'antd'
import { baseUrl } from './config'
const prefix = '/api'
const _fetch = (fetchRequest, timeout) => {
timeout = timeout > 0 ? timeout : 0;
let breaker = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('请求超时'))
}, timeout);
});
/*
* Promise.race(iterable)方法返回一个promise,
* 这个promise在iterable中的任意一个promise被解决或拒绝后,
* 立刻以相同的解决值被解决或以相同的拒绝原因被拒绝。
*/
return timeout === 0 ? fetchRequest : Promise.race([fetchRequest, breaker]);
};
const exportDataByPostFn = async (url, params) => {
let AFFTK = localStorage.getItem('AFFTK')
const token = `Bearer ${AFFTK}`
const merchant = localStorage.getItem('MID') ? localStorage.getItem('MID').split('_')[1] : ''
/* 'http://localhost:8081/user/login' */
const response = await fetch(baseUrl + prefix + url, {
mode: 'cors',
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": token,
merchant,
},
body: JSON.stringify(params),
})
// response.headers.get('content-type') 可能是 application/json
// 也可能是 application/json; charset=utf-8
if (!response.headers.get('content-type').includes('application/json')) {
response.blob().then((blob) => {
const a = window.document.createElement('a');
const downUrl = window.URL.createObjectURL(blob);// 获取 blob 本地文件连接 (blob 为纯二进制对象,不能够直接保存到磁盘上)
let filename = "download.xls";
if (response.headers.get('content-disposition') && response.headers.get('content-disposition').indexOf("filename=") !== -1) {
filename = response.headers.get('content-disposition').split('filename=')[1];
a.href = downUrl;
a.download = `${decodeURI(filename.split('"')[1])}` || "download.xls";
a.click();
window.URL.revokeObjectURL(downUrl);
}
}).catch(error =>{
message.error(error);
});
} else {
let res = await response.json();
message.error(res.msg);
}
}
/**
* 导出excel文件post请求
* @param url 请求地址
* @param url 请求地址样例:'/commission/extra-model/export'
* @param params 请求表单参数
* @param params 格式
* {
* a: 1,
* b: 2
* }
*/
export const exportDataByPost = async (url, params) => {
console.log('------exportDataByPost------');
let fetchRequest = exportDataByPostFn(url, params)
let res = await _fetch(fetchRequest, 10 * 1000)
return res;
}