业务需求中出现一个接口既有导入功能,又需要有导出功能,接口具体情况如下:
导入excel表格接口中,前端传递上传的excel文档,后端判断文档是否格式正常,如果格式正常,则返回json格式的成功数据,如果格式不成功,则返回错误的excel文档,前端负责把错误的excel文档导出,第三种情况,如果5s内再次导入文档,则后端返回不能频繁导入的错误json数据。
总结一下:
情况1:前端导入excel表格,触发请求,后端响应文件,blob格式,导入数据失败,前端导出文件;
情况2:前端导入excel表格,触发请求,后端响应json格式,导入数据正常,存储到数据库中;
情况3:前端5s内再次导入excel表格,触发请求,后端响应json格式,导入数据失败,不能频繁导入;
导出接口处理如下:
const apiImportExcel = (params) => {
return axiosBizcenterMarketingAct.request({
url: 'https//www.xxx.com/importExcelOptLessNum',
method: 'post',
data: params
})
}
// 上传excel,失败下载excel,成功返回msg
const uploading = async (data) => {
uploadLoading.value = true
let file = data.file
let params = new FormData()
params.append('file', file)
let res = await apiImportExcel(params)
console.log(res, 1234)
blobData.value = res
isShowDownload.value = true
uploadMessage.value = '处理失败,请整理表格后重新上传文档'
uploadLoading.value = false
dialogVisible.value = true
}
// 导出
const downErrorExcel = () => {
let fileName = '错误提示.xlsx'
if (!blobData.value) {
return
}
const blob = new Blob([blobData.value], { type: 'application/vnd.ms-excel' })
const url = URL.createObjectURL(blob)
downloadCommon(url, fileName)
}
// 下载处理
const downloadCommon = (url: string, fileName: string) => {
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link) // 下载完成移除元素
}
控制台输出为
axios拦截器输出为
由于后端返回文件时,前端必须设置responseType为blob,否则导出的excel会由于格式问题,打不开;
// 上传excel
const apiImportExcel = (params) => {
return axiosBizcenterMarketingAct.request({
url: 'https"//www.test.com/importExcelOptLessNum',
method: 'post',
responseType: 'blob',
data: params
})
}
如下图:控制台上方为拦截器输出内容,下方为拿到的data数据
但是当设置了响应格式为blob,则当后端返回json格式时,前端只能读到blob数据,blob数据看不到里面的内容。
相信大家看到了虽然返回都是blob格式,但是情况1和情况2返回的blob格式的type不一样。所以可以如下处理:
处理代码如下:
// 上传excel,失败下载excel,成功返回msg
const uploading = async (data) => {
uploadLoading.value = true
let file = data.file
let params = new FormData()
params.append('file', file)
let res = await apiImportExcel(params)
console.log(res, 1234)
if(res.type === 'text/xml') {
isShowDownload.value = true
uploadMessage.value = '处理成功,成功导入'
} else {
blobData.value = res
isShowDownload.value = true
uploadMessage.value = '处理失败,请整理表格后重新上传文档'
}
uploadLoading.value = false
dialogVisible.value = true
}
由于情况2和情况3返回都是json格式的问题,提示用户接口处理是否成功,所以正常的流程是后端接口返回的什么样的字符串,前端页面直接提示用户接口处理结果,而且目前是三种情况,将来扩展可能接口出现第四种第五种情况,所以使用上面的type判断,并不合适。
不设置responseType为blob,会导致excel因为格式问题打不开,所以只能让接口返回blob格式,那就只能当接口返回类型为text/xml的时候,将blob解析成json格式,然后再读取json字符串了。
如何解析blob格式呢?下面有三种方法:
// 方法1
const getJSONObjectFromBlob = (blob: Blob) => {
const reader = new FileReader()
reader.readAsArrayBuffer(blob)
reader.onload = function () {
console.info('reader.result>>>', reader.result) //ArrayBuffer {}
//将 ArrayBuffer 转换成Blob
const buf = new Uint8Array(reader.result as ArrayBuffer)
let enc = new TextDecoder('utf-8')
let text = enc.decode(buf)
try {
const obj = JSON.parse(text)
return obj
} catch (error) {
console.error('解析错误', error)
return error
}
}
}
// 方法2
const getJSONObjectFromBlob = (blob: Blob) => {
const text = await (new Response(blob)).text();
try {
const obj = JSON.parse(text)
return obj
} catch (error) {
console.error('解析错误', error)
return error
}
}
// 方法3
const getJSONObjectFromBlob = (blob: Blob) => {
const text = await blob.text();
try {
const obj = JSON.parse(text)
return obj
} catch (error) {
console.error('解析错误', error)
return error
}
}
看到这里,大家可能看到方法2和方法3都是读取Blob对象原型链上的text()方法进行解析,其实text()方法就是根据方法1这样设计的,不过目前这个方法1有点小瑕疵,大家能够发现吗?
方法1中使用FileReader对象去读取blob,然后通过onload去解析,不知道大家是否了解onload事件,onload是一个异步任务,所以js引擎走到这里不会去执行内部逻辑,而是等函数执行完才去宏任务队列中取出它,并执行。那函数还有未执行呢?走到这里,函数还没有返回值,所以大家应该猜到了吧,这里会返回undefined。
那怎么解决呢?大家有没有想到await和async关键字,但是使用这两个关键字,必须要是一个promise,所以把方法1改造成返回promise,然后去读取json对象,使用await关键字等待,promise响应再执行后续操作,就可以啦~~~
代码如下:
const getTextFromBlob = (blob: Blob): Promise<string> => {
//将Blob 对象转换成 ArrayBuffer
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsArrayBuffer(blob)
reader.onload = function () {
console.info('reader.result>>>', reader.result) //ArrayBuffer {}
// 经常会遇到的异常 Uncaught RangeError: byte length of Int16Array should be a multiple of 2
// const buf = new int16array(reader.result);
// console.info(buf);
//将 ArrayBuffer 转换成Blob
const buf = new Uint8Array(reader.result as ArrayBuffer)
let enc = new TextDecoder('utf-8')
resolve(enc.decode(buf))
}
reader.onerror = function (e) {
console.error('转换成ArrayBeffer失败')
reject(e)
}
})
}
// 解析 text/xml类型的blob, 成为JSON对象(失败则是json字符串)
export async function getJSONObjectFromBlob(blob: Blob): Promise<Record<string, unknown> | unknown> {
let text = ''
try {
text = await getTextFromBlob(blob)
const obj: Record<string, unknown> = JSON.parse(text)
return obj
} catch (error) {
return error
}
}
好了,为了能够通熟易懂,我还是使用了最简单的方法,代码如下:
const apiImportExcel = (params) => {
return axiosBizcenterMarketingAct.request({
url: 'https"//www.test.com/importExcelOptLessNum',
method: 'post',
data: params
})
}
// 解析application/json类型的blob, 成为JSON对象(失败则是json字符串)
const getJSONObjectFromBlob = (blob: Blob) => {
const text = await blob.text();
try {
const obj = JSON.parse(text)
return obj
} catch (error) {
console.error('解析错误', error)
return error
}
}
// 上传excel,失败下载excel,成功返回msg
const uploading = async (data) => {
uploadLoading.value = true
let file = data.file
let params = new FormData()
params.append('file', file)
let res = await apiImportExcel(params)
if (res.type === 'application/json') {
// 读取Blob
getJSONObjectFromBlob(res).then((res) => {
uploadMessage.value = res.msg
})
} else {
blobData.value = res
isShowDownload.value = true
uploadMessage.value = '处理失败,请整理表格后重新上传文档'
}
uploadLoading.value = false
dialogVisible.value = true
}
const downloadCommon = (url: string, fileName: string) => {
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link) // 下载完成移除元素
}
写到这里,有人会想到使用axios去请求接口会出现这种情况,那使用fetch呢?window对象自带的请求接口的方法,目前兼容性已经满足大部分浏览器了,所以我又做了以下内容
我们直接改造上面的uploading函数就行,说干就干
// 上传excel,失败下载excel,成功返回msg
const uploading = async (data) => {
uploadLoading.value = true
let file = data.file
let params = new FormData()
params.append('file', file)
window.fetch("https//www.xxx.com/importExcelOptLessNum", {
method: 'post',
body: params,
headers: {
'Authorization': accessToken,
}
}).then(res => {
console.log(res, '----1---')
return res.json();
// return res.blob();
}).tnen(res => {
console.log(res, '0----3---------')
blobData.value = res;
isShowDownload.value = true
uploadMessage.value = '处理失败,请整理表格后重新上传文档';
}).catch(err => {
console.error(err)
});
uploadLoading.value = false
dialogVisible.value = true
}
结果就是不行,从上图中可以看到,fetch确实不需要在请求头这里设置响应体类型,但是当数据响应给客户端时,必须要调用Response中的json()方法或者blob()方法,但在这之前,无法判断res是什么数据类型。
虽然情况1和情况2两次的blob的类型不一致,一个是application/vnd.ms-excel类型,一个是text/xml,但是由于第三种情况的介入,必须要获取到后端返回的json格式数据显示。
设置了responseType为blob类型,接口返回的数据如下:
没有设置responseType时,接口返回的数据如下:
目前是 解析 text/xml类型的blob, 成为JSON对象。
更多关于Blob对象的知识,可以点击链接查看:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
如果大家有什么更好的处理方法,欢迎评论区讨论~~~