一般上传文件时会因为ngnix对上传文件大小进行了限制、文件过大接口请求超时导致上传失败,此时可以考虑分片上传,分片就是说将文件拆分来进行上传,将各个文件的切片传递给后台,然后后台再进行合并(合并操作可由前端触发,也可由后端触发,本文由前端触发),切片就是文件切分字节(类似于字符串的截取)。
上传文件时需要一个文件的唯一标识,在上传文件过程中需要将文件的唯一标识(文件的唯一标识__该文件的MD5加密文件的字符串)传递给后端,后端会通过该标识返回给我们该上传文件目前的上传状态:
1、文件已存在,文件已经上传并且合并完成,此时我们无需再进行任何操作。
2、全部上传未合并,此时文件全部上传完成,我们需要触发合并操作。
3、部分上传或者尚未上传,把需要上传的分片进行上传,上传完成检测是否完全上传,完全上传触发合并
实现目标:
- 简单封装分片上传组件
- 可在其他vue的页面应用使用
- 手动触发上传
实现方式
- 新建vue文件,单独封装该上传组件
- 在vue页面触发使用
1、安装spark-md5
文件的MD5唯一标识,可通过spark-md5来获取,具体步骤:
npm i spark-md5 --save
2、单独封装切片所需js文件(本文起名为chunkFile.js),获取文件的唯一MD5标识码、切片大小等
import SparkMD5 from 'spark-md5'
/**
* 获取文件MD5唯一标识码
* @param file
* @returns {Promise<unknown>}
*/
export function getFileMd5(file, chunkCount) {
return new Promise((resolve, reject) => {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
const chunks = chunkCount
let currentChunk = 0
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
fileReader.onload = function(e) {
spark.append(e.target.result)
currentChunk++
if (currentChunk < chunks) {
loadNext()
} else {
const md5 = spark.end()
resolve(md5)
}
}
fileReader.onerror = function(e) {
reject(e)
}
function loadNext() {
const start = currentChunk * chunkSize
let end = start + chunkSize
if (end > file.size) {
end = file.size
}
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
}
loadNext()
})
}
// 切片大小,自定义
export const chunkSize = 5 * 1024 * 1024
3、封装分片上传组件(本文起名ChunksUpload.vue)
<template>
<div>
<el-input v-model="filename" readonly="readonly" maxlength="50" autocomplete="off" @click.native="$refs.fileUpload.click()">
<el-button slot="append" icon="el-icon-upload" />
</el-input>
<input ref="fileUpload" type="file" style="display:none" @change="fileChange">
</div>
</template>
<script>
import { chunkSize, getFileMd5 } from './chunkFile.js'
import request from './request.js' //此处为封装的axios,根据自己情况替换
export default {
name: 'ChunksUpload',
props: {
value: {
type: String,
default: ''
},
uploadUrl: { //文件上传服务地址
type: String,
default: ''
},
chunksUrl: { //检测文件上传服务地址
type: String,
default: ''
},
mergeUrl: { //合并文件服务地址
type: String,
default: ''
},
formData: { //上传过程中额外需要提交的参数
type: Object,
default: function() {
return {}
}
}
},
data() {
return {
filename: '',
uploadFile: null
}
},
watch: {
value(val) {
this.filename = val
}
},
methods: {
/**
* @description: 上传文件发生变化时触发
* @param {*}
* @return {*}
* @author: wangshunyao
*/
fileChange() {
if (!(this.$refs.fileUpload.files[0]?.name.endsWith('.zip'))) {
this.$message.error('请选择扩展名为.zip的文件包')
return
}
this.uploadFile = this.$refs.fileUpload.files[0]
this.filename = this.uploadFile?.name
this.$emit('input', this.filename)
},
async CheckChunckInfo() {
const file = this.uploadFile
// 校验已上传上传文件是否正确
if (!this.filename.endsWith('.zip')) {
this.$message.warning('请上传扩展名为.zip的压缩包')
return
}
const checkloading = this.$loading({
lock: true,
text: '上传文件检测中,请等待...',
background: 'rgba(0, 0, 0, 0.7)'
})
const fileSize = file.size
const chunkCount = Math.ceil(fileSize / chunkSize)
const fileMd5 = await getFileMd5(file, chunkCount)
const params = {
'identifier': fileMd5 // 文件的md5
//此处可添加额外需要参数
}
try {
const response = await request({ url: this.chunksUrl, method: 'post', params })
checkloading.close()
if (response.code !== 0) {
this.$message.warning(response?.msg)
return
}
const res = response.data
if (!res.exist) {
// 如果文件不存在则继续走上传接口
this.beforeUpload(res, chunkCount, fileMd5, file)
} else {
this.$message.warning(file.name + '文件已存在,请选择其他文件')
return
}
} catch (e) {
checkloading.close()
}
},
async beforeUpload(res, chunkCount, fileMd5, file) {
// 获取后端返回的已上传分片数字的数组
const uploaded = res.chunks
const totalChunk = Array.from(Array(chunkCount).keys(), n => n + 1)
const notUpload = totalChunk.filter(v => !uploaded.includes(v))
const cycleCompute = []
// 如果还有没有上传过的切片继续上传
// 此处将未上传的切片按照每5个为一组进行分组,为后面每次批量上传的5个切片做准备,该数量可根据自己情况修改
if (notUpload.length) {
notUpload.reduce(function(pre, item, index, notUpload) {
var begin = index * 5
var end = begin + 5
var res = notUpload.slice(begin, end)
if (res.length != 0) {
cycleCompute[index] = res
}
}, [])
} else {
// 如果都上传完了就合并
const mergeloading = this.$loading({
lock: true,
text: '文件合并中,请等待...',
background: 'rgba(0, 0, 0, 0.7)'
})
const mergeParams = {
'identifier': fileMd5, // 文件的md5
'totalChunks': chunkCount, // 分片总量
...this.formData //额外需要上传的参数
}
try {
const mergeResponse = await request({
url: this.mergeUrl,
method: 'post',
params: mergeParams
}
})
mergeloading.close()
if (mergeResponse.code !== 0) {
this.$message.warning(mergeResponse?.msg)
return
}
this.$emit('success', true)
return
} catch (e) {
mergeloading.close()
return
}
}
// 循环调用上传
if (cycleCompute.length) {
const loading = this.$loading({
lock: true,
text: '正在上传,请等待...',
background: 'rgba(0, 0, 0, 0.7)'
})
for (let c = 0; c < cycleCompute.length; c++) {
const reqArr = []
const cycleItem = cycleCompute[c]
for (let n = 0; n < cycleItem.length; n++) {
// 定义分片开始上传的序号
const i = cycleItem[n] - 1
const start = i * chunkSize
const end = Math.min(file.size, start + chunkSize)
const _chunkFile = file.slice(start, end)
const formDataFile = new FormData()
const params = {
identifier: fileMd5,
chunkNum: cycleItem[n], // 当前分片号
'totalChunks': chunkCount, // 分片总量
fileName: file.name,
...this.formData //额外需要上传的参数
}
formDataFile.append('file', _chunkFile)
reqArr.push(this.insertReq(params, formDataFile))
}
try {
//批量上传文件,等一批结束再上传下一批
const responselist = await Promise.all(reqArr)
const errprRes = responselist.filter(item => { return !item || item.code != 0 })
if (errprRes.length) {
loading.close()
errprRes.forEach(resItem => {
this.$message.error(resItem.msg)
})
return
}
} catch (e) {
loading.close()
return
}
}
loading.close()
}
// 都上传完检测下是否都上传完成,没有上传完成继续上传,上传完了就合并
this.CheckChunckInfo()
},
insertReq(params, formDataFile) {
return this.$http.post(this.uploadUrl, formDataFile, {params })
}
}
}
</script>
4、引入上传组件,手动触发上传
<template>
<div>
<el-form ref="appTemplateFormRef" label-width="140px" label-position="right" :rules="appTemplateFormRules" :model="appTemplateForm">
<el-form-item label="算法应用模板" prop="archive">
<chunksUpload
ref="chunksUploadRef"
v-model="appTemplateForm.archive"
@success="successCallback"
/>
</el-form>
<el-button type="primary" @click="submitAppTemplateForm">确定</el-button>
</div>
</template>
<script>
import chunksUpload from './chunksUpload'
export default {
data(){
return{
appTemplateForm:{
archive:''
}
}
},
methods: {
submitAppTemplateForm(){
this.$refs.chunksUploadRef.CheckChunckInfo()
},
successCallback() {
this.$message.success('上传成功!')
},
}
</script>