在本文中我们将深入探讨前端文件下载的多种实现方式、常见技术挑战以及相关优化策略,希望能够帮助开发者在实现文件下载功能时更好地平衡用户体验与技术细节,提供更加流畅和可靠的下载体验。
目录
下载文件
前端实现文件下载的方式多种多样,选择适合的方案取决于文件类型、大小、是否需要动态生成以及浏览器兼容性等因素,主要的下载方式有以下几种:
1)<a>标签的download属性下载:这种方式通常适用于文件已经存储在服务器上,可以通过URL直接访问的场景
优点:简单易用,适用于大多数浏览器
缺点:仅支持下载浏览器能访问到的静态文件,不适用于动态生成的文件或文件需要经过服务器处理的场景
html
<a href="path/to/your/file.txt" download="filename.txt">Download</a>
2)使用Blob和URL.createObjectURL():当需要动态生成文件内容(如用户输入的内容或后台生成的文件)时,可以使用Blob对象来创建文件内容,然后通过URL.createObjectURL()生成一个临时的URL最后触发下载
优点:可以动态生成内容进行下载,适用于客户端生成或从服务器获取的内容
缺点:对于大文件内存消耗较大,可能影响性能
javascript
const data = new Blob(["Hello, world!"], { type: "text/plain" });
const url = URL.createObjectURL(data);
const a = document.createElement("a");
a.href = url;
a.download = "example.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
3)使用FileSaver.js库:一个流行的前端库,它简化了Blob数据的文件保存操作,通过它开发者可以更轻松地实现文件下载,特别是跨浏览器的兼容性问题
优点:封装了Blob和URL.createObjectURL()简化代码并解决浏览器兼容性问题
缺点:需要引入第三方库
javascript
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
saveAs(blob, "example.txt");
4)后台生成并提供文件下载链接:当文件由服务器动态生成(如导出 Excel、PDF 等文件)时前端通常会向后台发送请求,后台生成文件并返回一个可以下载的链接前端通过获取到的链接触发下载
javascript
fetch('/download/file')
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "generated-file.txt";
a.click();
URL.revokeObjectURL(url);
});
一般来说<a>标签的download属性适用于简单的下载需求,Blob和FileSaver.js提供了更多的灵活性来处理动态生成的文件,而通过后台生成文件的方式适合需要服务器支持的大型文件下载,当然为了兼容IE浏览器,我们一般在IE浏览器借助msSaveBlob进行操作:

