javascript读取文件_文件上传带进度条进阶-断点续传

说明
1. 把文件按大小1M分割成N份
2. 每次上传时,告诉后台大文件的md5、当前第几份(从0开始)、总共几份
3. 并行上传,前端同时开启5个请求进行传输增加速度
4. 上传失败或出错后,继续上传下一份,把出错的份放在队尾,如果一直出错则中断请求防止死循环
5. 后台接受文件后通过md5进行比对,上次是否接受过此文件,如果接受则跳过,最后进行文件合并出来
6. 前端代码如下...
7. 查看源代码请点击在线演示地址

先上html

<input id="fileInput" type="file" multiple="multiple" name="" />

<ul id="box">
    <!-- <li>
        <span>文件名</span>
        <span>文件类型</span>
        <span>文件大小</span>
        <span>上传进度</span>
        <span>总进度</span>
        <span>操作</span>
    </li>
    <li>
        <span></span>
        <span></span>
        <span></span>
        <span>
            <i><em></em></i>
            <i><em></em></i>
            <i><em></em></i>
            <i><em></em></i>
            <i><em></em></i>
        </span>
        <span>
            <i><em></em></i>
        </span>
        <span>
            <a href="javascript:;">上传</a>
            <a href="javascript:;">暂停</a>
            <a href="javascript:;">删除</a>
        </span>
    </li> -->
</ul>

选择input时把文件信息写在页面上,因为涉及到断点续传,为保证文件的唯一性,需要本地读取文件并对其进行md5

fileInput.addEventListener('change', function() {
    var files = this.files;
    if (files.length) {
        let str = '<li><span>文件名</span><span>文件类型</span><span>文件大小</span><span>上传进度</span><span>总进度</span><span>操作</span></li>';
        for (var i = 0; i < files.length; i++) {
            var file = files[i];
            str += `<li>
                <span>${file.name}</span>
                <span>${file.type}</span>
                <span>${formatByte(file.size)}</span>
                <span>
                    <i><em></em></i>
                    <i><em></em></i>
                    <i><em></em></i>
                    <i><em></em></i>
                    <i><em></em></i>
                </span>
                <span>
                    <i><b>文件读取中</b></i>
                    <i><em></em></i>
                </span>
                <span data-index="${i}">
                    <a data-control="1" href="javascript:;">上传</a>
                    <a data-control="2" href="javascript:;">暂停</a>
                    <a data-control="3" href="javascript:;">删除</a>
                </span>
            </li>`;
        }
        box.innerHTML = str;
        readFilesStep(0); // 文件太大,同步读取
    } else {
        box.innerHTML = ''
    }
}, false);

同步读取文件操作

function readFilesStep(i) {
    var files = fileInput.files;
    if (!files[i]) {return}
    var oLi = box.children[i + 1];
    oLi.dataset.count = Math.ceil(files[i].size / SIZE); // 总共多少份
    var readProgress = oLi.children[4].children[0].children[0];
    var reader = new FileReader();
    reader.readAsDataURL(files[i]);
    reader.onload = function () {
        oLi.dataset.md5 = md5(this.result);
        readProgress.innerHTML = '文件读取完毕';
        readProgress.parentNode.className = 'stop hide';
        readFilesStep(i + 1);
        reader = null;
    }
    reader.onerror = function (e) {
        console.error(e);
        readProgress.innerHTML = '文件读取失败,请重新选择';
        readFilesStep(i + 1);
        reader = null;
    }
}

因为li是创建出现的,对box进行事件委托

box.addEventListener('click', function (ev) {
    var target = ev.target;
    var control = target.dataset.control;
    var index = Number(target.parentNode.dataset.index);
    if (control === "1") {
        // 上传
        uploadItem(index);
    } else if (control === "2") {
        // 暂停
        pauseItem(index);
    } else if (control === "3") {
        // 删除
        delItem(index);
    }
}, false);

上传代码如下

var SIZE = 1024 * 1024; // 切片大小 
var FETCH_NUM = 5; // 上传文件同时发起请求数
var FETCH_MAP = {}; // 上传请求句柄,取消请求用
var FETCH_POOL = {}; // 每一个上传文件的份数

function uploadItem(index) {
    var file = fileInput.files[index];
    var oLi = box.children[index + 1];
    var isPlaying = Number(oLi.dataset.playing) || 0;
    if (isPlaying) { return }
    oLi.dataset.playing = 1;
    var fileMd5 = oLi.dataset.md5;
    var count = oLi.dataset.count;
    var lastLoaded = Number(oLi.dataset.lastLoaded || 0);
    if (!fileMd5) {
        alert('请等待文件读取')
    } else {
        var maxErrorTimes = 10; // 最大出错次数

        // 第一次点开始会进行创建
        if (!FETCH_POOL[fileMd5]) {
            FETCH_POOL[fileMd5] = [];
            for (var i = 0; i < count; i++) {
                FETCH_POOL[fileMd5][i] = i;
            }
        }

        FETCH_MAP[fileMd5] = [];
        for (var i = 0; i < FETCH_NUM; i++) {
            FETCH_MAP[fileMd5][i] = null;
            step(i)
        }
        function step(i) {
            var cur = FETCH_POOL[fileMd5].shift();
            if (cur !== undefined) {
                FETCH_MAP[fileMd5][i] = uploadStep({
                    file: file, cur: cur, count: count, md5: fileMd5,
                    progressCb: function(e) {
                        var loaded = lastLoaded + e.loaded;
                        setProgress(loaded);
                        var progressItem = (e.loaded / e.total * 100).toFixed(2) + '%';
                        var oProgressItem = oLi.children[3].children[i];
                        oProgressItem.title = progressItem;
                        oProgressItem.children[0].style.width = progressItem;
                        if (progressItem === '100.00%') {
                            oProgressItem.className = 'stop'
                        } else {
                            oProgressItem.className = ''
                        }
                    }, successCb: function(){
                        lastLoaded += SIZE;
                        setProgress(lastLoaded, true);
                        step(i);
                    }, errorCb: function(status) {
                        // 失败把当前份放在末尾,继续下一步
                        FETCH_POOL[fileMd5].push(cur);
                        if (status === 0) {
                            // 手动取消 暂停
                        } else if (maxErrorTimes--) {
                            // 出错10次后不再上传,防止进入死循环
                            step(i);
                        }
                    }
                });
            }
        }

        function setProgress(loaded, isFinished) {
            var oProgress = oLi.children[4].children[1];
            // 实际上传的数据大小 > 文件大小,此处做修正处理
            if (loaded > file.size) {
                if (isFinished) {
                    loaded = file.size;
                    oProgress.className = 'stop';
                } else {
                    loaded = file.size * 0.9999;
                }
            }
            oLi.children[2].innerHTML = formatByte(loaded) + '/' + formatByte(file.size);

            var progress = (loaded / file.size * 100).toFixed(2) + '%';
            var lastProgress = oProgress.title || '0%';
            // 并行上传 此处可能是线路1的进度和线路2的进度比较,优先显示最大值
            progress = parseFloat(lastProgress) < parseFloat(progress) ? progress : lastProgress;
            // 总进度条
            oProgress.title = progress;
            oProgress.children[0].style.width = progress;
            if (isFinished) {
                oLi.dataset.lastLoaded = loaded;
            }
        }
    }
}

分步上传代码

function uploadStep(obj) {
    var file = obj.file;
    var cur = obj.cur;
    var fileMd5 = obj.md5;
    var count = obj.count;
    var progressCb = obj.progressCb;
    var successCb = obj.successCb;
    var errorCb = obj.errorCb;

    var params = new FormData();
    var filename = file.name;
    var fileChunk = file.slice(SIZE * cur, SIZE * (cur + 1));
    params.append('md5', fileMd5);
    params.append('file', fileChunk);
    params.append('cur', cur);
    params.append('count', count);
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                successCb && successCb(JSON.parse(xhr.responseText))
            } else {
                errorCb && errorCb(xhr.status)
            }
        }
    }
    xhr.upload.onprogress = function (e) {
        progressCb && progressCb(e)
    }
    xhr.open('POST', '/api/upload', true);
    xhr.send(params);
    return xhr;
}

function formatByte(b) {
    var kb = b / 1024;
    if (kb >= 1024) {
        var m = kb / 1024;
        if (m >= 1024) {
            var g = m / 1024;
            return g.toFixed(2) + 'G';
        } else {
            return m.toFixed(2) + 'M';
        }
    } else {
        return kb.toFixed(2) + 'K';
    }
}

点击暂停时,取消上个建立的XMLHttpRequest请求,这里用FETCH_MAP[md5]进行标记

function pauseItem(index) {
    var file = fileInput.files[index];
    var oLi = box.children[index + 1];
    var isPlaying = Number(oLi.dataset.playing) || 0;
    if (!isPlaying) { return }
    oLi.dataset.playing = 0;
    var fileMd5 = oLi.dataset.md5;

    for (var i = 0; i < FETCH_MAP[fileMd5].length; i++) {
        if (FETCH_MAP[fileMd5][i]) {
            FETCH_MAP[fileMd5][i].abort();
            FETCH_MAP[fileMd5][i] = null;
        }
    }
}

点击删除时,取消上次请求,并隐藏li

function delItem(index) {
    var file = fileInput.files[index];
    var oLi = box.children[index + 1];
    var fileMd5 = oLi.dataset.md5;
    oLi.className = 'hide';

    if (FETCH_MAP[fileMd5]) {
        for (var i = 0; i < FETCH_MAP[fileMd5].length; i++) {
            if (FETCH_MAP[fileMd5][i]) {
                FETCH_MAP[fileMd5][i].abort();
                FETCH_MAP[fileMd5][i] = null;
            }
        }
    }
}

效果图:

593c6dd7129743a1634772de11ae4a13.png
https://www.zhihu.com/video/1086059444886044672

在线演示地址:上传大文件

Web Worker 真正的多线程上传,待更新。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值