切片上传(秒传、断点续传)
结果展示
每个方块都代表一个文件切片的上传状态。绿色表示此文件切片已经上传成功;蓝色高度代表正在上传的文件切片的上传进度;红色代表上传失败。
前端实现
1. 文件切片
将文件分割成指定大小的chunk,存储到数组中
createFileChunk(file, size=CHUNK_SIZE) {
const chunks = []
let cur = 0
while (cur < this.file.size) {
chunks.push({index: cur, file: this.file.slice(cur, cur+size)})
cur+=size
}
return chunks
}
2.计算文件hash
采用md5算法计算hash,作为文件唯一标识。
如果文件太大,md5算法会比较耗时,所以对文件进行抽样,然后再计算。
md5算法插件:spark-md5
async calculateHashSimple () {
const file = this.file
return new Promise(async (resolve) => {
const offset = 0.1 * 1024 * 1024
const size = file.size
let cur = offset
// 取第一段内容
let chunks = [file.slice(0, cur)]
while (cur <= size) {
if (cur + offset >= size) {
// 取最后一段
chunks.push(file.slice(cur))
} else {
const middle = cur + offset/2
const end = cur + offset
const middleChunk = file.slice(middle, middle + 2)
// 中间每段内容都取头部、中间、尾部两个字节
chunks.push(file.slice(cur, cur+ 2), middleChunk, file.slice(end - 2, end))
}
cur += offset
}
let reader = new FileReader()
// ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。
// Blob 对象表示一个不可变、原始数据的类文件对象
reader.readAsArrayBuffer(new Blob(chunks))
reader.onload = async (e) => {
const result = e.target.result
const spark = new sparkMD5.ArrayBuffer()
spark.append(result)
resolve(spark.end())
}
})
}
3.调用接口,获取文件是否已经上传
async uploadFile () {
// 文件切片上传
this.chunks = this.createFileChunk(this.file)
// 计算文件hash
this.hash = await this.calculateHashSimple()
// 文件是否上传过
const {data: {uploaded, uploadedList}} = await this.$http.post('/checkfile', {
hash: this.hash,
ext: this.file.name.split('.').pop()
})
......
}
4.文件秒传
若文件之前已经上传成功,则直接提示“文件秒传成功”。
这里根据’/checkfile’接口返回的uploaded值来判断是否已经上传过。
async uploadFile () {
......
if (uploaded) {
this.chunks = this.chunks.map((chunk, index) => {
return {
name: this.hash + '-' + index,
progress: 100
}
})
return this.$message.success('秒传成功')
}
......
}
5.为文件切片添加属性值
map遍历this.chunks,为文件切片添加切片名称、hash、progress(上传进度,100-成功,0-未上传,视图的上传进度需要此值)等属性值。
async uploadFile () {
......
this.chunks = this.chunks
.map((chunk, index) => {
return {
name: this.hash + '-' + index,
index: index,
hash: this.hash,
chunk: chunk.file,
progress: uploadedList.indexOf(`${this.hash}-${index}`) < 0 ? 0 : 100
}
})
// 上传文件切片
this.uploadChunks(uploadedList)
......
}
6.上传文件切片(之前未上传过的)
async uploadChunks (uploadedList) {
const requests = this.chunks
.filter((chunk => uploadedList.indexOf(chunk.name) < 0))
.map((chunk) => {
let form = new FormData()
form.append('name', chunk.name)
form.append('chunk', chunk.chunk)
form.append('hash', chunk.hash)
return {form, index: chunk.index, error: 0}
})
// 并发量控制
try {
await this.requestLimit(requests, 3)
await this.mergeRequest()
} catch (err) {
this.$message.error('当前网络不好,已中断上传')
}
}
7.并发量控制
如果文件切片较多,同时发起请求,会造成浏览器卡顿甚至崩溃,所以需要控制请求并发量。
控制请求并发量为limit,如果某个文件切片连续3次都上传失败的话,则立即停止文件上传,并抛出网络异常提示。
async requestLimit (chunkRequests, limit) {
let stopFlag = false
return new Promise((resolve, reject) => {
const start = async () => {
if (stopFlag) {
return
}
let task = chunkRequests.shift()
try {
await this.$http.post('/uploadfile', task.form, {
onUploadProgress: (progress) => {
this.chunks[task.index].progress = Number(((progress.loaded/progress.total)*100).toFixed(2))
}
})
if (chunkRequests.length > 0) {
start()
} else {
resolve()
}
} catch (err) {
this.chunks[task.index].progress = -1
task.error++
if (task.error > 2) {
stopFlag = true
reject()
} else {
chunkRequests.unshift(task)
start()
}
}
}
while(limit > 0) {
// setTimeout(() => {
start()
// }, (Math.random) * 2000)
limit--
}
})
}
8.合并文件
告诉后台,文件切片已经全部上传,后台合并文件碎片并存储。
async mergeRequest () {
this.$http.post('/mergefile', {
ext: this.file.name.split('.').pop(),
size: CHUNK_SIZE,
hash: this.hash
})
}
9.html代码
<div>
<!-- chunk.progress
progress < 0 error 红色
process == 100 success
别的数字 方块高度显示 -->
<div class="cube-container" :style="{width: cubeWidth + 'px'}">
<div class="cube" v-for="chunk in chunks" :key="chunk.name">
<div
:class="{
'uploading': chunk.progress > 0 && chunk.progress < 100,
'success': chunk.progress == 100,
'error': chunk.progress < 0
}"
:style="{height: chunk.progress + '%'}"
>
<i class="el-icon-loading" style="color:#f56c6c" v-if="chunk.progress > 0 && chunk.progress < 100"></i>
</div>
</div>
</div>
</div>
<!-- 预加载loading,避免上传进度中loading图块刚开始显示不全 -->
<i class="el-icon-loading" style="visibility: hidden"></i>
10.css代码
// 使用less
.cube-container
.cube
width 16px
height 16px
line-height 12px
border 1px solid black
background #eee
float left
> .success
background-color green
> .uploading
background-color blue
> .error
background-color red