使用 a 标签下载

  1. 这是最简单直接的下载方式,适用于已知文件 URL 的情况;

  2. 优点:

    1. 简单易用;
    2. 兼容性好 (包括旧版浏览器)
  3. 缺点:

    1. 无法处理需要授权的情况 (如需要携带 cookie 或 token)
    2. 无法处理跨域资源 (除非服务器设置 CORS)
  4. 示例代码:

    <!DOCTYPE html>
    <html>
    <head>
        <title>a标签下载示例</title>
    </head>
    <body>
        <a href="https://example.com/files/sample.pdf" download="sample.pdf">下载PDF文件</a>
        
        <!-- 动态创建a标签下载 -->
        <button onclick="downloadFile()">动态下载</button>
        
        <script>
            function downloadFile() {
                const link = document.createElement('a');
                link.href = 'https://example.com/files/sample.pdf';
                link.download = 'sample.pdf';
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            }
        </script>
    </body>
    </html>
    

使用 window.open 或 location.href

  1. 这种方式类似于 a 标签,但更适用于通过 JavaScript 触发下载;

  2. 优点:

    1. 代码极其简单;
  3. 缺点:

    1. 会打开新窗口;
    2. 无法自定义下载文件名;
    3. 同源策略限制;
    4. 可能被浏览器拦截;
  4. 示例代码:

    window.open('https://example.com/files/sample.pdf', '_blank');
    // 或
    window.location.href = 'https://example.com/files/sample.pdf';
    

使用 Fetch API + Blob 下载

  1. 这种方法适用于需要添加认证头或处理 API 返回的文件数据;

  2. 优点:

    1. 可以设置请求头 (如认证信息)
    2. 可以处理各种 HTTP 方法 (GET/POST等)
    3. 可以在下载前对数据进行处理;
  3. 缺点:

    1. 代码相对复杂;
    2. 对于大文件可能会占用较多内存;
    3. IE 不支持 (但可以使用 polyfill)
  4. 示例代码:

    <button onclick="downloadFile()">下载文件</button>
    
    <script>
    function downloadFile() {
        fetch('https://example.com/sample.pdf')
            .then(response => {
                if (!response.ok) {
                    throw new Error('网络响应不正常');
                }
                return response.blob();
            })
            .then(blob => {
                const url = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'sample.pdf';
                document.body.appendChild(a);
                a.click();
                
                // 清理
                window.URL.revokeObjectURL(url);
                document.body.removeChild(a);
            })
            .catch(error => {
                console.error('下载失败:', error);
            });
    }
    </script>
    

使用 XMLHttpRequest + Blob 下载

  1. 这是传统的 AJAX 方式,与 Fetch 类似但使用回调而非 Promise

  2. 优点:

    1. 兼容性比 Fetch 更好 (支持更旧的浏览器)
    2. 可以设置请求头;
  3. 缺点:

    1. 基于回调的 API 不如 Promise 友好;
    2. 代码相对冗长;
  4. 示例代码:

    <!DOCTYPE html>
    <html>
    <head>
        <title>XHR下载示例</title>
    </head>
    <body>
        <button onclick="downloadWithXHR()">使用XHR下载</button>
        
        <script>
            function downloadWithXHR() {
                const xhr = new XMLHttpRequest();
                xhr.open('GET', 'https://example.com/api/download', true);
                xhr.setRequestHeader('Authorization', 'Bearer your-token-here');
                xhr.responseType = 'blob';
                
                xhr.onload = function() {
                    if (this.status === 200) {
                        const blob = this.response;
                        const url = window.URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = url;
                        a.download = 'xhr-download.pdf';
                        document.body.appendChild(a);
                        a.click();
                        window.URL.revokeObjectURL(url);
                        document.body.removeChild(a);
                    } else {
                        console.error('下载失败');
                    }
                };
                
                xhr.onerror = function() {
                    console.error('请求错误');
                };
                
                xhr.send();
            }
        </script>
    </body>
    </html>
    

