前端h5开发过程中会经常遇到下载文件或导出文件的需求,有时候接口响应慢或者出于优化交互的目的需要前端自定义处理文件下载。
下面由浅入深分享一些常用的处理方法。
一、window.location.href
这也是最简单的解决方式,
window.location.href = url
- url即文件或接口的地址
- 如需要额外参数,通过url后问号拼接参数,后端get请求方式接收。
缺陷:
- 直接访问可能会覆盖当前页面地址,影响用户体验
- 图片、pdf 等url资源会表现为预览而非下载
二、window.open
window.open(url, '_blank')
- url即文件或接口的地址
- 如需要额外参数,通过url后问号拼接参数,后端get请求方式接收。
- _blank表示新开标签页打开,防止覆盖当前页面地址。
缺陷:
- 虽然避免了覆盖当前网址,但也需要新开标签页打开链接,用户体验上也不完美
- 图片、pdf 等url资源会表现为预览而非下载
三、动态a标签
// 封装
function downloadFile (url, name) {
url = url || ''
name = name || ''
let ele = document.createElement('a')
ele.target = '_blank'
ele.href = url
ele.download = name
ele.click()
ele = null
}
// 调用
downloadFile(url)
- url即文件或接口的地址
- 如需要额外参数,通过url后问号拼接参数,后端get请求方式接收
- a标签的download表明链接的资源将被下载,加了这个属性告诉浏览器以弹窗提示下载保存文件。
- 服务端需要配置url资源的响应头
Content-disposition
值为attachment
,浏览器识别会调用下载弹窗,进一步配置filename
值即下载的默认文件名。
- 注:跨域情况下需要服务端配置
Access-Control-Expose-Headers
,才能在客户端获取到Content-disposition
字段,详见链接。
- name参数是前端自定义的下载文件名,带后缀,给download属性赋值的,仅资源响应头未配置filename时生效,一般后端配置好了的情况下可不传。
缺陷:
- a标签的download属性只在访问同源地址资源时有效,跨域情况下就和上述window.open表现一致了。
- url资源或接口响应头配置了filename时,前端无法自定义下载文件名。
- 前端需要自定义展示下载进度条需求时无法支持。
四、ajax请求
上述动态创建a标签
的方式在一些特殊场景下表现不够。
- 例如:跨域的图片、视频、pdf等资源在浏览器下访问默认是预览而非下载。
解决方式就是通过ajax请求来处理。
1、数据传输形式
- 如果是图片等带文件后缀名的资源,可以直接使用;如果是后端接口形式,需要接口返回文件流形式的数据。
// java
InputStream fis = new BufferedInputStream(new FileInputStream(file))
- 前端请求接口,并以blob形式接收数据
// js
xhr.responseType = 'blob'
2、创建xhr请求
function downloadFromApi (url) {
const xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.responseType = 'blob' // 设置responseType是必须的
xhr.onload = e => {
if (xhr.status === 200) {
const response = xhr.response
// 在这里获取到接口响应的数据
console.log(response)
}
}
xhr.send(null)
})
}
- url即资源或接口地址
- 如需要额外参数:
- 一种是通过url后问号拼接参数传输,请求方法用get,一般下载文件用这种。
- 一种是通过请求体传输,
xhr.send(data)
,请求方法用post,一般上传文件用这种,当然你两种方式一起用也行。
3、封装promise
封装成promise形式,便于异步处理,在接口响应慢或文件较大下载慢的时候给出加载中提示。
// 封装
function downloadFromApi (url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.responseType = 'blob'
xhr.onload = e => {
if (xhr.status === 200) {
const response = xhr.response
resolve(response)
} else {
reject(new Error(`${xhr.status}:请求失败`))
}
}
xhr.send(null)
})
xhr.error = err => {
reject(err)
}
}
// 调用(以 vue + element 示例)
const message = this.$message('加载中...')
downloadFromApi(url).then(res => {
message.close()
})
4、获取文件名
文件名建议服务端配置,前端获取。
- 服务端在响应头 Content-disposition 字段配置文件名,例如:
Content-disposition: attachment;filename=test.xlsx
- 前端获取响应头,字符串截取:
const dispoition = xhr.getResponseHeader('Content-Disposition') || ''
const nameStr = dispoition.split(';')[1] || ''
const fileName = decodeURIComponent(nameStr.split('=')[1] || '')
前端自定义文件名也可以,需要带文件后缀名。
5、blob文件下载
前端以blob形式获取数据后下载下来。
const response = xhr.response
if (window.navigator.msSaveOrOpenBlob) {
// 兼容处理,ie下的blob下载
window.navigator.msSaveOrOpenBlob(response, fileName)
} else {
// 常规浏览器,blob下载处理
const href = URL.createObjectURL(response)
let ele = document.createElement('a')
ele.target = '_blank'
ele.href = href
ele.download = fileName
ele.click()
ele = null
URL.revokeObjectURL(href)
}
- 前端获取到的blob数据会临时存储在本地内存,然后读取blob对象生成本地url映射,然后通过动态创建a标签调用浏览器下载。
- fileName 即上述获取的接口定义的文件名。
6、下载进度
文件比较大时从接口获取完所有数据会有一个过程,通过a标签直接访问接口地址时浏览器会自动处理下载进度,而通过ajax请求的只能自行处理。
- 服务端需要配置响应头的
Content-Length
,值为文件大小(未配置的话会在文件传输完成后才弹窗提示下载,响应感知太慢)。 - 前端监听xhr对象的
progress
事件:
xhr.onprogress = e => {
if (e.lengthComputable) {
const percentComplete = e.loaded / e.total
// 打印当前已完成的进度比例
console.log(percentComplete)
}
}
- onprogress 是在 xhr 完成之前周期性调用的函数。
- lengthComputable 表示所关联的资源是否具有可以计算的长度。
7、完整代码
/**
* @description: ajax下载/导出文件
* @param {string} url 资源或接口地址,可携带参数
* @param {function} progressCallback 选填,前端自定义的下载进度回调
*/
function downloadFromApi (url, progressCallback) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.responseType = 'blob'
xhr.onload = e => {
if (xhr.status === 200) {
const response = xhr.response
resolve(response)
const dispoition = xhr.getResponseHeader('Content-Disposition') || ''
const nameStr = dispoition.split(';')[1] || ''
const fileName = decodeURIComponent(nameStr.split('=')[1] || '')
if (window.navigator.msSaveOrOpenBlob) {
// 兼容处理,ie下的blob下载
window.navigator.msSaveOrOpenBlob(response, fileName)
} else {
const href = URL.createObjectURL(response)
let ele = document.createElement('a')
ele.target = '_blank'
ele.href = href
ele.download = fileName
ele.click()
ele = null
URL.revokeObjectURL(href)
}
} else {
reject(new Error(`${xhr.status}:请求失败`))
}
}
xhr.error = err => {
reject(err)
}
xhr.onprogress = e => {
if (e.lengthComputable) {
const percentComplete = e.loaded / e.total
progressCallback && progressCallback(percentComplete)
}
}
xhr.send(null)
})
}
使用:
// 调用(以 vue + element 示例)
const message = this.$message('加载中...')
downloadFromApi(url).then(res => {
message.close()
})
五、小结
-
window.location.href
和window.open
的方式虽然简单,但用户体验上不太友好。 - 常规情况下使用
动态a标签
的方式即可。 - 如果需要弹窗下载跨域的图片、视频、pdf等浏览器能直接预览的资源,需要使用
ajax请求
方式。 - 如果url资源或接口已配置filename,而前端又想自定义文件下载名时,使用
ajax请求
方式。 - 如果需要前端自定义展示下载文件进度,使用
ajax请求
方式。
参考链接:
1、https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest