说明
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://i-blog.csdnimg.cn/blog_migrate/f3380efde73dd61a8d37d6f5b3762d3d.jpeg)
在线演示地址:上传大文件
Web Worker 真正的多线程上传,待更新。。。