文件上传已是老生常谈了。
文件上传,原始的方法有:
- form表单提交。
- 通过iframe+form表单进行模拟异步上传。
由于Http协议的限制,在处理大文件上传会存在超时的现象。在旧的浏览器中是无法读取文件二进制数据。无法将文件以分片的方式进行上传。
最近由于产品需求,在不安装插件的情况下上传大文件(百度webuploader开源项目做得已经很不错了,但在秒传和低版本浏览器上使用flash不太符合要求)。
于是重新撸了一把html大文件上传(整个项目是spring boot搭建)。
先来简单的介绍用了哪些javascript类库
- File 文件
- FileRead 读取文件
- XMLHttpRequest 上传文件
- FormData 上传时所需数据
- spark-md5 用于MD5 HASH值计算(第三方插件)
本次实现拖拽上传,用到了页面拖拽事件:ondragenter,ondragleave,ondragover,ondrop
实现思路
- 建立一个上传队列。
- 通过拖拽获取得到文件。
- 为每个文件添加各自的上传方法。
- 秒传判断
- 上传分片文件 (后台存放临时分片文件)
- 合并文件
- 添加文件到上传队列。
最终效果
代码部分
html
* {
margin: 0;
padding: 0;
}
.dropZone {
width: 300px;
height: 200px;
line-height: 200px;
margin: 10px auto;
border: 2px dashed #ccc;
color: #cccccc;
text-align: center;
}
.dropZone.dropOver {
border-color: #000;
color: #000;
}
button {
margin-right: 10px;
}
drop file
fileList
MD5
//md5
!function (win, sparkMD5) {
let Md5File = function (options) {
this._init(options);
this.fileMd5Hash();
};
Md5File.prototype = {
_init: function (opts) {
this.file = opts.file;
this.fileSliceLength = opts.fileSliceLength || 1024 * 1024;
this.chunks = opts.chunks || 10;
this.chunkSize = parseInt(this.file.size / this.chunks, 10);
this.currentChunk = 0;
this.md5Complete = opts.md5Complete;
this.spark = new sparkMD5.ArrayBuffer();
this.sparks = [];
},
fileMd5Hash: function () {
this._readFileMd5Hash();
},
_readFileMd5Hash: function () {
let self = this;
console.group("currentChunk:", self.currentChunk,
'chunkSize', self.chunkSize,
'fileSize:', self.file.size,
'fileSliceLength:', self.fileSliceLength);
let start = self.currentChunk * self.chunkSize;
let end = Math.min(start + self.chunkSize, self.file.size);
if (self.currentChunk < self.chunks) {
end = start + self.fileSliceLength;
}
console.log('start:', start, 'end:', end, 'get chunk:', end - start);
console.groupEnd();
let fileReader = new FileReader();
fileReader.onload = function (e) {
let spark = new sparkMD5.ArrayBuffer();
spark.append(e.target.result);
let tempSpark = spark.end();
console.log(tempSpark);
self.sparks.push(tempSpark);
self.currentChunk++;
if (self.currentChunk < self.chunks) {
self._readFileMd5Hash();
} else {
let endSparkStr = self.sparks.join('');
console.log(endSparkStr);
self.md5Complete && typeof self.md5Complete == 'function' && self.md5Complete(endSparkStr);
}
/* self.spark.append(e.target.result);
self.currentChunk++;
if (self.currentChunk < self.chunks) {
self._readFileMd5Hash();
} else {
self.md5Complete && typeof self.md5Complete == 'function' && self.md5Complete(self.spark.end());
}*/
};
fileReader.readAsArrayBuffer(self.file.slice(start, end));
}
};
win.md5File = function (options) {
return new Md5File(options);
}
}(window, SparkMD5);
上传类
//文件上传类
!function (win) {
'use strict';
let FileUpload = function (options) {
if (typeof options != 'object') {
throw Error('options not is object');
}
this.init(options);
if (this.autoUpload) {
this.start();
}
};
FileUpload.prototype = {
init: function (opts) {
this.file = opts.file;
this.url = opts.url;
this.compileFileUrl = opts.compileFileUrl;
this.autoUpload = opts.autoUpload || 0;
this.chunk = opts.chunk || (10 * 1024 * 1024);
this.chunks = Math.ceil(this.file.size / this.chunk);
this.currentChunk = 0;
this.taskName = opts.taskName || win.uuid();
this.isUploading = false;
this.isUploaded = false;
this.upProgress = opts.upProgress; //上传进度
this.upComplete = opts.upComplete; //上传完成
this.timeHandle = opts.timeHandle; // 时间处理
this.timeInfo = {
h: 0,
m: 0,
s: 0
};
this.md5FileHash = null;
this.remoteMd5FileHash = opts.remoteMd5FileHash; //远程对比文件,是http请求函数
this.isSendCompleteFile = opts.isSendCompleteFile || 0; //是否发送合并文件指令
return this;
},
start: function () {
let self = this;
//秒传判断
if (!self.md5FileHash) {
win.md5File({
file: self.file,
md5Complete: function (hash) {
self.md5FileHash = hash;
//后台进行判断hash值是否一致,如何一致则直接上传完成。
if (self.remoteMd5FileHash && typeof self.remoteMd5FileHash == 'function') {
self.remoteMd5FileHash(self.file.name, self.md5FileHash, function (result) {
if (result) {
self.initInterval();
self.isUploaded = true;
self.isUploading = true;
self.upProgress && typeof self.upProgress == 'function' && self.upProgress(1, 1);
if (self.upComplete && typeof self.upComplete == 'function') {
if (self.time) win.clearInterval(self.time);
self.upComplete();
}
} else {
if (!self.isUploading && !self.isUploaded) {
self.initInterval();
self._upload();
}
}
});
}
}
});
} else {
//上传
if (!self.isUploading && !self.isUploaded) {
self.initInterval();
self._upload();
}
}
},
pause: function () {
if (this.isUploading) {
this.xhr && this.xhr.abort();
this.isUploading = false;
if (this.time) win.clearInterval(this.time);
}
},
_initXhr: function () {
let self = this;
self.xhr = new XMLHttpRequest();
self.xhrLoad = function () {
if (self.end == self.file.size) {
self.isUploaded = true;
if (self.upComplete && typeof self.upComplete == 'function') {
if (self.time) win.clearInterval(self.time);
self.upComplete();
}
self.isSendCompleteFile && self.compileFile();
} else {
if (self.upProgress && typeof self.upProgress == 'function') {
self.upProgress((self.currentChunk + 1), self.chunks);
}
self.currentChunk++;
self._upload();
}
};
self.xhrError = function () {
console.log('xhr error');
};
self.xhrAbort = function () {
console.log('xhr abort');
};
self.xhr.onload = this.xhrLoad;
self.xhr.onerror = this.xhrError;
self.xhr.onabort = this.xhrAbort;
},
_upload: function () {
let self = this;
self.isUploading = true;
self._initXhr();
self.xhr.open('POST', self.url);
//计算上传开始位置或结束位置
self.begin = self.currentChunk * self.chunk;
self.end = Math.min((self.begin + self.chunk), self.file.size);
let blob = self.file.slice(self.begin, self.end, {type: 'text/plain'});
let formData = new FormData();
let tempFileName = 'temp-' + self.currentChunk + '-' + self.taskName;
formData.append('fileData', blob, tempFileName);
formData.append('tempFileName', tempFileName);
formData.append('taskName', self.taskName);//用于后台进行判断如果此片已存在则进行删除。
formData.append('fileName', self.file.name);
formData.append('position', self.begin);
formData.append('chunkIndex', (self.currentChunk + 1));
formData.append('chunks', self.chunks);
self.xhr.send(formData);
},
compileFile: function () {
let self = this;
self._initXhr();
self.xhrLoad = function () {
console.log(self.xhr.responseText);
};
self.xhr.onload = self.xhrLoad;
self.xhr.open('POST', self.compileFileUrl);
let formData = new FormData();
formData.append("fileName", (self.file.name));
formData.append('taskName', self.taskName);
self.xhr.send(formData);
},
initInterval: function () {
let self = this;
if (self.time) win.clearInterval(self.time);
self.time = win.setInterval(function () {
self.timeInfo.s++;
if (self.timeInfo.s == 60) {
self.timeInfo.s = 0;
self.timeInfo.m++;
if (self.timeInfo.m == 60) {
self.timeInfo.m = 0;
self.timeInfo.h++;
}
}
self.timeHandle && self.timeHandle(self.formatStr());
}, 1000);
},
formatStr: function () {
let _ = this,
sY = (_.timeInfo.h < 10) ? '0' + _.timeInfo.h : _.timeInfo.h,
sM = (_.timeInfo.m < 10) ? '0' + _.timeInfo.m : _.timeInfo.m,
sS = (_.timeInfo.s < 10) ? '0' + _.timeInfo.s : _.timeInfo.s;
return sY + ':' + sM + ':' + sS;
}
};
win.fileUpload = function (options) {
return new FileUpload(options);
};
}(window);
拖拽
//拖拽
!function () {
'use strict';
let dropZone = document.querySelector('#dropZone');
let uploadList = document.querySelector('#uploadList');
dropZone.ondragenter = function () {
this.className = 'dropZone dropOver';
};
dropZone.ondragleave = function () {
this.className = 'dropZone';
return false;
};
dropZone.ondragover = function () {
return false;
};
let upComplete = function () {
console.log('file up complete');
this.progressDivIng.style.width = '100%';
};
let upProgress = function (chunkIndex, chunks) {
console.log('up progress:', chunkIndex, chunks);
//console.log(this.progressDivIng);
this.progressDivIng.style.width = (chunkIndex / chunks) * 100 + '%';
};
let timeHandle = function (timeStr) {
this.timeSpan.innerText = timeStr;
};
let remoteMd5FileHash = function (fileName, md5FileHash, callback) {
let xhr = new XMLHttpRequest();
xhr.onload = function () {
let data = xhr.responseText;
callback && callback(data == 'ok');
};
xhr.open('POST', '/fileHash');
let formData = new FormData();
formData.append("fileName", fileName);
formData.append('md5Hash', md5FileHash);
xhr.send(formData);
};
let tasks = {};
let uploadHandler = function (files) {
let x = 0;
for (x; x < files.length; x++) {
let taskName = window.uuid();
let file = files[x];
tasks[taskName] = window.fileUpload({
file: files[x],
url: '/upload',
compileFileUrl: '/compileFile',
autoUpload: 0,
isSendCompleteFile: 1,
taskName: taskName,
upProgress: upProgress,
upComplete: upComplete,
timeHandle: timeHandle,
remoteMd5FileHash: remoteMd5FileHash
});
let li = document.createElement('li');
let startBtn = document.createElement('button');
startBtn.innerText = 'START';
startBtn.onclick = function () {
console.log('start');
tasks[taskName].start();
};
let endBtn = document.createElement('button');
endBtn.innerText = 'PAUSE';
endBtn.onclick = function () {
tasks[taskName].pause();
};
let cancelBtn = document.createElement('button');
cancelBtn.innerText = 'CANCEL';
cancelBtn.onclick = function () {
let currentUpTask = tasks[taskName];
if (currentUpTask.isUploaded) {
uploadList.removeChild(document.getElementById(taskName));
} else {
//TODO:向后台发送取消请求
currentUpTask.pause();
let xhr = new XMLHttpRequest();
xhr.onload = function () {
uploadList.removeChild(document.getElementById(taskName));
};
xhr.open('GET', '/cancel');
let formData = new FormData();
formData.append('taskName', taskName);
xhr.send(formData);
}
};
let progressDiv = document.createElement('div');
progressDiv.style.border = '1px solid #ccc';
progressDiv.style.height = '10px';
progressDiv.style.marginLeft = '5px';
progressDiv.style.marginRight = '5px';
let progressDivIng = document.createElement('div');
progressDivIng.style.background = 'red';
progressDivIng.style.width = '0px';
progressDivIng.style.height = '10px';
progressDiv.appendChild(progressDivIng);
let timeSpan = document.createElement('span');
timeSpan.innerText = '00:00:00';
timeSpan.style.paddingLeft = '10px';
timeSpan.style.paddingRight = '10px';
li.innerHTML = file.name;
li.setAttribute('id', taskName);
li.appendChild(timeSpan);
li.appendChild(startBtn);
li.appendChild(endBtn);
li.appendChild(cancelBtn);
li.appendChild(progressDiv);
uploadList.appendChild(li);
tasks[taskName].progressDivIng = progressDivIng;
tasks[taskName].timeSpan = timeSpan;
}
};
let acceptFile = function (file) {
let rExt = /\.\w+$/;
return rExt.exec(file.name) && (file.size || file.type);
};
dropZone.ondrop = function (e) {
e.preventDefault();
let files = e.dataTransfer.files;
let newFiles = [];
for (let i = 0; i < files.length; i++) {
let file = files[i];
if (acceptFile(file)) {
newFiles.push(file);
}
}
uploadHandler(newFiles);
//uploadHandler(e.dataTransfer.files);
};
}();