HTML5 大文件上传
用到的库
File 文件
FileRead 读取文件
XMLHttpRequest 上传文件
FormData 上传时所需数据
spark-md5 用于MD5 HASH值计算(第三方插件)
本次实现拖拽上传,用到了页面拖拽事件:ondragenter,ondragleave,ondragover,ondrop
实现思路
建立一个上传队列。
通过拖拽获取得到文件。
为每个文件添加各自的上传方法。
秒传判断
上传分片文件 (后台存放临时分片文件)
合并文件
添加文件到上传队列。
HTML
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
<style>
* {
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;
}
</style>
</head>
<body>
<div id="dropZone" class="dropZone"> drop file</div>
<div>
<h1>fileList</h1>
<ul id="uploadList"></ul>
</div>
</body>
</html>
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);
};
}();
检查浏览器是否完全支持 File API
// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
// Great success! All the File APIs are supported.
} else {
alert('The File APIs are not fully supported in this browser.');
}
使用表单输入进行选择
<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>
<script>
function handleFileSelect(evt) {
var files = evt.target.files; // FileList object
// files is a FileList of File objects. List some properties.
var output = [];
for (var i = 0, f; f = files[i]; i++) {
output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
f.size, ' bytes, last modified: ',
f.lastModifiedDate.toLocaleDateString(), '</li>');
}
document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
}
document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>
使用拖放操作进行选择
<div id="drop_zone">Drop files here</div>
<output id="list"></output>
<script>
function handleFileSelect(evt) {
evt.stopPropagation();
evt.preventDefault();
var files = evt.dataTransfer.files; // FileList object.
// files is a FileList of File objects. List some properties.
var output = [];
for (var i = 0, f; f = files[i]; i++) {
output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
f.size, ' bytes, last modified: ',
f.lastModifiedDate.toLocaleDateString(), '</li>');
}
document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
}
function handleDragOver(evt) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
}
// Setup the dnd listeners.
var dropZone = document.getElementById('drop_zone');
dropZone.addEventListener('dragover', handleDragOver, false);
dropZone.addEventListener('drop', handleFileSelect, false);
</script>
参考链接:
1.https://developer.mozilla.org/zh-CN/docs/Web/API/FormData/Using_FormData_Objects
2.https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Submitting_forms_and_uploading_files