Vue實(shí)現(xiàn)封裝一個(gè)切片上傳組件
組件效果
單文件切片上傳

多文件切片上傳

組件使用案例
<template>
<div id="app">
<div class="upload-wrap">
<UploadSlice
:action="uploadInfoSlice.actionChunk"
:headers="uploadInfoSlice.headers"
:limit="uploadInfoSlice.limit"
:accept="uploadInfoSlice.accept"
:show-file-list="false"
cancelable
:on-success="handleSuccess"
:on-remove="handleRemove"
:on-cancel="handleCancel"
:on-upload-pre="handleUploadPre"
:on-upload-merge="handleUploadMerge"
:on-form-data="genFormData"
/>
</div>
</div>
</template>
<script>
import UploadSlice from './components/UploadSlice.vue'
import { uploadPre, uploadMerge } from '@/api/upload-slice'
export default {
name: 'App',
components: {
UploadSlice
},
data() {
return {
// 上傳部分
uploadInfoSlice: {
actionChunk: process.env.VUE_APP_BASE_API + '/storage/file/v3/chunk', // 切片請(qǐng)求上傳路徑
headers: { 'Authorization': 'Bearer XXX' }
}
}
},
methods: {
// 分片預(yù)請(qǐng)求
async handleUploadPre(file) {
const form = new FormData()
form.append('fileSource', 'APPLICATION')
form.append('originFileName', file.name)
let res = ''
try {
res = await uploadPre(form)
} catch (error) {
throw new Error(error)
}
},
// 構(gòu)造分片參數(shù)
genFormData(chunks, uid) {
const prepareId = this.getCurrentPrepareId(uid)
return chunks.map(chunk => {
const form = new FormData()
form.append('chunk', chunk.file)
form.append('uploadId', prepareId)
form.append('partNumber', chunk.index)
return form
})
},
// 合并請(qǐng)求
async handleUploadMerge(file, uid) {
const prepareId = this.getCurrentPrepareId(uid)
const form = new FormData()
form.append('fileSource', 'APPLICATION')
form.append('hash', prepareId)
form.append('filename', file.name)
// return 建議使用, 用于handleSuccess獲取數(shù)據(jù)
try {
const res = await uploadMerge(form)
return res
} catch (error) {
return error
}
},
// 判斷當(dāng)前處理prepareId
getCurrentPrepareId(uid) {
for (const item of this.progressFileList) {
if (item.uid === uid) {
return item.prepareId
}
}
}
}
}
</script>使用文檔
Attribute
標(biāo)紅色部分為二次封裝處理過(guò)的功能,其他為el-upload自帶屬性
| 參數(shù) | 說(shuō)明 | 類型 | 可選值 | 默認(rèn)值 | 備注 |
|---|---|---|---|---|---|
| action | 必選參數(shù),分片上傳的地址,預(yù)請(qǐng)求和合并請(qǐng)求在組件外操作 | String | - | - | |
| headers | 設(shè)置上傳的請(qǐng)求頭部 | String | - | - | |
| multiple | 是否支持多選文件 | boolean | - | ||
| accept | 可上傳文件類型,多種類型用","分隔 (格式不符合自動(dòng)提示) | String | - | - | |
| on-remove | 文件列表移除文件時(shí)的鉤子 | function(file, fileList) | — | — | |
| on-success | 文件上傳成功時(shí)的鉤子 | function(response, file, fileList) | — | — | |
| on-error | 文件上傳失敗時(shí)的鉤子 | function(err, file, fileList) | — | — | |
| on-progress | 文件上傳時(shí)的鉤子 | function(event, file, fileList) | — | — | |
| on-change | 文件狀態(tài)改變時(shí)的鉤子,添加文件、上傳成功和上傳失敗時(shí)都會(huì)被調(diào)用 | function(file, fileList) | — | — | |
| on-exceed | 文件超出個(gè)數(shù)限制時(shí)的鉤子 | function(files, fileList) | — | — | |
| list-type | 文件列表的類型 | string | text/picture/picture-card | text | |
| show-file-list | 是否顯示已上傳文件列表(文件分片上傳時(shí)建議設(shè)置false,否則會(huì)有兩個(gè)進(jìn)度條) | boolean | — | true | |
| file-list | 上傳的文件列表, 例如: [{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}] | array | — | [] | |
| disabled | 是否禁用 | boolean | — | false | |
| cancelable | 是否支持取消 | boolean | — | false | |
| limit | 最多允許上傳個(gè)數(shù)(超出個(gè)數(shù)自動(dòng)提示) | number | — | — | |
| size | 限制大小 | String | — | — | |
| hideBtn | 是否在上傳過(guò)程中隱藏上傳按鈕 | boolean | — | — | false |
Slot
| 插槽名 | 說(shuō)明 |
|---|---|
| trigger | 觸發(fā)文件選擇框的內(nèi)容 |
| tip | 提示說(shuō)明文字 |
| more-tips | 在默認(rèn)提示后補(bǔ)充說(shuō)明 |
封裝過(guò)程
切片上傳組件是基于el-upload進(jìn)行的二次封裝,文章開(kāi)頭組件效果演示可以看到上傳一個(gè)文件會(huì)發(fā)送三個(gè)請(qǐng)求:prepare,chunk, merge,也就是整個(gè)上傳過(guò)程,主要分為三步:1.預(yù)請(qǐng)求 2.分片請(qǐng)求 3.合并請(qǐng)求,預(yù)請(qǐng)求和合并請(qǐng)求就是我們正常的http請(qǐng)求,主要處理的是分片請(qǐng)求,分片請(qǐng)求主要的步驟是:
- 將文件切片
- 構(gòu)造切片請(qǐng)求參數(shù)
- 控制分片請(qǐng)求的并發(fā)
1. 文件切片
在el-upload上傳后, 在on-change屬性的回調(diào)里可以獲取文件file,通過(guò)file.raw.slice對(duì)文件進(jìn)行切片,目前的切片規(guī)則是:1.小于10M 固定一片 2.小于50M 文件10%為一片 3.大于50M 固定5M 一片(可以根據(jù)自己的需求進(jìn)行修改)
genFileChunks(file) {
const chunks = []
let cur = 0
// 小于10M 固定一片
if (file.size < (10 * 1024 * 1024)) {
chunks.push({
index: cur,
file: file.raw.slice(cur, file.size),
originFilename: file.name
})
return chunks
}
// 小于50M 文件10%為一片
if (file.size < (50 * 1024 * 1024)) {
const chunkSize = parseInt(file.size * 0.1)
while (cur < file.size) {
chunks.push({
index: cur,
file: file.raw.slice(cur, cur + chunkSize),
originFilename: file.name
})
cur += chunkSize
}
return chunks
}
// 大于50M 固定5M 一片
const chunkSize = parseInt(5 * 1024 * 1024)
while (cur < file.size) {
chunks.push({
index: cur,
file: file.raw.slice(cur, cur + chunkSize),
originFilename: file.name
})
cur += chunkSize
}
return chunks
},一個(gè)32M的文件按照10%切一片,構(gòu)造好的切片數(shù)據(jù)是這樣的

