React--》文件下载优化技巧与最佳实践

在本文中我们将深入探讨前端文件下载的多种实现方式、常见技术挑战以及相关优化策略,希望能够帮助开发者在实现文件下载功能时更好地平衡用户体验与技术细节,提供更加流畅和可靠的下载体验。

目录

下载文件

并行下载

压缩下载

切片下载

下载文件

前端实现文件下载的方式多种多样,选择适合的方案取决于文件类型、大小、是否需要动态生成以及浏览器兼容性等因素,主要的下载方式有以下几种:

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;    
相关推荐
赵谨言39 分钟前
基于数据挖掘的网络入侵检测关键技术研究
经验分享·毕业设计
Blossom.11842 分钟前
基于Python的机器学习入门指南
开发语言·人工智能·经验分享·python·其他·机器学习·个人开发
大土豆的bug记录4 小时前
鸿蒙进行视频上传,使用 request.uploadFile方法
开发语言·前端·华为·arkts·鸿蒙·arkui
maybe02094 小时前
前端表格数据导出Excel文件方法,列自适应宽度、增加合计、自定义文件名称
前端·javascript·excel·js·大前端
HBR666_4 小时前
菜单(路由)权限&按钮权限&路由进度条
前端·vue
A-Kamen4 小时前
深入理解 HTML5 Web Workers:提升网页性能的关键技术解析
前端·html·html5
锋小张6 小时前
a-date-picker 格式化日期格式 YYYY-MM-DD HH:mm:ss
前端·javascript·vue.js
鱼樱前端6 小时前
前端模块化开发标准全面解析--ESM获得绝杀
前端·javascript
yanlele6 小时前
前端面试第 75 期 - 前端质量问题专题(11 道题)
前端·javascript·面试
SunAqua7 小时前
《软件安装与使用教程》— NVIDIA CUDA在Windows的安装教程
windows·经验分享·随笔