大文件分片上传 轻松拿捏

18,772 阅读5分钟

      日子一天天的过去,帮她带早餐成为了一天中最快乐的事情了,我和她关系也近了许多,交谈起来也不会再面红耳赤了。一天中午她突然发消息说为了感谢我每天帮她带早餐要请我吃午饭,当时心情只能说开心到炸裂……她带我来到了食堂的二楼吃麻辣香锅,和她走在一起,路人会时不时的瞥向我们,我不知道是因为她的漂亮,还是好奇她居然和我这么一个人走在一起,我很不喜欢这种目光。
      那是我第一次吃麻辣香锅,也是第一次和她在食堂吃饭,感觉麻辣香锅应该是大学最好吃的东西了,就是价格贵了点,我只吃了一点点,好的东西都要留给她,回到寝室后,室友纷纷调侃了我,我也天真的以为我爱情正在路上了,于是我又准备在她生日的表白了,这次我不会怂了……
      终于等到了她生日的那天,我早早的写好了对她的祝福,十二点一过就发给她了,可等了好久都没等到她的回复,我想她应该是睡着了.....本来计划下午把蛋糕送给她,然后约她一起吃个晚饭,联系她的时候,她早已和朋友出去庆生了,独留我对着蛋糕发呆,这种等待是非常煎熬的事情,只能打打英雄联盟来让时间过得快点,5杀我也提不起半点开心....
      她说她快到了,我赶紧放下手上的游戏去她楼下等她,等了好一会才看到她,我把蛋糕小心翼翼的递给她,说了一堆祝福的话,在她快上楼的时候,我终于鼓足了勇气说了,我喜欢你,你能做我女朋友吗?

大文件上传前言

为了方便大家阅读和理解,我将以单个大文件上传为例,先简单描述下思路。
antd的上传组件有一个上传前的钩子,里面是可以拿到file信息,上传前将file切片,然后包装成一个一个的请求,放到一个数组,上传的的时候将数组的请求执行就可以了,执行完后发送一个合并请求,我没有用Promise.all去执行,而是2个2个的递归执行。

对大文件先通过slice进行切片

核心是利用 Blob.prototype.slice 方法

createFileChunk接收两个参数
dataSource:所上传的File大文件,size:每个分片大小

  //切片
     createFileChunk = (dataSource, size = 5 * 1024 * 1024) => {
        const fileChunkList = [];//因为只有一个文件,数组只有1项
        let cur = 0;
        let index = 0;//每个分片给一个索引,最后后端合并按序合并分片
        let obj: IFileChunksList = {
            name:dataSource.name,
            progressArr: [], //记录每一个分片的上传进度
            errChunkFile: [],//上传失败的文件
            keys: [],//将每个分片包装成一个http请求
        };

        let arr = [];
        while (cur < dataSource.size) {
            arr.push(this.createHttp({ hash:dataSource.name+'_'+index, file: dataSource.slice(cur, cur + size)}));
            index += 1;
            cur += size;
        }
        obj.keys = arr;
        fileChunkList.push(obj);
        this.setState({fileChunkList})
    };

hash由文件名和序号组成,后端合并的时候需要按顺序合并。

this.createHttp方法分析
简单的做了参数处理,this.request里面才是真是ajax请求
onProgress:监听ajax进度并实时记录下来

   createHttp = (data) => {
        const { hash, file } = data;
        const formData = new FormData();
        formData.append('chunk', file);
        formData.append('hash', hash);
        return () =>
            this.request({
                url: this.props.action,
                data: formData,
                onProgress: this.createProgressHandler(data, hash),
            });
    };
为每个分片创建一个http请求

this.request 方法通过promise和ajax包装
url:分片上传接口。data:分片参数。onProgress:监听此分片上传进度。
requestList:所有正在上传的分片请求集合。(断点续传用的)
也可以在此方法里面设置token认证

   //创建ajax
    request = ({
        url, 
        method = 'post',
        data,  
        onProgress = (e: any) => e,
        requestList = this.state.requestList,
    }: IAjax) => {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.upload.onprogress = onProgress;
            xhr.open(method, url, true);
            xhr.setRequestHeader('Authorization', this.props.token);
            xhr.send(data);
            xhr.onload = (e: any) => {
                // 将请求成功的 xhr 从列表中删除
                const { requestList } = this.state;
                if (requestList) {
                    const xhrIndex = requestList.findIndex((item) => item.xhr === xhr);
                    requestList.splice(xhrIndex, 1);
                    this.setState({ requestList });
                }
                resolve({
                    data: e.target.response,
                });
            };
            xhr.onerror = (e) => {
                reject(e);
                // throw new Error('fail');
            };
            requestList.push({ xhr, hash:data.get('hash')});

            this.setState({
                requestList,
            });
        });
    };
记录分片进度方法

this.createProgressHandler(data, hash)在上面createHttp方法调用

  createProgressHandler = (item1, hash) => {
        return (e: any) => {
            let { fileChunkList } = this.state;
            let index=hash.split("_")[1]
            fileChunkList[0].progressArr[index]=e.load
            this.setState({
                fileChunkList,
            });
        };
    };
调用开始上传的方法

this.upFile(this,state.fileChunkList[0])(true)
参数true是保证2个请求一发

 // 开始上传
    upFile = ( item) => {
        let fileArr=item.keys
        let init = 0;
        let loopFun = (initValue) => {
            fileArr[initValue]()
                .then((res) => {
                    if (JSON.parse(res.data).statusCode === 200) {
                        init++;
                        if (init < fileArr.length) {
                         //继续传下一个分片
                            loop();
                        } else if (init === fileArr.length && !item.errChunk.length && fileArr.length !== 1) {
                        //分片传完,合并分片
                            this.mergeChunk(item);
                        }
                    } 
                })
                .catch((err) => {
                //捕获上传失败的分片存起来
                    let arrChunk = item.errChunkFile.concat(fileArr[initValue]);
                    init++;
                    item.errChunkFile = arrChunk;
                    this.setState({
                        fileChunkList: [...item],
                    });
                    if (init < fileArr.length) {
                        loop();
                    }
                });
        };
        let loop = (initFlag) => {
            loopFun(init);
            if (initFlag) {
                loopFun(++init);
            }
        };
        return loop;
    };

合并分片的方法我就不写了,就调用一个接口即可。
假如存在上传失败的分片,会被记录在fileChunkList[0].errChunkFile.对这个失败的数组做一个上传就可以了。

断点续传 暂停

this.state.requestList是当前正在请求的分片集合。暂停就是把请求abort,

upFileCancel = (itemCurrent: IFileChunksList) => {
        this.state.requestList.forEach((item) => {
                item.xhr.abort();
        });
    };

续传,可以获取已经上传成功的,然后把未上传的重新上传即可。

总结

我只写了前端的大致实现思想,后端只需提供单个分片上传的接口,合并分片的接口。我的hash用文件名+索引,用spark-md5对文件内容生成一个hash才是最合适的。

单个大文件上传感觉其实并不复杂,知道它的大致思想再去扩展多文件排队上传,断点续传,记录每个文件的进度条、总进度条甚至每个分片的进度条,还要考虑暂停的时候,由于onProgress是实时监听进度条的,当分片上传了百分之80,取消后变为0,进度条回退的情况....

大家对大文件有疑问的,可以评论,最后麻烦点个赞。搬砖不易

看故事学习es6原理的:juejin.cn/post/702429…