前端开发中实现文件下载最基础的方式莫过于使用a标签和window.open方法。这两种方式简单直接,一行代码就能搞定,但实际使用时会遇到不少坑。记得我第一次用a标签下载跨域图片时,发现点击后图片直接在浏览器打开了,根本没有触发下载,当时还以为是代码写错了。
a标签的download属性看起来很美,它能指定下载文件名,但实际有个致命限制——只能下载同源文件。跨域文件即使加了download属性也会变成普通跳转。比如下面这个典型例子:
html复制<a href="https://example.com/image.jpg" download="my-image.jpg">下载图片</a>
点击后会直接在浏览器打开图片而非下载。这是因为浏览器出于安全考虑,禁止了跨域资源的强制下载。同理,使用JavaScript动态创建a标签也一样受限:
javascript复制const link = document.createElement('a')
link.href = 'https://example.com/file.pdf'
link.download = 'document.pdf'
link.click()
window.open的表现更让人困惑。对于PDF、图片等浏览器可预览的文件类型,它会直接在新窗口打开文件。只有像.zip/.exe这类浏览器无法直接渲染的文件才会触发下载。更糟的是,某些浏览器还会拦截这种"非用户主动触发"的弹窗,导致下载失败。
解决跨域下载的关键在于将文件内容转换为前端可操作的Blob对象。Blob就像个安全的沙盒,浏览器允许我们对获取的二进制数据进行处理。整个过程分为三个关键步骤:
这里有个实际项目中的代码示例:
javascript复制function downloadFile(url, filename) {
fetch(url)
.then(response => response.blob())
.then(blob => {
const blobUrl = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = blobUrl
a.download = filename || 'download'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
// 释放内存
setTimeout(() => URL.revokeObjectURL(blobUrl), 100)
})
}
从服务端获取文件时,正确的MIME类型至关重要。我曾遇到过下载的PDF在iOS设备上无法打开的问题,最后发现是Blob的type设置错误。推荐几种获取文件类型的方法:
javascript复制const type = response.headers.get('content-type')
javascript复制import { fileTypeFromStream } from 'file-type'
const type = await fileTypeFromStream(response.body)
const blob = new Blob([data], { type })
javascript复制const ext = filename.split('.').pop()
const typeMap = {
pdf: 'application/pdf',
jpg: 'image/jpeg',
png: 'image/png'
}
const blob = new Blob([data], { type: typeMap[ext] || 'application/octet-stream' })
要让跨域下载正常工作,服务端必须正确配置CORS。我遇到过最棘手的情况是CDN返回了正确的CORS头,但OSS却没有。以下是服务端需要设置的关键头信息:
code复制Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Disposition
Content-Disposition: attachment; filename="example.pdf"
特别注意:如果需要在前端自定义文件名,服务端不应设置Content-Disposition头,否则浏览器会优先使用服务端提供的文件名。
对于需要认证的私有文件,通常有两种处理方式:
javascript复制fetch(`https://api.example.com/file?token=${token}`)
javascript复制fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
})
在OSS场景下,还可以使用预签名URL。比如阿里云OSS的签名示例:
javascript复制// 服务端生成签名URL返回给前端
const signedUrl = 'https://bucket.oss-cn-beijing.aliyuncs.com/file.pdf?OSSAccessKeyId=xxx&Expires=xxx&Signature=xxx'
downloadFile(signedUrl)
下载大文件时,可以考虑使用流式处理和进度提示。这是我项目中用到的方案:
javascript复制async function downloadLargeFile(url, name, onProgress) {
const response = await fetch(url)
const reader = response.body.getReader()
const contentLength = +response.headers.get('Content-Length')
let receivedLength = 0
const chunks = []
while(true) {
const {done, value} = await reader.read()
if(done) break
chunks.push(value)
receivedLength += value.length
onProgress(receivedLength / contentLength)
}
const blob = new Blob(chunks)
// 后续下载逻辑...
}
健壮的下载功能需要完善的错误处理:
javascript复制async function safeDownload(url) {
try {
const response = await fetch(url)
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const filename = getFilenameFromHeaders(response)
const blob = await response.blob()
// 下载逻辑...
} catch(error) {
console.error('下载失败:', error)
showToast('下载失败,请重试')
// 可以考虑重试机制
}
}
function getFilenameFromHeaders(response) {
const disposition = response.headers.get('Content-Disposition')
const filename = disposition?.split('filename=')[1]
return filename ? decodeURIComponent(filename) : 'download'
}
虽然现代浏览器都支持Blob API,但仍有几点需要注意:
一个实用的兼容性检查方案:
javascript复制function checkBlobSupport() {
try {
new Blob()
return !!URL.createObjectURL
} catch(e) {
return false
}
}
if(!checkBlobSupport()) {
// 降级到传统下载方式
window.location.href = fileUrl
}
在实际项目中,我通常会先尝试Blob方案,失败后再自动降级到传统方式,确保功能在所有环境下都能正常工作。