最近在學(xué)習(xí)文件上傳相關(guān)知識杆勇,記錄下整體流程
頁面效果
一贪壳、上傳按鈕和進(jìn)度條等
<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進(jìn)度條</p>
<el-progress style="width: 500px;" :percentage="hashProgress"></el-progress>
</div>
<div>
<p>網(wǎng)格進(jìn)度條</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>
二、選擇文件
//點(diǎn)擊按鈕上傳
handlerChange (e) {
const [file] = e.target.files
if (!file) return
this.fileData = file
}
//拖拽上傳
dragRelevant () {
const dragDom = this.$refs.drag
//進(jìn)入?yún)^(qū)域
dragDom.addEventListener('dragover', e => {
dragDom.style.borderColor = '#f00'
e.preventDefault()
})
//離開區(qū)域
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()
})
}
三蚜退、利用文件內(nèi)容計算hash
為了防止文件上傳重復(fù)寥袭,我們可以使用將每個文件都用hash作為文件名來上傳,這里用的是spark-md5來計算hash值关霸。
首先定一個分塊的大小
const CHUNK_SIZE = 1 * 1024 * 1024 //每次分片大小
因?yàn)榇笪募谜麄€內(nèi)容來計算hash肯定是很慢的传黄,我們不能阻塞頁面執(zhí)行其他任務(wù),所以我通過下面三種方式來計算:
- 使用WebWorker來計算
//使用webWorker來計算文件的md5值
calculateHashByWebWorker (chunks) {
this.hashProgress = 0 //hash進(jìn)度條
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值 這個方法會在瀏覽器空閑時調(diào)用
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)
})
}
- 實(shí)現(xiàn)抽樣hash队寇,降低精度膘掰,提高效率
大文件每次都全量計算md5的話,效率很低佳遣,如果我們每次取每個分片的一部分用來計算识埋,這樣會大大提高計算的效率
//抽樣hash 取前兩個和后一個 中間每兆取前中后三個點(diǎn)
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())
}
})
}
四、上傳
將上傳的諸多分片都放在對應(yīng)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
}
//斷點(diǎn)續(xù)傳 根據(jù)之前上傳的文件
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 //如果當(dāng)前分片已經(jīng)上傳诵盼,進(jìn)度直接設(shè)置為100
}
})
- 上傳請求(斷點(diǎn)續(xù)傳)
//上傳請求
async uploadRequest (hash) {
//如果已經(jīng)上傳過了 就不用上傳了 用filter過濾掉(斷點(diǎn)續(xù)傳)
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)
//實(shí)現(xiàn)并發(fā)數(shù)控制
await this.sendRequest(requests)
//合并上傳的分片
this.mergeFile(hash)
}
- 并發(fā)數(shù)控制+錯誤重試
//請求并發(fā)數(shù)控制
sendRequest (requests, limit = 3) {
return new Promise((resolve, reject) => {
const len = requests.length
let counter = 0
let isStop = false //如果一個片段失敗超過三次 認(rèn)為當(dāng)前網(wǎng)洛有問題 停止全部上傳
const startRequest = async () => {
if (isStop) return
const task = requests.shift()
if (task) {
//利用try...catch捕獲錯誤
try {
//具體的接口 抽離出去了
await this.launchRequest(task)
if (counter === len - 1) { //最后一個任務(wù)
resolve()
} else { //否則接著執(zhí)行
counter++
startRequest() //啟動下一個任務(wù)
}
} 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)
}
}
}
}
//啟動任務(wù)
while (limit > 0) {
//模擬不同大小啟動
setTimeout(() => {
startRequest()
}, Math.random() * 2000)
limit--
}
})
}
完整代碼地址(file-vue银还、file-node)