一、上传按钮和进度条等
<div>
<h2>上传文件</h2>
<div ref="drag" class="drag">
<input class="file" type="file" @change="handlerChange" />
</div>
<el-progress style="width: 500px;" :percentage="progress"></el-progress>
<div style="margin-top: 16px;">
<el-button type="primary" @click="upload">上传</el-button>
</div>
<div>
<p>hash进度条</p>
<el-progress style="width: 500px;" :percentage="hashProgress"></el-progress>
</div>
<div>
<p>网格进度条</p>
<ul class="grid" :style="{'width': gridWidth + 'px'}">
<li class="grid-block" 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 < 100 && chunk.progress > 0"></i>
</div>
</li>
</ul>
</div>
</div>
二、选择文件
//点击按钮上传
handlerChange (e) {
const [file] = e.target.files
if (!file) return
this.fileData = file
}
//拖拽上传
dragRelevant () {
const dragDom = this.$refs.drag
//进入区域
dragDom.addEventListener('dragover', e => {
dragDom.style.borderColor = '#f00'
e.preventDefault()
})
//离开区域
dragDom.addEventListener('dragleave', e => {
dragDom.style.borderColor = '#41B883'
e.preventDefault()
})
//放下文件
dragDom.addEventListener('drop', e => {
dragDom.style.borderColor = '#41B883'
const [file] = e.dataTransfer.files
if (!file) return
this.fileData = file
e.preventDefault()
})
}
三、利用文件内容计算hash
为了防止文件上传重复,我们可以使用将每个文件都用hash作为文件名来上传,这里用的是spark-md5来计算hash值。
首先定一个分块的大小
const CHUNK_SIZE = 1 * 1024 * 1024 //每次分片大小
因为大文件用整个内容来计算hash肯定是很慢的,我们不能阻塞页面执行其他任务,所以我通过下面三种方式来计算:
- 使用WebWorker来计算
//使用webWorker来计算文件的md5值
calculateHashByWebWorker (chunks) {
this.hashProgress = 0 //hash进度条
return new Promise(resolve => {
const worker = new Worker('/hash.js')
worker.postMessage(chunks)
worker.onmessage = e => {
const { hash, progress } = e.data
this.hashProgress = progress
if (hash) {
resolve(hash)
}
}
})
}
- 使用requestIdleCallbck来计算
//使用requestIdleCallbck来计算文件的md5值 这个方法会在浏览器空闲时调用
calculateHashByRequestIdleCallback (chunks) {
return new Promise(resolve => {
const spark = new Spark.ArrayBuffer()
let count = 0
const appendToSpark = file => {
return new Promise(resolve => {
const reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.onload = data => {
spark.append(data.target.result)
resolve()
}
})
}
const workLoop = async deadLine => {
while (count < chunks.length && deadLine.timeRemaining() > 1) {
await appendToSpark(chunks[count].file)
count++
if (count < chunks.length) {
this.hashProgress = (count * 100 / chunks.length).toFixed(2) - 0
} else {
this.hashProgress = 100
resolve(spark.end())
}
}
window.requestIdleCallback(workLoop)
}
window.requestIdleCallback(workLoop)
})
}
- 实现抽样hash,降低精度,提高效率
大文件每次都全量计算md5的话,效率很低,如果我们每次取每个分片的一部分用来计算,这样会大大提高计算的效率
//抽样hash 取前两个和后一个 中间每兆取前中后三个点
calulateSamplingHash (chunks) {
return new Promise(resolve => {
const spark = new Spark.ArrayBuffer()
const head = chunks.slice(0, 2)
const tail = chunks[chunks.length - 1]
const middle = chunks.slice(2, chunks.length - 1)
const files = []
files.push(head[0].file, head[1].file)
middle.forEach(item => {
const head = item.file.slice(0, 1)
const tail = item.file.slice(-1, item.file.length)
const center = Math.floor(item.file.length - 1) / 2
const middle = item.file.slice(center, center + 1)
files.push(head, tail, middle)
})
files.push(tail.file)
//追加计算hash
const reader = new FileReader()
reader.readAsArrayBuffer(new Blob(files))
reader.onload = data => {
spark.append(data.target.result)
this.hashProgress = 100
resolve(spark.end())
}
})
}
四、上传
将上传的诸多分片都放在对应hash值得目录下面,每次上传前检查下是否有这个文件了
如果有就提示秒传成功
如果没有就读取下这个目录,将这个目录下面的所有文件名都返回给前端
- 检查文件是否已上传
//检查文件是否已上传
const fileExt = this.fileData.name.split('.').pop()
// uploaded:文件是否已上传,uploadedList:上传的分片列表
const { data: { uploaded, uploadedList } } = await this.$axios.get('/checkFile', {
params: {
hash,
ext: fileExt
}
})
if (uploaded) {
this.$message.success('秒传成功')
return
}
//断点续传 根据之前上传的文件
this.chunks = chunks.map((chunk, index) => {
const fileName = `${hash}-${index}`
return {
file: new File([chunk.file], fileName + '.' + fileExt, { type: 'image/mp4' }),
name: fileName,
hash,
progress: uploadedList.includes(fileName) ? 100 : 0 //如果当前分片已经上传,进度直接设置为100
}
})
- 上传请求(断点续传)
//上传请求
async uploadRequest (hash) {
//如果已经上传过了 就不用上传了 用filter过滤掉(断点续传)
const requests = this.chunks.map((chunk, index) => {
if (chunk.progress === 100) {
return null
} else {
const form = new FormData()
form.append('chunk', chunk.file)
form.append('hash', chunk.hash)
form.append('name', chunk.name)
return { form, index, error: 0 }
}
}).filter(val => val)
//实现并发数控制
await this.sendRequest(requests)
//合并上传的分片
this.mergeFile(hash)
}
- 并发数控制+错误重试
//请求并发数控制
sendRequest (requests, limit = 3) {
return new Promise((resolve, reject) => {
const len = requests.length
let counter = 0
let isStop = false //如果一个片段失败超过三次 认为当前网洛有问题 停止全部上传
const startRequest = async () => {
if (isStop) return
const task = requests.shift()
if (task) {
//利用try...catch捕获错误
try {
//具体的接口 抽离出去了
await this.launchRequest(task)
if (counter === len - 1) { //最后一个任务
resolve()
} else { //否则接着执行
counter++
startRequest() //启动下一个任务
}
} catch (error) {
this.$set(this.chunks[task.index], 'progress', -1)
//接口报错重试,限制为3次
if (task.error < 3) {
task.error++
requests.unshift(task)
startRequest()
} else {
isStop = true
reject(error)
}
}
}
}
//启动任务
while (limit > 0) {
//模拟不同大小启动
setTimeout(() => {
startRequest()
}, Math.random() * 2000)
limit--
}
})
}
参考文章:http://blog.ncmem.com/wordpress/2023/09/25/vue%e5%ae%9e%e7%8e%b0%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%87%e7%89%87%e4%b8%8a%e4%bc%a0%e3%80%81%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e3%80%81%e5%b9%b6%e5%8f%91%e6%95%b0%e6%8e%a7%e5%88%b6%e7%ad%89/
欢迎入群一起讨论
标签:断点续传,vue,hash,file,new,chunks,const,上传 From: https://www.cnblogs.com/songsu/p/17727480.html