因为工作需要这个东西经常要用到,记录下一个小demo
<div class="file-label" @click="handleUpload">
<slot>
<div class="tips-box">
<el-button>
上传文件
</el-button>
<div class="progress" :style="{ width: uploadProgress + '%' }"></div>
</div>
</slot>
<input type="file" name="file" id="fileid" ref="inputFile" class="file-input" @change="handleInputFileChange" />
</div>
</template>
<style lang="scss" scoped>
.file-label {
display: inline-block;
}
.file-input {
display: none;
}
.tips-box {
position: relative;
display: inline-block;
}
.progress {
position: absolute;
left: 0;
bottom: 0;
width: 0%;
height: 3px;
background-color: #409eff;
}
</style>
<script>
import axios from 'axios'
import BMF from 'browser-md5-file'
export default {
name: 'MChunkUpload',
data() {
return {
file: {},
hashProgress: 0,
uploadProgress: 0
}
},
props: {
chunkSize: {
type: Number,
default: 1
},
requestLimit: {
type: Number,
default: 3
},
customField: {
type: Array,
default: function () {
return []
}
}
},
methods: {
// 点击按钮,模拟点击input[type="file"]
handleUpload() {
this.$refs.inputFile.click()
},
// 选定文件,触发上传
handleInputFileChange(event) {
const file = event.target.files[0]
const bmf = new BMF()
bmf.md5(
file,
(err, md5) => {
if (err !== null) console.error(err)
// 上传文件
this.chunkFileUpload(file, md5)
},
progress => {
this.hashProgress = Math.ceil(progress * 100)
this.uploadProgress = 0
this.$emit('hashProgress', this.hashProgress)
}
)
// 清空,防止文件不能重复上传
event.target.value = ''
},
// 服务器检测文件是否已上传及上传过哪些分片
async chunkFileCheck(md5, chunks) {
return this.http().get('/file/check_file', {
params: {
hash: md5,
chunks: chunks
}
}).then(r => {
const result = r.data
if (result.errno !== 0) {
console.error(result.errmsg)
return []
}
if (result.data.code === 1) {
this.$emit('successed', result.data.data)
this.uploadProgress = 100
this.$emit('uploadProgress', 100)
return [...new Array(chunks)].map((it, k) => k)
}
return result.data.data.uploaded
})
},
// 计算文件分片数
async chunkFileUpload(file, hash) {
let uploadedProgress = 0
const chunkSize = this.chunkSize * 1024 * 1024
const fileSize = file.size
// 计算文件分片总数
const chunks = Math.ceil(fileSize / chunkSize)
// 分片组大小
const groupSize = this.requestLimit
// 分片索引
let chunksArray = [...new Array(chunks)].map((it, k) => k)
// 检查文件是否上传过
const uploaded = await this.chunkFileCheck(hash, chunks)
chunksArray = chunksArray.filter((it, k) => uploaded.includes(it) === false)
if (chunksArray.length === 0) {
return false
}
// 分片索引分组,解决浏览器同域名最大并发请求数问题
const chunksGroups = [...new Array(Math.ceil(chunksArray.length / groupSize))].map((it, k) => chunksArray
.slice(k * groupSize, k * groupSize + groupSize))
for (let i = 0; i < chunksGroups.length; i++) {
let computed = 0
// 利用promise控制并发请求连接数
await new Promise((resolve, reject) => {
const group = chunksGroups[i]
for (let k = 0; k < group.length; k++) {
const it = group[k]
// 分片文件
const sPos = chunkSize * it
const chunk = file.slice(sPos, sPos + chunkSize)
const formData = new FormData()
// blob组装file对象
formData.append('file', new File([chunk], file.name, {
type: file.type
}), file.name)
formData.append('hash', hash)
formData.append('chunks', chunks)
formData.append('chunk', it)
// 自定义字段
if (this.customField.length > 0) {
this.customField.forEach(field => {
formData.append(Object.keys(field)[0], Object.values(field)[0])
})
}
this.http().post('/file/upload_file', formData).then(r => {
computed += 1
if (computed === group.length) {
resolve('success')
}
// 计算上传进度
this.uploadProgress = Math.ceil((uploadedProgress += 1) / chunks * 100)
this.$emit('uploadProgress', this.uploadProgress)
const result = r.data
// 失败,请取消请求
if (result.errno !== 0) {
console.error(result.errmsg)
return false
}
// 上传完成
if (result.data.code === 1) {
this.$emit('successed', result.data.data)
this.uploadProgress = 100
this.$emit('uploadProgress', 100)
return false
}
}).catch(e => {
reject(e)
})
}
})
}
},
// 请求配置项
http() {
const baseURL = process.env.VUE_APP_API_BASEURL
const apiVersion = process.env.VUE_APP_API_VERSION.replace(/\./g, '_')
return axios.create({
baseURL: baseURL + apiVersion + '/',
timeout: 0,
withCredentials: true
})
}
}
}
</script>