网上已经有了很多关于大文件上传的技术解决文案,但是有一个难点没有解决,大部分都是直接将大文件直接分成n份然后全部上传(如果n特别大是怎么办?),但是实际上用户可能把大文件切为n份,每次上传m(m < n)个文件。这里主要记录一下此问题的关键技术点。
项目需求
首先是实现原理,1个G的大文件如果在上传至服务器的时候分成10份来传输,在带宽够用的情况下,理论上的上传速度可以提升10倍。
初始需要定义的属性
return {
// 被上传的文件
file: {},
// 表示切分文件的集合
chunks: [],
// 文件上传进度
uploadProgress: 0,
// chunks上传序号
uploadNum: 0,
// 已经上传完成的切片数
uploadedNum: 0,
// 文件上传并发数
maxUploadNums: 10,
// 文件切片大小
splieSize: 1024 * 1024 * 5,
// 文件MD5校验值
md5: ''
}
切分文件的方法
function sliceFile (file, piece = 1024 * 1024 * 5) {
// piece表示文件被切割的单个大小,默认5M,支持自定义大小
// 文件总大小
const totalSize = file.size
// 每次上传的开始字节
let start = 0
// 每次上传的结尾字节
let end = start + piece
// chunks表示切分文件的集合
const chunks = []
while (start < totalSize) {
const blob = file.slice(start, end)
chunks.push(blob)
start = end
end = start + piece
}
}
生成文件MD5(文件MD5提交给后端,后端在合并分片后用以校验文件是否和前端上传的文件一致)
// 需要使用spark-md5
import SparkMD5 from 'spark-md5'
function generateMD5 (file) {
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
let currentChunk = 0
const chunkSize = this.splieSize
const chunksNum = Math.ceil(file.size / chunkSize)
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
fileReader.onload = (e) => {
console.log('read chunk nr', currentChunk + 1, 'of', chunksNum)
spark.append(e.target.result)
currentChunk++
if (currentChunk < chunksNum) {
loadNext()
} else {
this.fileMD5 = spark.end()
console.log(this.fileMD5)
}
}
fileReader.onerror = function () {
console.warn('oops, something went wrong.')
}
function loadNext () {
const start = currentChunk * chunkSize
const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
}
loadNext()
}
开始上传
startUpload () {
const lth = this.chunks.length > this.maxUploadNums ? this.maxUploadNums : this.chunks.length
this.uploadNum = 0
this.uploadedNum = 0
for (let i = 0; i < lth; i++) {
this.uploadChunk({
serial: this.file.uid,
chunk: this.uploadNum,
file: this.chunks[this.uploadNum]
})
this.uploadNum++
}
},
上传文件的方法
uploadChunk (params = {}) {
const form = new FormData()
form.append('serial', params.serial)
form.append('chunk', params.chunk)
form.append('file', params.file)
const { code } = await uploadFileAPI(form)
if (code === 200) {
this.uploadedNum++
if (this.uploadNum < this.chunks.length) {
this.uploadChunk({
serial: this.file.uid,
chunk: this.uploadNum,
file: this.chunks[this.uploadNum]
})
this.uploadNum++
} else if (this.uploadedNum === this.chunks.length) {
// 切片上传完成,开始合并切片
this.mergeFile({
id: this.file.uid
})
}
}
},
合并文件
async mergeFile (params = {}) {
params.md5 = this.fileMD5
const { code } = await mergeFileAPI(params)
if (code === 200) {
// 合并成功
}
}