背景介绍:
1、文件上传超时:原因是前端请求框架认限制最大请求时长,或者是 nginx(或其它代理/网关) 限制了最大请求时长。
2、文件大小超限:原因在于后端对单个请求大小做了限制,一般 nginx 和 server 都会做这个限制。上传耗时久。
3、由于各种网络原因上传失败,且失败之后需要从头开始。
分片原理:
分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,文件切片和核心是使用 Blob 对象的 slice 方法blob.slice(startByte, endByte),上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。
文件妙传,秒传指的是文件在传输之前计算其内容的散列值,也就是 Hash 值,将该值传到后台,如果后台存在 Hash 值一致的文件,认为该文件上传完成。(推荐使用 spark-md5 生成 md5文件,既可以直接返回md5又可以对分片返回md5,对于大体积的文件计算更加稳定)
let baseUrl = 'http://localhost:5000'
let chunkSize = 5 * 1024 * 1024
let fileSize = 0
let file = null
let hasUploaded = 0
let chunks = 0
$("#file").on('change', function () {
file = this.files[0]
fileSize = file.size;
responseChange(file)
})
// 0.响应点击
async function responseChange(file) {
$("#process1").slideDown(200)
// 第一步:开始生成md5
let fileMd5Value = await md5File(file)
// 第二步:校验文件的MD5
let result = await checkFileMD5(file.name, fileMd5Value)
// 如果文件已存在, 就秒传
if (result.file) {
alert('文件已秒传')
return
}
// let exit = false
// 显示文件上传进度
$("#process2").slideDown(200)
// 第三步:检查并上传MD5
await checkAndUploadChunk(fileMd5Value, result.chunkList)
// 第四步: 通知服务器所有分片已上传完成
notifyServer(fileMd5Value)
}
function md5(file, chunkSize) {
let _this = this
return new Promise((resolve, reject) => {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let spark = new SparkMD5.ArrayBuffer(); //追加数组缓冲区。
let fileReader = new FileReader(); //读取文件
fileReader.onload = function (e) {
spark.append(e.target.result);
currentChunk++;
_this.md5Obj.percent = Math.floor((currentChunk / chunks) * 100);
_this.container.file.MD5Progress = _this.md5Obj.percent
if (_this.onMD5Progress(_this.container.file) === false) return;
if (currentChunk < chunks) {
loadNext();
} else {
_this.md5Obj.md5 = spark.end(); //完成md5的计算,返回十六进制结果。
resolve(_this.md5Obj);
}
};
fileReader.onerror = function (e) {
reject(e);
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = start + chunkSize;
(end > file.size) && (end = file.size);
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
}
// 2.校验文件的MD5
function checkFileMD5(fileName, fileMd5Value) {
return new Promise((resolve, reject) => {
let url = baseUrl + '/check/file?fileName=' + fileName + "&fileMd5Value=" + fileMd5Value
$.getJSON(url, function (data) {
resolve(data)
})
})
}
// 3.上传chunk
async function checkAndUploadChunk(fileMd5Value, chunkList) {
chunks = Math.ceil(fileSize / chunkSize)
hasUploaded = chunkList.length
for (let i = 0; i < chunks; i++) {
let exit = chunkList.indexOf(i + "") > -1
// 如果已经存在, 则不用再上传当前块
if (!exit) {
let index = await upload(i, fileMd5Value, chunks)
hasUploaded++
let radio = Math.floor((hasUploaded / chunks) * 100)
$("#uploadProcessStyle").css({
width: radio + '%'
})
$("#uploadProcessValue").html(radio + '%')
}
}
}
// 3-2. 上传chunk
function upload(i, fileMd5Value, chunks) {
return new Promise((resolve, reject) => {
//构造一个表单,FormData是HTML5新增的
let end = (i + 1) * chunkSize >= file.size file.size : (i + 1) * chunkSize
let form = new FormData()
form.append("data", file.slice(i * chunkSize, end)) //file对象的slice方法用于切出文件的一部分
form.append("total", chunks) //总片数
form.append("index", i) //当前是第几片
form.append("fileMd5Value", fileMd5Value)
$.ajax({
url: baseUrl + "/upload",
type: "POST",
data: form, //刚刚构建的form数据对象
async: true, //异步
processData: false, //很重要,告诉jquery不要对form进行处理
contentType: false, //很重要,指定为false才能形成正确的Content-Type
success: function (data) {
resolve(data.desc)
}
})
})
}
// 第四步: 通知服务器所有分片已上传完成
function notifyServer(fileMd5Value) {
let url = baseUrl + '/merge?md5=' + fileMd5Value + "&fileName=" + file.name + "&size=" + file.size
$.getJSON(url, function (data) {
alert('上传成功')
})
}