简易文件切片上传
- axios 并发控制
- 文件切片
- 文件唯一性校验
spark-md5
- 拖拽上传
<template>
<div class="b-upload">
<input
v-show="false"
type="file"
ref="upload"
:multiple="multiple"
@change="handleChange"
/>
<div>
<slot>
<!-- 默认内容 -->
<div
:class="['drag-box', isEnter ? 'is-enter' : '']"
v-if="drag"
@click="handleUpload"
@drop.prevent="handleDrop"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
></div>
<BButton v-else>
文件上传
</BButton>
</slot>
<BButton @click.native.stop="handleBtn">{{
isUpload ? '暂停' : '继续'
}}</BButton>
<BButton v-if="isErrorReUpload" @click="checkIsError">错误重传</BButton>
</div>
</div>
</template>
<script>
import BButton from '../../b-button'
import axios from 'axios'
import SparkMd5 from 'spark-md5'
export default {
name: 'BUpload',
components: { BButton },
props: {
action: {
type: String,
default: '',
},
headers: {
type: Object,
default: () => ({}),
},
data: {
type: Object,
default: () => ({}),
},
multiple: {
type: Boolean,
default: true,
},
autoUpload: {
type: Boolean,
default: true,
},
showList: {
type: Boolean,
default: true,
},
type: {
type: String,
default: 'parallel',
},
drag: {
type: Boolean,
default: true,
},
showStop: {
type: Boolean,
default: false,
},
},
data: () => ({
file: {},
complete: 0,
source: {},
isEnter: false,
isUpload: true,
isContinue: false,
isError: false,
isErrorReUpload: false,
errorRequest: [],
total: 0,
current: 0,
chunkLis: [],
requestList: [],
hash: '',
suffix: '',
img: '',
max: 5,
currentReqNumber: 0,
startIndex: 0,
blockQueue: [],
sources: [],
completes: [],
successReq: [],
doneReq: 0,
reqTotal: 0,
timer: 0,
}),
computed: {
process() {
return parseInt((this.current / this.total) * 100 || 0) + '%'
},
},
mounted() {
if (this.$slots.default) {
this.$slots.default[0].elm.addEventListener('click', this.handleUpload)
}
},
methods: {
handleUpload() {
this.$refs.upload.click()
},
handleDrop(e) {
if (this.isEnter) {
this.file = e.dataTransfer.files[0]
this.handleChange()
}
this.file = e.dataTransfer.files[0]
},
handleDragEnter() {
console.log('handleDragEnter')
},
handleDragOver() {
this.isEnter = true
console.log('handleDragOver')
},
handleDragLeave() {
this.isEnter = false
console.log('handleDragLeave')
},
async handleChangeSingle() {
this.$emit('change', this.$refs.upload.files)
if (this.action && this.autoUpload) {
this.source = axios.CancelToken.source()
const forms = new FormData()
for (let f of this.$refs.upload.files) {
forms.append('file', f)
}
for (let i in this.data) {
forms.append(i, this.data[i])
}
try {
const result = await axios({
method: 'post',
url: this.action,
headers: this.headers,
data: forms,
cancelToken: this.source.token,
onUploadProgress: (progressEvent) => {
this.complete =
((progressEvent.loaded / progressEvent.total) * 100) | 0
this.$emit('progress', this.complete)
},
})
this.$refs.upload.value = ''
this.$emit('success', result.data)
} catch (error) {
this.$emit('error', error)
}
}
},
handleStop() {
this.source.cancel('取消上传')
this.source = axios.CancelToken.source()
},
async handleChange() {
const file = this.$refs.upload.files[0] || this.file
if (file) {
this.current = 0
this.completes = []
this.timer = Date.now()
this.$emit('change', this.$refs.upload.files)
const fBuffer = await this.fileParse(file, 'buffer')
const spark = new SparkMd5.ArrayBuffer()
spark.append(fBuffer)
this.hash = spark.end()
this.suffix = /.([0-9a-zA-Z]+)$/.exec(file.name)[0]
this.chunkList = this.createFileChunk(file, this.hash, this.suffix)
this.createRequestList()
await this.sendRequest()
}
},
async handleBtn() {
this.isUpload = !this.isUpload
if (!this.isUpload) {
console.log('停止上传')
this.requestList.map((req) => {
if (req.status === false) {
console.log('req: ', req.index)
console.log('this.sources[req.index]: ', this.sources[req.index])
this.sources[req.index].cancel('取消请求')
}
})
}
if (this.isUpload) {
this.requestList.map((req) => {
if (req.status === false) {
this.sources[req.index] = axios.CancelToken
}
})
}
if (this.isUpload && this.type === 'serial') {
console.log('重新开始-串行')
this.isError = true
await this.checkIsError()
}
if (this.isUpload && this.type === 'parallel') {
console.log('重新开始-并行')
this.isError = true
await this.checkIsError()
}
},
async checkIsError() {
this.isErrorReUpload = false
this.isUpload = true
const errorList = this.requestList.filter((req) => req.status === false)
this.reqTotal = errorList.length
this.requestList.map((req) => {
if (req.status === false) {
this.sources[req.index] = axios.CancelToken
}
})
if (errorList.length > 0 && this.type === 'serial') {
console.log('串行错误处理')
this.doneReq = 0
for (let req of errorList) {
if (!this.isUpload) return
if (this.current >= this.requestList.length) {
this.current = this.requestList.length
return
}
await req.request()
}
} else if (errorList.length > 0 && this.type === 'parallel') {
console.log('并行错误处理')
this.doneReq = 0
this.blockQueue = []
for (let i = 0; i < errorList.length; i++) {
if (this.currentReqNumber >= this.max) {
await new Promise((res) => this.blockQueue.push(res))
}
errorList[i].request()
}
} else {
this.fileMerge()
}
},
async fileMerge() {
const { data } = await axios({
url: 'http://localhost:5000/assets/fileMerge',
method: 'post',
data: {
fileName: `${this.data.filePath}/${this.hash}${this.suffix}`,
},
})
this.$emit('success', data)
this.resetFile()
console.log('upload', Date.now() - this.timer)
},
async sendRequest() {
this.reqTotal = this.requestList.length
if (this.type === 'serial') {
console.log('sendRequest串行请求')
const send = async () => {
if (!this.isUpload) return
if (this.current >= this.requestList.length) {
this.current = this.requestList.length
return
}
await this.requestList[this.current].request()
send()
}
send()
} else if (this.max > 0 && this.type === 'parallel') {
console.log('sendRequest并行请求')
for (let i = 0; i < this.requestList.length; i++) {
if (this.currentReqNumber >= this.max) {
await new Promise((res) => this.blockQueue.push(res))
}
this.requestList[i].request()
}
}
},
createRequestList() {
this.chunkList.map((params, index) => {
this.sources[index] = axios.CancelToken.source()
const request = async () => {
const fd = new FormData()
for (let i in params) {
fd.append(i, params[i])
}
try {
this.currentReqNumber++
const result = await axios({
url: this.action,
method: 'post',
data: fd,
headers: this.headers,
cancelToken: this.sources[index].token,
onUploadProgress: (p) => {
this.completes[index] = ((p.loaded / p.total) * 100) | 0
this.requestList[index].process =
((p.loaded / p.total) * 100) | 0
this.$emit('progress', [this.process, this.completes])
},
})
this.current++
this.doneReq++
this.chunkList.splice(index, 1)
this.requestList[index].status = true
if (this.current >= this.total) {
this.isError = false
this.isErrorReUpload = false
this.fileMerge()
this.$emit('progress', [this.process, this.completes])
}
return result
} catch (error) {
console.log('error: ', '开始出错了', error)
this.doneReq++
this.isUpload = false
this.isErrorReUpload = true
} finally {
if (this.type === 'parallel') {
this.currentReqNumber--
if (this.blockQueue.length > 0) {
this.blockQueue[0]()
this.blockQueue.shift()
}
}
if (this.doneReq >= this.reqTotal && this.isError) {
console.log('全部完成--去处理错误--')
this.isError = false
this.isErrorReUpload = true
this.checkIsError()
this.$emit('progress', [this.process, this.completes])
}
}
}
this.requestList.push({ index, status: false, process: 0, request })
})
},
createFileChunk(file, hash, suffix, size = 1024 * 1024) {
const chunkList = []
const total = Math.ceil(file.size / size)
this.total = total
let index = 0
for (let i = 0; i < total; i++) {
index = i * size
let chunk = file.slice(index, (i + 1) * size)
chunkList.push({
index: i,
chunk,
hash,
fileName: `${this.data.filePath}/${hash}/${hash}_${i}${suffix}`,
})
}
return chunkList
},
fileParse(file, type = 'base64') {
return new Promise((resolve) => {
const fileRead = new FileReader()
if (type === 'base64') {
fileRead.readAsDataURL(file)
} else if (type === 'buffer') {
fileRead.readAsArrayBuffer(file)
}
fileRead.onload = (e) => {
resolve(e.target.result)
}
})
},
resetFile() {
this.complete = 0
this.source = {}
this.isUpload = true
this.errorRequest = []
this.chunkLis = []
this.requestList = []
this.hash = ''
this.suffix = ''
this.img = ''
this.max = 5
this.currentReqNumber = 0
this.startIndex = 0
this.blockQueue = []
this.sources = []
this.successReq = []
this.doneReq = 0
this.$refs.upload.value = ''
},
},
}
</script>
<style scoped>
.b-upload {
}
.drag-box {
width: 300px;
height: 300px;
border: 4px dashed #ccc;
border-radius: 20px;
transition: all 0.3s;
}
.is-enter {
border-color: #409eff;
}
.chunk-box {
margin: 0;
padding: 0;
list-style: none;
width: 200px;
display: flex;
flex-wrap: wrap;
}
.chunk-box li {
position: relative;
width: 20px;
height: 20px;
background-color: #f0f0f0;
}
.chunk-box li div {
position: absolute;
bottom: 0;
left: 0;
width: 20px;
height: 0px;
transition: all 0.3s;
background-color: greenyellow;
}
</style>
··