2. 構(gòu)造切片請(qǐng)求參數(shù)
切片請(qǐng)求不同業(yè)務(wù)的參數(shù)是變化的,所以參數(shù)部分可以拋出給父組件處理,增加組件的復(fù)用性
父組件
<template>
<UploadSlice
:action="uploadInfoSlice.actionChunk"
:headers="uploadInfoSlice.headers"
:on-form-data="genFormData"
/>
</template>
<script>
methods: {
// 構(gòu)造分片參數(shù)
genFormData(chunks, uid) {
const prepareId = this.getCurrentPrepareId(uid)
return chunks.map(chunk => {
const form = new FormData()
form.append('chunk', chunk.file)
form.append('uploadId', prepareId)
form.append('partNumber', chunk.index)
return form
})
},
},
</script>
子組件
<template>
<el-upload
action=""
:accept="accept"
>
</template>
<script>
props: {
onFormData: {
type: Function,
default: () => {}
},
},
methods: {
async uploadChunks(uid) {
// 預(yù)請(qǐng)求
// ---------------
// 上傳切片
const requests = this._genRequest(this._genUploadData(uid), uid)
// 控制并發(fā)
await this.sendRequest(requests)
// 合并請(qǐng)求
// ---------------
},
// 構(gòu)造分片參數(shù)
_genUploadData(uid) {
const chunks = this.getCurrentChunks(uid)
return this.onFormData(chunks, uid)
},
// 生成調(diào)用請(qǐng)求:[Promise, Promise]
_genRequest(uploadData, uid) {
console.log('uploadData', uploadData)
const file = this.getCurrentFile(uid)
const chunks = this.getCurrentChunks(uid)
return uploadData.map((form, index) => {
const options = {
headers: this.$attrs.headers,
file: file,
data: form,
action: this.action,
onProgress: progress => {
chunks[index].progress = Number(
((progress.loaded / progress.total) * 100).toFixed(2)
)
this.handleProgress(progress, file, uid)
}
}
return options
})
},
},
</script>3. 控制分片請(qǐng)求的并發(fā)
切片上傳如果不控制并發(fā),在分片很多時(shí),就會(huì)同時(shí)發(fā)送很多個(gè)http請(qǐng)求,導(dǎo)致線程阻塞,影響頁(yè)面其他請(qǐng)求的操作,所以控制并發(fā)是需要的。我設(shè)置的是最多允許3個(gè)并發(fā)請(qǐng)求。
sendRequest(requests, limit = 3) {
return new Promise((resolve, reject) => {
const len = requests.length
let counter = 0
let isTips = false // 只提示一次失敗
let isStop = false // 如果一個(gè)片段失敗超過(guò)三次 認(rèn)為當(dāng)前網(wǎng)洛有問(wèn)題 停止全部上傳
const startRequest = async() => {
if (isStop) return
const task = requests.shift()
if (task && task.file.status !== 'cancel') {
// 利用try...catch捕獲錯(cuò)誤
try {
// 具體的接口 抽離出去了
await ajax(task)
if (counter === len - 1) { // 最后一個(gè)任務(wù)
resolve()
} else { // 否則接著執(zhí)行
counter++
startRequest() // 啟動(dòng)下一個(gè)任務(wù)
}
} catch (error) {
// 網(wǎng)絡(luò)異常
if (error === 'NETWORK_ERROR' && !isTips) {
Message.error('網(wǎng)絡(luò)異常,文件上傳失敗')
this.upLoading = false
this.preLoading = false
isTips = true
this.handleRemove('', [])
}
// 接口報(bào)錯(cuò)重試,限制為3次
if (task.error < 3) {
task.error++
requests.unshift(task)
startRequest()
} else {
isStop = true
reject(error)
}
}
}
}
// 啟動(dòng)任務(wù)
while (limit > 0) {
// 模擬不同大小啟動(dòng)
setTimeout(() => {
startRequest()
}, Math.random() * 2000)
limit--
}
})
}
}完整代碼
文章只整理了核心代碼,對(duì)于格式數(shù)量限制、進(jìn)度條處理、取消上傳等操作也進(jìn)行了封裝,詳細(xì)請(qǐng)看完整代碼
https://github.com/Xmengling/el-upload-slice
待完善
- 目前還沒(méi)有做斷點(diǎn)續(xù)傳的處理
- 可能還有一些異常邊界情況沒(méi)有考慮完整
到此這篇關(guān)于Vue實(shí)現(xiàn)封裝一個(gè)切片上傳組件的文章就介紹到這了,更多相關(guān)Vue封裝切片上傳組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue.js 中 axios 跨域訪問(wèn)錯(cuò)誤問(wèn)題及解決方法
這篇文章主要介紹了Vue.js 中 axios 跨域訪問(wèn)錯(cuò)誤問(wèn)題及解決方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2018-11-11
Vue刷新后頁(yè)面數(shù)據(jù)丟失問(wèn)題的解決過(guò)程
在做vue項(xiàng)目的過(guò)程中有時(shí)候會(huì)遇到一個(gè)問(wèn)題,就是進(jìn)行F5頁(yè)面刷新的時(shí)候,頁(yè)面的數(shù)據(jù)會(huì)丟失,這篇文章主要給大家介紹了關(guān)于Vue刷新后頁(yè)面數(shù)據(jù)丟失問(wèn)題的解決過(guò)程,需要的朋友可以參考下2022-11-11
使用vue3實(shí)現(xiàn)簡(jiǎn)單的滑塊組件
這篇文章主要給大家介紹一下如何使用vue3實(shí)現(xiàn)簡(jiǎn)單的滑塊組件,文中有詳細(xì)的代碼示例講解,具有一定的參考價(jià)值,感興趣的小伙伴跟著小編一起來(lái)看看吧2023-08-08
Windows系統(tǒng)下使用nginx部署vue2項(xiàng)目的全過(guò)程
nginx是一個(gè)高性能的HTTP和反向代理服務(wù)器,因此常用來(lái)做靜態(tài)資源服務(wù)器和后端的反向代理服務(wù)器,下面這篇文章主要給大家介紹了關(guān)于Windows系統(tǒng)下使用nginx部署vue2項(xiàng)目的相關(guān)資料,需要的朋友可以參考下2023-03-03
vue項(xiàng)目中路徑使用@和~的區(qū)別及說(shuō)明
這篇文章主要介紹了vue項(xiàng)目中路徑使用@和~的區(qū)別及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12

