功能实现系列-超大文件下载(超过1GB)(不用分片下载)(避免页面崩溃情况)

3,067 阅读4分钟

谷歌浏览器支持数据存储最大2GB。

文件下载

在我们实现文件下载功能时,你知道其中是怎么样的流程吗?

首先,我们先发送请求,将下载所需的参数组合,通过http请求发送到后端,此时f12中可以看到请求是处于发送中。

然后后端接收到请求,开始将要下载的内容往服务器硬盘里面存,这段时间,前端发送的请求都是在等待,没有什么响应,也接收不到下载进度(因为这时候后端还在准备数据,将数据打包成zip包,还未将数据返给前端)。

当后端将数据都放到服务器硬盘后,并打包好,开始和前端接口建立文件流数据传输通道,这时候请求是可以拿到资源传输整体大小,我们也可以在这个时候实现加载进度条。

当所有数据都返回给前端时,我们可以在f12中的这个请求中的响应中看到返回的数据。这之前都是空白。

超大文件下载

下载超大文件,我们不应该使用new Blob这种将数据先存储到内存,然后转下载资源再进行下载。因为当资源很大时,这些资源都是先存储到内存的,浏览器分配到的内存是有限的,过多的占用内存会导致浏览器卡顿、崩溃。

而使用window.open(url)打开一个路径,其实就是发送请求,但是让浏览器帮我们处理数据,则不会出现这种情况。

下面举个例子:

new Blob这种先存内存的方式(适用于小文件):

const config = {
    headers: {
        'Content-Type': 'application/json'
    },
    withCredentials: true,
    responseType: 'arraybuffer'
};
const params = {
    id_kind: '',
    id_no: '',
    query_flag: ''
};
const res = await axios.post(downloadUrl, params, config);
if (res.code === '200') {
    try {
        // 处理接口请求成功但是下载错误返回json格式的情况
        //如果JSON.parse(enc.decode(new Uint8Array(res.data)))不报错,说明后台返回的是json对象,则弹框提示
        //如果JSON.parse(enc.decode(new Uint8Array(res.data)))报错,说明返回的是文件流,进入catch,下载文件
        let enc = new TextDecoder('utf-8')
        let data = JSON.parse(enc.decode(new Uint8Array(res.data))) //转化成json对象
        if (data.message) {
            // 下载错误
            setTimeout(this.msgList[msgIndex], 0)
            return this.$hMessage.error(data.message)
        }
    } catch(err) {
        // 下载成功:
        // 处理参数:接口返回参处理、下载格式、文件名
        let blob = new Blob([res.data], {
            type: res.headers["content-type"]
        });
        const disposition = decodeURI(res.headers['content-disposition'])
        const match = disposition ? disposition.match(/attachment; filename="(.+)"/i) : null;
        const filename = match ? match[1] : 'unknowfile'
        
        // 下载
        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // 兼容IE,window.navigator.msSaveBlob:以本地方式保存文件
            window.navigator.msSaveBlob(blob, filename);
            this.$hMessage.success('下载成功')
        } else {
            // 普通浏览器
            let url = window.URL.createObjectURL(blob);
            if (url) {
                let link = document.createElement('a');
                link.style.display = 'none';
                link.href = url;
                link.setAttribute('download', filename);
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link); //下载完成移除元素
                window.URL.revokeObjectURL(url); //释放掉blob对象
                this.$hMessage.success('下载成功')
            }
        }
    }
} else {
    this.$hMessage.success('下载失败')
}
​

window.open这种交给浏览器处理的方式:

import { Base64 } from './js-base64.js';
​
const params = {
    id_kind: '',
    id_no: '',
    query_flag: ''
};
const base64Str = Base64.encodeURI(JSON.stringify(params)); // 将查询参数转为base64,如果不会很长的话就不用
const url = `${downloadUrl}?base64Str=${base64Str}`;
const openWin = window.open(url);
​
// 简单实现监听下载完毕
const timer = setInterval(_ => {
    if (openWin.closed) {
        console.log('下载成功'); // 资源加载完毕,此时可选择下载存放目录或者取消下载,选择后新打开的页签自动关闭
        clearInterval(timer);
        timer = null;
    }
}, 1000);

window.open()虽然能解决浏览器下载超大文件的问题,但是新打开一个页签,并且页签内空白内容,需要等后端建立数据传输时才会关闭,假设这个数据传输建立得很慢(建立之前需要将数据进行打包),那么就会有长时间的等待。体验上不是很好。

所以还有一种方式:ReadableStream。

ReadableStream这种交给浏览器处理的方式:

ReadableStream的文档说明:developer.mozilla.org/zh-CN/docs/…

fetch的文档说明:developer.mozilla.org/zh-CN/docs/…

export default {
    methods: {
        urlLOcalizecBlob(url) {
            const localBlob = null, filename = 'unknowfile';
            // fetch是es6的请求语法,返回一个promise
            fetch(url, {
                method: 'GET',
                mode: 'cors',
                credentials: 'include',
                headers: {
                    Accept: '*/*'
                }
            }).then(response => {
                // 使用fetch时,如果要从响应头中获取数据,需要通过get方法
                
                // 返回报错
                if (response.header.get('content-type').includes('application/json')) {
                    // 处理如果返回的是json格式,那说明请求报错,需要将报错信息展示
                    // response.json()将信息json化,返回一个Promise,还有其他很多方法,可以在mdn搜fetch
                    return response.json().then(err => {
                        // throw new Error()可以将错误抛出,直接进入到catch,另外括号里放String
                        // err.message是后端返回的报错信息文字
                        throw new Error(err.message)
                    })
                }
                
                // 返回文件流
                const contentDisposition = response.headers.get('content-disposition');
                const disposition = decodeURI(contentDisposition);
                const match =  disposition ? disposition.match(/attachment; filename="(.+)"/i) : null;
                filename = match ? match[1] : 'unknowfile';
                const reader = response.body.getReader();
                return new ReadableStream({
                    start(controller) {
                        return pump();
                        function pump() {
                            return reader.read().then(({done, value}) => {
                                if (done) {
                                    controller.close();
                                    return;
                                }
                                controller.enqueue(value);
                                return pump()
                            })
                        }
                    }
                })
            }).then(stream => new Response(stream)).then(response => response.blob()).then(blob => {
                this.fileDownload(filename, blob)
            }).catch(err => {
                this.$hNotice.error(err?.message || '下载失败');
            })
        }
    },
    fileDownload(filename, blob) {
        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // ie浏览器
            window.navigator.msSaveBlob(blob, filename);
            this.$hMessage.success('下载成功')
        } else {
            // 其他浏览器
            let url = window.URL.createObjectURL(blob);
            if (url) {
                let link = document.createElement('a');
                link.style.display = 'none';
                link.href = url;
                link.setAttrbute('download', filename);
                document.body.appendChild(link);
                link.click();
                // 释放内存,释放blob对象
                document.body.removeChild(link);
                window.URL.revokeObjectURL(url);
                link = null;
                this.$hMessage.success('下载成功')
            }
        }
    }
}

如果get请求参数太长,或者说url拼接太长,可以考虑js-base,将之转为base64。