前端文件下载

1,256 阅读3分钟

1.1 a 标签下载

只要在浏览器能通过 url 访问到文件,那就能通过 a 标签下载,不需要任何服务器配置

问题是:图片能直接下载,但如果是 pdf,浏览器默认会打开 pdf,而不是下载(即使设置 download)

怎么解决这个问题?

一、使用 blob 流强制下载

  • 优点:可以自定义文件名
  • 缺点:需要服务器配置跨域('Access-Control-Allow-Origin')

二、后端响应头指定文件类型 Content-Type 和文件名 Content-Disposition

/**
 * 通用文件下载(可自定义文件名,需要服务器支持跨域)
 * 注意:大文件blob转换可能内存溢出
 * @param {string} url 文件地址
 * @param {string} fileName 自定义文件名
 * @param {boolean} supportCORS 服务器是否支持跨域
 */
function downloadFile(url, fileName, supportCORS = true) {
    if (supportCORS) {
        // 保证强制下载,可自定义文件名
        let x = new window.XMLHttpRequest();
        let timestamp = `t=${new Date().getTime()}`;
        let separator = url.includes('?') ? '&' : '?';
        x.open('GET', `${url}${separator}${timestamp}`);
        x.responseType = 'blob';
        x.onload = () => {
            let url = window.URL.createObjectURL(x.response);
            let a = document.createElement('a');
            a.href = url;
            a.download = fileName;
            a.click();
            a.remove();
            window.URL.revokeObjectURL(url);
        };
        x.send();
    } else {
        // 如果是pdf,浏览器默认会打开而不是下载
        let a = document.createElement('a');
        a.href = url;
        a.download = fileName;
        a.target = '_blank';
        a.click();
        a.remove();
    }
}

1.2 下载库

常用:FileSaver.js、StreamSaver.js

两者都用于文件下载,主要区别在于,是否将整个文件加载到内存中

FileSaver.js 原理:

  1. 将文件数据封装成为 Blob 对象
  2. 然后利用 URL.createObjectURL(blob) 创建临时文件链接
  3. 通过 a 标签的 download 属性触发浏览器的下载行为

StreamSaver.js 原理(比较复杂)

  1. 注册 Service Worker(SW)
  2. fetch 发起带自定义 headers 的流式请求(Accept: application/octet-stream)
  3. SW 拦截请求并伪造响应头(Content-Disposition: attachment,Content-Type: application/octet-stream),强制激活浏览器下载
  4. 最后利用 WritableStream API 将返回的文件流(ReadableStream)直接写入文件系统

1.3 纯前端批量下载方案

方案一:使用 a 标签循环下载

  • 优点:实现最简单
  • 缺点:需要用户点击允许下载多文件、反复弹窗用户体验差

方案二:jszip + FileSaver.js,先压缩再下载

  • 优点:比方案一用户体验好

  • 缺点:

    1. 压缩需要时间,等压缩完才会调出浏览器下载弹窗 》可以增加一个进度条
    2. FileSaver 下载文件总大小不能超过 2GB

方案二:StreamSaver.js + zip-stream.js,先压缩再流式下载

  • 优点:将下载逻辑移至 Web Worker,避免阻塞主线程,并且会直接调出浏览器下载弹窗
  • 缺点:zip-stream 打包文件总大小不能超过 4GB

综上,纯前端实现批量下载,文件总大小不能超过 4GB

如果需要下载更大的文件,必须后端先把文件下载到服务器,然后把文件拆分成小块,再由前端下载

handleBatchDownload: () => {
    // 获取所有选中文件
    const selectArr = ownFileList.value.filter((item) => orderAttachment.selectedRowKeys.includes(item.id))

    // 如果所有选中文件的文件大小之和超过4GB,则提示用户
    const totalSize = selectArr.reduce((acc, cur) => acc + cur.size, 0)
    if (totalSize > 4 * 1024 * 1024 * 1024) {
        Modal.error({
            title: '附件过大',
            content: '文件总大小超过4G,无法批量下载,请分批下载',
            centered: true,
            zIndex: 9999,
        })
        return
    }

    // 注意:无法生成超过 4GB 的 zip 文件
    const readableZipStream = new window.ZIP({
        async pull(ctrl) {
            for (const item of selectArr) {
                try {
                    const res = await fetch(item.url)
                    if (!res.ok) {
                        console.error(`fetch error: ${item.url}`)
                        continue
                    }
                    const stream = () => res.body
                    const name = item.name.split('/').pop()
                    ctrl.enqueue({ name, stream })
                } catch (error) {
                    console.error(`unknown error: ${item.url}`, error)
                    continue
                }
            }
            ctrl.close()
        },
    })

    const fileStream = streamSaver.createWriteStream('附件汇总.zip')

    if (window.WritableStream && readableZipStream.pipeTo) {
        readableZipStream
            .pipeTo(fileStream)
            .then(() => {
                console.log('下载成功')
            })
            .catch((error) => {
                console.error('下载失败', error)
            })
    } else {
        // 兼容旧浏览器
        const writer = fileStream.getWriter()
        const reader = readableZipStream.getReader()
        const pump = () =>
            reader.read().then((res) => (res.done ? writer.close() : writer.write(res.value).then(pump)))
        pump()
    }
},

1.4 最后

如果帮到你了可以点个赞。

2025/06/07:发布文章