zl程序教程

您现在的位置是:首页 >  其他

当前栏目

大文件分片上传 轻松拿捏

2023-03-14 22:54:41 时间

大文件上传前言

为了方便大家阅读和理解,我将以单个大文件上传为例,先简单描述下思路。 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,进度条回退的情况....