并行下载
在前端实现并行下载文件时通常的需求是同时发起多个文件下载请求,而每个文件的下载可以独立进行互不影响,JS可以通过并行的HTTP请求实现这一目标,这里我们可以使用使用async/await和Promise.all进行并行下载,示例代码如下所示:
javascript
const fileUrls = [
'path/to/file1.txt',
'path/to/file2.txt',
'path/to/file3.txt'
];
const downloadFile = async (url) => {
try {
const response = await fetch(url);
const blob = await response.blob();
const a = document.createElement('a');
const objectUrl = URL.createObjectURL(blob);
a.href = objectUrl;
a.download = url.split('/').pop();
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(objectUrl);
} catch (error) {
console.error('Error downloading file:', error);
}
};
const downloadAllFiles = async () => {
const downloadPromises = fileUrls.map(url => downloadFile(url));
await Promise.all(downloadPromises);
console.log('All files downloaded');
};
downloadAllFiles();
如下代码,我们通过调用后端接口来实现并行文件下载,由于 并行下载时所有请求会在同一时刻发出,如果服务器无法处理这么多并发请求就可能导致某些请求失败,这里我给每个请求设置了超时配置并且为下载请求实现一个简单的重试机制,以确保在请求失败时自动重试:
javascript
import axios from 'axios'
const Index = () => {
const fileList = [
"097d6805-531b-4bf9-93ab-3e06cf6232a0",
"135a9c08-b536-48a4-9ffc-cdebdeb595ce",
"64719c61-b287-435b-8fd8-b89992580b01",
"b289c12b-fad1-4dbb-8858-67b9e97948eb"
]
// 下载每个文件的函数
const downloadFile = async (id: string, retries = 3) => {
try {
const res = await axios.get(`https://localhost:7189/api/File/DownloadFile?id=${id}`, { responseType: 'blob',timeout: 10000 });
const url = window.URL.createObjectURL(new Blob([res.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `file-${id}.mp4`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
if (retries > 0) {
console.log(`Retrying download for file ${id}... (${3 - retries} retries left)`);
await downloadFile(id, retries - 1); // 重试
} else {
console.error('Error downloading file after retries:', error);
}
}
}
// 处理并行下载
const handleDownload = async () => {
const downloadPromises = fileList.map(id => downloadFile(id));
await Promise.all(downloadPromises); // 并行执行所有下载请求
}
return (
<div onClick={() => handleDownload()}>并行下载</div>
)
}
export default Index
最终实现的效果如下所示,可以看到增加了超时配置和重试机制,下载的容错率大大提升了:

压缩下载
在JS中我们可以使用第三方库如jszip来实现ZIP压缩,jszip是一个纯JS编写的库允许在浏览器中创建、读取、编辑和压缩 ZIP 文件,下面是一个简单的例子展示了如何使用jszip来压缩一个文件,终端执行如下命令按照第三方库:
javascript
npm install jszip
接下来我们在并行下载的时候,将下载的文件进行压缩,如下所示:
javascript
import axios from 'axios';
import JSZip from "jszip";
const Index = () => {
const fileList = [
"097d6805-531b-4bf9-93ab-3e06cf6232a0",
"135a9c08-b536-48a4-9ffc-cdebdeb595ce",
"64719c61-b287-435b-8fd8-b89992580b01",
"b289c12b-fad1-4dbb-8858-67b9e97948eb"
];
// 下载每个文件的函数
const downloadFile = async (id: any, retries = 3) => {
try {
const res = await axios.get(`https://localhost:7189/api/File/DownloadFile?id=${id}`, {
responseType: 'blob',
timeout: 10000,
});
console.log("res", res)
// 从响应头中获取文件后缀名
const fileExtension = res.headers['x-file-extension'];
return { blob: res.data, fileExtension };
} catch (error) {
if (retries > 0) {
console.log(`Retrying download for file ${id}... (${3 - retries} retries left)`);
return await downloadFile(id, retries - 1); // 重试
} else {
console.error('Error downloading file after retries:', error);
return null;
}
}
};
// 处理并行下载并压缩
const handleDownload = async () => {
const zip = new JSZip();
const downloadPromises = fileList.map(async (id) => {
const fileBlob = await downloadFile(id);
if (fileBlob) zip.file(`file-${id}.${fileBlob.fileExtension}`, fileBlob.blob);
});
await Promise.all(downloadPromises);
// 生成 ZIP 文件
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = window.URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'downloaded_files.zip');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
};
return (
<div onClick={() => handleDownload()}>并行下载并压缩</div>
);
};
export default Index;
后端代码如下所示,这里我们设置了自定义的头部来传递给前端用来拿文件后缀名:
cs
// 根据文件id下载文件
[HttpGet]
public IActionResult DownloadFile(Guid id)
{
var file = reponsitory.Queryable<FileUpload>().InSingle(id);
if (file == null)
{
return NotFound();
}
string filePath = Path.Combine(MergedDirectory, file.Name);
byte[] bytes = System.IO.File.ReadAllBytes(filePath);
// 获取文件后缀名
string fileExtension = Path.GetExtension(filePath);
// 设置自定义响应头,传递文件后缀名
Response.Headers.Add("X-File-Extension", fileExtension);
return File(bytes, "application/octet-stream", file.Name);
}
最终呈现的效果如下所示:

切片下载
对于大文件下载切片下载是一种常用的技术,可以通过将文件分成多个小块来并行下载,从而提高下载效率并减少单次下载超时的风险。切片下载的基本思路是将文件分成若干部分,每一部分独立下载,最后合并这些部分
javascript
import axios from 'axios';
const Index = () => {
// 下载文件的函数
const downloadFile = async (id) => {
let chunkIndex = 0;
const chunkSize = 2 * 1024 * 1024; // 2MB
const fileChunks = [];
let totalSize = 0;
try {
while (true) {
const response = await axios.get(`/api/downloadFileChunk?id=${id}&chunkIndex=${chunkIndex}&chunkSize=${chunkSize}`, {
responseType: 'blob',
timeout: 10000,
});
console.log('Response status:', response);
// 获取文件部分
const chunk = response.data;
fileChunks.push(chunk);
// 读取响应头中的Content-Range,获取文件总大小
const contentRange = response.headers['content-range'];
console.log('Content-Range:', contentRange);
if (contentRange) {
const matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);
if (matches) {
totalSize = parseInt(matches[3]);
console.log('Total size:', totalSize);
}
}
// 计算当前已下载的字节数
const downloadedSize = fileChunks.reduce((sum, currentChunk) => sum + currentChunk.size, 0);
// 判断是否已经下载完整个文件
if (downloadedSize >= totalSize) {
break;
}
chunkIndex++;
}
// 拼接所有的文件块
const fileBlob = new Blob(fileChunks);
// 下载文件
const url = URL.createObjectURL(fileBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'downloadedFile.mp4';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('下载文件时出错:', error);
}
};
// 处理并行下载
const handleDownload = async () => {
await downloadFile('64719c61-b287-435b-8fd8-b89992580b01');
}
return (
<div onClick={() => handleDownload()}>并行下载</div>
)
}
export default Index;