一. 下载文件
- 下载:包括但不限于导出、保存……
- 文件:包括但不限于图片、文档、表格……
数据来源:文件路径、二进制流文件数据
-
二进制文件流数据
当后端返回的是二进制流数据时,则需要借助Blob对象进行读取,才可获得文件
- 封装完整的使用 Blob 和 URL.createObjectURL 下载文件流的 util 函数:
/** * @func downloadExprotFile * @desc 根据文件流下载文件 * @param {string} fileStream 文件流 * @param {string} name 文件名 * @param {string} extension 文件后缀 * @param {string} [type] 文件类型(当不知道类型时可以选择不写) * @returns {object} undefined * @example * downloadExprotFile(data, '证件申办表', 'docx') * .then(res => { this.$message.success('导出成功') }) * .catch(errMsg => { this.$message.error(errMsg || '导出失败') }) */ export const downloadExprotFile = (fileStream, name, extension, type = "") => { return new Promise((resolve, reject) => { const blob = new Blob([fileStream], {type: type || fileStream.type}); const fileName = `${name}.${extension}`; if ("download" in document.createElement("a") && fileStream.type) { // 非IE下载 const elink = document.createElement("a"); elink.download = fileName; elink.style.display = "none"; elink.href = URL.createObjectURL(blob); document.body.appendChild(elink); elink.click(); URL.revokeObjectURL(elink.href); document.body.removeChild(elink); resolve(fileStream) } else { // IE10+下载 navigator.msSaveBlob(blob, fileName); reject(fileStream) } }) };- fileName后端定义,前端获取
- 如果前端下载文件流时,fileName文件名由后端决定并设置在Headers中的
Content-disposition字段上 - 那么前端通过
res.headers["content-disposition"]进行获取时 - 确保后端在响应头也配置了
Access-Control-Expose-Headers允许前端使用该字段,否则前端将无法获取
// response interceptor 响应拦截 service.interceptors.response.use( response => { // 获取headers中的filename文件名且进行解码处理 response.headers['content-disposition'] && response.data && (response.data.filename = decodeURIComponent(response.headers['content-disposition'].split(';')[1].split('filename=')[1])) // …… return response.data }, error => { return Promise.reject(error) } ) - 如果前端下载文件流时,fileName文件名由后端决定并设置在Headers中的
-
文件路径
图片资源路径分为同源(本地)文件和跨域非同源文件,而这两种路径,用不同方法保存,在PC端和移动端又有不同的表示形式 保存的最大问题就是当图片路径是跨域非同源的这种情况。
-
图片资源路径为同源(本地):
- PC端和移动端都表现为弹出图片新页面:以下保存方法的1和2
- 想要保存,PC端右键或
Ctrl+S保存,移动端只能进行长按操作
- 想要保存,PC端右键或
- PC端和移动端都被识别为文件正常保存操作:以下保存方法的3、4、5、6
- PC端会自动弹出保存窗口
- 移动端不同手机、不同浏览器会有不同的保存表现形式,如预览下载,弹窗下载,……
- PC端和移动端都表现为弹出图片新页面:以下保存方法的1和2
-
图片资源路径为跨域非同源:首先肯定得让后端设置允许跨域访问,这是保存前提。
- PC端和移动端都表现为弹出图片新页面:以下保存方法的1、2、3、4
- 想要保存,PC端右键或
Ctrl+S保存,移动端只能进行长按操作
- 想要保存,PC端右键或
- PC端和移动端都被识别为文件正常保存操作:以下保存方法的5、6
- PC端会自动弹出保存窗口
- 移动端不同手机、不同浏览器会有不同的保存表现形式,如预览下载,弹窗下载,……
结束了? 没有!!! 跨域非同源的海报图用5和6的方法保存,PC端正常,但移动端还存在很多不确定因素。
比如,在Vivo的QQ浏览器上,会自动进入预览功能,但无法看到图片,却依然可以保存成功,在相册可查看。
比如,在rognyao play5的百度浏览器上,完全无法下载,但自带的浏览器却可以正常保存。
- PC端和移动端都表现为弹出图片新页面:以下保存方法的1、2、3、4
-
结论:
- 当图片路径是跨域非同源时,想办法实现为同源或本地路径的资源。
- 比如,二维码海报图,不让后端生成了,前端自个儿生成二维码,与背景图结合,保存时用canvas转化成图片。
- 否则,当你用以下方法的5或6时,PC端正常,移动端就祈祷甲方爸爸的手机及浏览器刚好支持吧。
- 我也想、很想一步到位。。。
- 当然,如果不考虑用户体验的话,那么a标签再加个保存提示,so easy。
-
常见6种保存方法:
// 1. window.location.href = "url" // 2. window.open(url) // 3. <a target="_blank" download="poster.jpg" :href="url">保存至相册</a> // 4. saveImg(Url){ if(!Url){ Toast.fail('暂无海报!'); return; } var blob=new Blob([''], {type:'application/octet-stream'}); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = Url; // a.target = '_blank'; a.download = Url.replace(/(.*\/)*([^.]+.*)/ig,"$2").split("?")[0]; console.log("a:", a) var e = document.createEvent('MouseEvents'); e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); a.dispatchEvent(e); URL.revokeObjectURL(url); }, // 5. downloadExprotFile (fileStream, name, extension, type = "") { return new Promise((resolve, reject) => { const blob = new Blob([fileStream], {type: type || fileStream.type}); const fileName = `${name}.${extension}`; if ("download" in document.createElement("a") && fileStream.type) { // 非IE下载 const elink = document.createElement("a"); elink.download = fileName; elink.style.display = "none"; elink.href = URL.createObjectURL(blob); document.body.appendChild(elink); elink.click(); URL.revokeObjectURL(elink.href); document.body.removeChild(elink); resolve(fileStream) } else { // IE10+下载 navigator.msSaveBlob(blob, fileName); reject(fileStream) } }) }, //注意: fetch(Url, { mode: "no-cors" }); "no-cors"是无法解决跨域问题的,设置之后将无法获取响应结果 fetch(Url).then((res) => res.blob()).then((fileStream) => { this.downloadExprotFile(fileStream, 'poster', 'jpg') .then(res => { this.$message.success('保存成功') }) .catch(errMsg => { this.$message.error(errMsg || '保存失败') }) }) // 6. downloadIamge(imgsrc, name) { let image = new Image(); image.setAttribute("crossOrigin", "anonymous"); image.onload = function() { let canvas = document.createElement("canvas"); canvas.width = image.width; canvas.height = image.height; let context = canvas.getContext("2d"); context.drawImage(image, 0, 0, image.width, image.height); let url = canvas.toDataURL("image/png"); //得到图片的base64编码数据 let a = document.createElement("a"); // 生成一个a元素 let event = new MouseEvent("click"); // 创建一个单击事件 a.download = name || "海报"; // 设置图片名称没有设置则为默认 a.href = url; // 将生成的URL设置为a.href属性 a.dispatchEvent(event); // 触发a的单击事件 }; image.src = imgsrc; },
Blob对象转JSON格式
- 应用场景:文件下载请求时,后端接口成功时响应数据是文件流Blob对象,错误时响应数据是json格式,因为响应头设置为
responseType为blob, 针对下载失败提示处理,需将blob对象转为json数据格式 BlobToJson函数封装:参数字段
res.type和data.code,根据自身实际需要,进行相应调整
/**
* @func: blobToJson
* @description: 将blob对象转为json数据
* @param {Object} res Blob对象
* @return {Boolean} true/false false表示导出有误,中断执行
* @example: blobToJson(res)
*/
function blobToJson(res) {
if (res && res.type === 'application/json') {
var enc = new TextDecoder('utf-8')
res.arrayBuffer().then(buffer => {
const data = JSON.parse(enc.decode(new Uint8Array(buffer))) || {}
if (data.code == 500) {
Message({
message: data.msg || '导出失败',
type: 'error',
duration: 2 * 1000
})
return false
}
return true
})
} else {
return true
}
}
二. 上传文件:包括但不限于图片、文档、表格
上传文件流数据: FormData & multipart/form-data
- 上传文件功能,可以借助各种组件库插件,也可以自己手写原生form表单指定enctype为
multipart/form-data,继而进行提交 - 提交方式有两种:
- 通过插件或form的action或相应属性,设置好接口路径,这样submit提交时会自动封装文件数据上传到后端
- 手动 new一个FormData对象,用来装载文件流数据,再将数据作为接口入参
Post请求接口的contentType必须指定为
multipart/form-data,否则将无法上传文件流数据let _formData = new FormData(); _formData.append("file", this.importFile[0].raw); // this.importFile[0].raw 就是binary文件流数据 // 所以入参都append到_formData 中,再将_formData作为接口参数data传给后端
延伸知识:Method 与 Headers.ContentType的
-
无论是原生js的ajax,还是vue的axios,封装请求接口时,有两个参数特别注意优雅设置:Method 和 Headers.ContentType。
- Method :请求方法,表明要对给定资源执行的操作
- Headers.ContentType:指定服务器与客户端发送或接收的数据类型
- 注意get请求方式与contentType数据类型无关
- 它的请求不存在请求实体部分,入参的键值对会放置在 URL 尾部,浏览器自动把数据转换成一个字串
- 如果有特殊符号,最好自行先预编码,预防不同浏览器编码方式差异,导致后端接收有误。
- 请求报文和响应报文的contentTyp:
封装接口时,前端与后端约定好接收的数据类型,以免开发结束后联调时出错
-
用post提交方式,常用ContentType数据类型有三种:
- ‘application/x-www-form-urlencoded’
- $.ajax、form等常见的默认数据类型,它的默认行为表现形式是自动将入参数据编码为键值对,这是标准的编码格式。
- ‘application/json’
- 设置该数据类型后,需要用
JSON.stringify将入参数据序列化成JSON 字符串
- 设置该数据类型后,需要用
- ‘multipart/form-data’
- 设置该数据类型,则需要将数据封装进
new FormData对象中,或用form表单指定enctype为multipart/form-data,再提交数据
- 设置该数据类型,则需要将数据封装进
- ‘application/x-www-form-urlencoded’