如果文件很大, 会需要分片上传, 原理是用File.prototype.slice或者Blob.prototype.slice去切分二进制的文件数据, 然后用FileReader读取这些数据片段, 然后获取这些片段的md5, 然后开始传输, 如果需要merge, 后端会返回对应的相应, 前端再请求merge的接口, 将分开的多个文件片段merge成一个文件.
使用插件: vue-simple-uploader
下面是封装好的组件:
<template>
<div>
<div id="status" style="position:relative;"></div>
<uploader ref="uploader"
:options="totalOptions"
:autoStart="true"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-progress="onFileProgress"
@file-error="onFileError">
<uploader-drop>
<p style="margin:0">选择上传文件类型</p>
<uploader-btn style="margin-right:10px" :attrs="totalOptions.attrs">选择上传文件</uploader-btn>
<uploader-btn :directory="true" v-if="totalOptions.selectDirectory">选择上传文件夹</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>
</div>
</template>
<script>
import SparkMD5 from 'spark-md5'
import { uploadFileMerge } from '@/api/uploader'
import $ from 'jquery'
import { getToken } from '@/utils/auth'
export default {
props: {
options: {
type: Object,
default () {
return {}
}
}
},
data () {
return {
defaultOptions: {
target: window.location.origin + window.location.pathname + 'xxx',
chunkSize: 1 * 1024 * 1000,
fileParameterName: 'file',
maxChunkRetries: 2,
testChunks: true, // 是否开启服务器分片校验
checkChunkUploadedByResponse: function (chunk, message) {
// 服务器分片校验函数,秒传及断点续传基础
const objMessage = JSON.parse(message)
if (objMessage.skipUpload) {
return true
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
multiple: false,
headers: {
'Auth-Token': getToken()
},
query () {
},
attrs: {
accept: [] //可上传的文件类型
}
}
}
},
methods: {
onFileAdded (file) {
//清空文件列表, 目的是让用户感知到只可以上传一个文件
if (!this.totalOptions.multiple) {
this.uploader.fileList = []
this.uploader.fileList.push(file)
}
console.log(file, 'file=====')
this.computeMD5(file)
},
onFileProgress (rootFile, file, chunk) {
console.log('... onFileProgress')
},
onFileSuccess (rootFile, file, response, chunk) {
const res = JSON.parse(response)
console.log('... onFileSuccess %o', res)
console.log(this.uploader, 'uploader')
// 如果服务端返回需要合并
if (res.needMerge) {
console.log('... onFileSuccess needMerge')
// 文件状态设为“合并中”
this.statusSet(file.id, 'merging')
const param = {
'filename': rootFile.name,
'identifier': rootFile.uniqueIdentifier,
'totalSize': rootFile.size
}
uploadFileMerge(param).then(res => {
this.statusRemove(file.id)
console.log('... onFileSuccess merge done')
this.$emit('uploadSuccess', res.data)
}).catch(e => {
console.log('合并异常,重新发起请求,文件名为:', file.name)
file.retry()
})
} else {
this.$emit('uploadSuccess', res.data)
}
},
onFileError (rootFile, file, response, chunk) {
console.log('... onFileError')
},
computeMD5 (file) {
const fileReader = new FileReader()
const time = new Date().getTime()
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
let currentChunk = 0
const chunkSize = 10 * 1024 * 1000
const chunks = Math.ceil(file.size / chunkSize)
const spark = new SparkMD5.ArrayBuffer()
// 文件状态设为"计算MD5"
this.statusSet(file.id, 'md5')
file.pause()
loadNext()
fileReader.onload = e => {
spark.append(e.target.result)
if (currentChunk < chunks) {
currentChunk++
loadNext()
// 实时展示MD5的计算进度
this.$nextTick(() => {
$(`.myStatus_${file.id}`).text('校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%')
})
} else {
const md5 = spark.end()
this.computeMD5Success(md5, file)
console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`)
}
}
fileReader.onerror = function () {
this.error(`文件${file.name}读取出错,请检查该文件`)
file.cancel()
}
function loadNext () {
const start = currentChunk * chunkSize
const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
console.log('[loadNext] currentChunk=%d start=%d end=%d', currentChunk, start, end)
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
}
},
statusSet (id, status) {
const statusMap = {
md5: {
text: '校验MD5',
bgc: '#fff'
},
merging: {
text: '合并中',
bgc: '#e2eeff'
},
transcoding: {
text: '转码中',
bgc: '#e2eeff'
},
failed: {
text: '上传失败',
bgc: '#e2eeff'
}
}
console.log('.....', status, '...:', statusMap[status].text)
this.$nextTick(() => {
$(`<p class="myStatus_${id}"></p>`).appendTo(`#status`).css({
'position': 'absolute',
'top': '0',
'left': '0',
'right': '0',
'bottom': '0',
'zIndex': '1',
'line-height': 'initial',
'backgroundColor': statusMap[status].bgc
}).text(statusMap[status].text)
})
},
computeMD5Success (md5, file) {
Object.assign(this.uploader.opts, {
query: {
...this.params
}
})
file.uniqueIdentifier = md5
file.resume()
this.statusRemove(file.id)
},
statusRemove (id) {
this.$nextTick(() => {
$(`.myStatus_${id}`).remove()
})
}
},
computed: {
// Uploader实例
uploader () {
return this.$refs.uploader.uploader
},
totalOptions () {
return { ...this.defaultOptions, ...this.options }
}
}
}
</script>