最近在学习文件上传相关知识,记录下整体流程
一、上传按钮和进度条等
上传文件
上传
hash进度条
网格进度条
-
二、选择文件
//点击按钮上传
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--
}
})
}
完整代码地址(file-vue、file-node)