下载 Base64 数据或文本

  1. 适用于需要下载非二进制数据 (如文本、图片 Base64 等)

  2. 优点:

    1. 可以直接下载生成的内容;
    2. 不需要服务器支持;
  3. 缺点:

    1. 不适合大文件;
    2. Base64 编码会增加约 33% 的体积;
  4. 示例代码:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Base64下载示例</title>
    </head>
    <body>
        <button onclick="downloadBase64()">下载Base64图片</button>
        <button onclick="downloadText()">下载文本文件</button>
        
        <script>
            function downloadBase64() {
                const base64Data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...';
                const link = document.createElement('a');
                link.href = base64Data;
                link.download = 'image.png';
                link.click();
            }
            
            function downloadText() {
                const content = '这是一段要下载的文本内容';
                const blob = new Blob([content], { type: 'text/plain' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'example.txt';
                a.click();
                URL.revokeObjectURL(url);
            }
        </script>
    </body>
    </html>
    

下载前端生成的内容

  1. 适用于 PWA 应用或需要离线访问的场景;

  2. 特点:

    1. 适合在前端生成内容后下载;
    2. 可以下载各种类型的数据 (文本、JSON、CSV 等)
    3. 不需要服务器参与;
  3. 示例代码:

    <button onclick="downloadText()">下载文本文件</button>
    <button onclick="downloadJSON()">下载JSON文件</button>
    
    <script>
    function downloadText() {
        const content = '这是一段示例文本\n第二行内容';
        const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'example.txt';
        a.click();
        URL.revokeObjectURL(url);
    }
    
    function downloadJSON() {
        const data = {
            name: '张三',
            age: 30,
            hobbies: ['阅读', '游泳', '编程']
        };
        const content = JSON.stringify(data, null, 2);
        const blob = new Blob([content], { type: 'application/json;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'data.json';
        a.click();
        URL.revokeObjectURL(url);
    }
    </script>
    

使用 Web Workers 进行后台下载

  1. 对于非常大的文件或需要复杂处理的下载,可以使用 Web Workers 在后台线程中进行,避免阻塞 UI 线程;

  2. 优点:

    1. 不阻塞 UI 线程;
    2. 可以报告下载进度;
    3. 适合大文件下载;
  3. 缺点:

    1. 实现复杂;
    2. 需要额外的 worker 文件;
  4. 示例代码:

    // worker.js
    self.onmessage = function(e) {
        const { url, filename } = e.data;
        
        fetch(url)
            .then(response => response.blob())
            .then(blob => {
                self.postMessage({
                    status: 'completed',
                    blob,
                    filename
                });
            })
            .catch(error => {
                self.postMessage({
                    status: 'error',
                    error: error.message
                });
            });
    };
    
    // 主线程
    const worker = new Worker('worker.js');
    worker.onmessage = function(e) {
        if (e.data.status === 'completed') {
            const url = URL.createObjectURL(e.data.blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = e.data.filename;
            a.click();
            URL.revokeObjectURL(url);
        } else {
            console.error('下载失败:', e.data.error);
        }
    };
    
    worker.postMessage({
        url: 'https://example.com/large-file.zip',
        filename: 'large-file.zip'
    });
    

分片并发下载文件

  1. 下面是一个实现分片并发下载文件并拼装的前端代码方案;这个方案假设后端支持分片下载,并且可以通过请求头或 URL 参数指定分片信息;

  2. 实现思路:

    1. 首先获取文件总大小和分片信息;
    2. 根据分片大小计算需要分成多少片;
    3. 并发请求所有分片;
    4. 按照分片顺序拼装数据;
    5. 触发浏览器下载完整文件;
  3. 关键点说明:

    1. HEAD 请求:首先发送 HEAD 请求获取文件大小,确定需要分成多少片;
    2. Range 请求:使用 HTTP Range 头请求 特定字节范围的数据;
    3. 并发控制:使用 concurrentMap 函数控制并发请求数量,避免浏览器限制;
    4. 数据拼装:按照分片索引顺序将 ArrayBuffer 数据合并;
    5. 下载触发:使用 BlobObject URL 创建下载链接并触发点击;
  4. 优化建议:

    1. 断点续传:可以本地存储已下载的分片信息,实现断点续传;
    2. 进度显示:添加进度回调函数,显示下载进度;
    3. 错误重试:为每个分片添加重试机制;
    4. 内存优化:对于超大文件,可以考虑流式处理而不是全部加载到内存;
  5. 注意事项:

    1. 后端必须支持 Range 请求
    2. 跨域请求需要正确配置 CORS
    3. 对于超大文件,内存消耗可能较大,可以考虑使用 Streams API 处理;
  6. 示例代码:

    /**
    * 分片并发下载文件
    * @param {string} url 文件下载地址
    * @param {object} options 配置选项
    * @param {number} [options.chunkSize=1*1024*1024] 分片大小,默认1MB
    * @param {number} [options.maxConcurrent=5] 最大并发数
    * @param {object} [options.headers={}] 自定义请求头
    * @param {string} [options.filename] 下载文件名,如果不提供则尝试从响应头获取
    */
    async function downloadFileWithChunks(url, options = {}) {
        const {
            chunkSize = 1 * 1024 * 1024, // 默认分片大小1MB
            maxConcurrent = 5, // 最大并发数
            headers = {},
            filename
        } = options;
    
        // 1. 首先发送HEAD请求获取文件总大小
        const headResponse = await fetch(url, {
            method: 'HEAD',
            headers
        });
        
        if (!headResponse.ok) {
            throw new Error(`Failed to get file info: ${headResponse.status} ${headResponse.statusText}`);
        }
        
        const contentLength = headResponse.headers.get('Content-Length');
        if (!contentLength) {
            throw new Error('Content-Length header is missing');
        }
        
        const totalSize = parseInt(contentLength, 10);
        const chunkCount = Math.ceil(totalSize / chunkSize);
        
        console.log(`File size: ${totalSize} bytes, will be downloaded in ${chunkCount} chunks`);
        
        // 2. 创建所有分片的请求
        const chunkPromises = [];
        const chunkRanges = [];
        
        for (let i = 0; i < chunkCount; i++) {
            const start = i * chunkSize;
            const end = Math.min(start + chunkSize - 1, totalSize - 1);
            chunkRanges.push({ start, end, index: i });
        }
        
        // 3. 使用并发控制下载所有分片
        const chunks = await concurrentMap(
            chunkRanges,
            async ({ start, end, index }) => {
                console.log(`Downloading chunk ${index + 1}/${chunkCount} [${start}-${end}]`);
                
                const response = await fetch(url, {
                    headers: {
                        ...headers,
                        'Range': `bytes=${start}-${end}`
                    }
                });
                
                if (!response.ok) {
                    throw new Error(`Failed to download chunk ${index}: ${response.status} ${response.statusText}`);
                }
                
                return {
                    index,
                    data: await response.arrayBuffer()
                };
            },
            maxConcurrent
        );
        
        // 4. 按照分片顺序拼装数据
        console.log('All chunks downloaded, assembling...');
        
        // 按索引排序
        chunks.sort((a, b) => a.index - b.index);
        
        // 计算总大小并创建缓冲区
        const totalBytes = chunks.reduce((sum, chunk) => sum + chunk.data.byteLength, 0);
        const buffer = new Uint8Array(totalBytes);
        
        // 合并所有分片数据
        let offset = 0;
        for (const chunk of chunks) {
            buffer.set(new Uint8Array(chunk.data), offset);
            offset += chunk.data.byteLength;
        }
        
        console.log('File assembled, creating download link');
        
        // 5. 创建下载链接
        const blob = new Blob([buffer]);
        const downloadUrl = URL.createObjectURL(blob);
        
        const a = document.createElement('a');
        a.href = downloadUrl;
        a.download = filename || getFilenameFromHeaders(headResponse.headers) || 'download';
        document.body.appendChild(a);
        a.click();
        
        // 清理
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(downloadUrl);
        }, 100);
    }
    
    /**
    * 并发控制执行异步任务
    * @param {Array} items 要处理的数组
    * @param {Function} mapper 异步处理函数
    * @param {number} concurrency 并发数
    * @returns {Promise<Array>} 处理结果数组
    */
    async function concurrentMap(items, mapper, concurrency = 5) {
        const results = [];
        const activePromises = new Set();
        
        for (const [index, item] of items.entries()) {
            // 等待有空闲的并发槽
            while (activePromises.size >= concurrency) {
                await Promise.race(activePromises);
            }
            
            const promise = mapper(item, index).then(result => {
                activePromises.delete(promise);
                return result;
            });
            
            activePromises.add(promise);
            results.push(promise);
        }
        
        return Promise.all(results);
    }
    
    /**
    * 从响应头中提取文件名
    * @param {Headers} headers 响应头
    * @returns {string|null} 文件名
    */
    function getFilenameFromHeaders(headers) {
        const contentDisposition = headers.get('Content-Disposition');
        if (!contentDisposition) return null;
        
        const filenameMatch = contentDisposition.match(/filename\*?=["']?(?:UTF-\d["']*)?([^;"']*)["']?;?/i);
        if (filenameMatch && filenameMatch[1]) {
            return decodeURIComponent(filenameMatch[1]);
        }
        
        return null;
    }
    
    // 使用示例
    downloadFileWithChunks('https://example.com/large-file.zip', {
        chunkSize: 2 * 1024 * 1024, // 2MB分片
        maxConcurrent: 3, // 最大3个并发请求
        filename: 'large-file.zip',
        headers: {
            'Authorization': 'Bearer your-token'
        }
    }).then(() => {
        console.log('File downloaded successfully');
    }).catch(error => {
        console.error('Download failed:', error);
    });
    

断点续传

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 使用 a 标签下载
  2. 2. 使用 window.open 或 location.href
  3. 3. 使用 Fetch API + Blob 下载
  4. 4. 使用 XMLHttpRequest + Blob 下载
  5. 5. 下载 Base64 数据或文本
  6. 6. 下载前端生成的内容
  7. 7. 使用 Web Workers 进行后台下载
  8. 8. 分片并发下载文件
  9. 9. 断点续传