vue 配合el-upload做大文件切片上传
我看现在很多在使用vue-simpile-upload,如果不用他,手撸一个应该怎么搞
- 首先我们需要对文件md5加密作为文件的唯一标记,后端可以用这个md5做个文件名称或者标记,下次再上传的时候就能知道是否有上传过或者上传过部分切片,读取文件然后加密这里使用sparkMD5
这里要注意我们在对文件进行加密的时候不要用整个文件,否则大文件加密执行很慢,也会引起崩溃,要在截取以后取出第一片进行加密
javascript
npm i spark-md5 -S
fileToBuffer (optionFile) {
// const chunkSize = 1024 * 1024 * 5;//这里是每片要切的大小
const fileReader = new FileReader();// 文件读取类
const chunks = [];//当前文件切的片段集合
return new Promise((resolve, reject) => {//取出第一片计算md5
for (let i = 0; i < optionFile.size; i = i + CHUNK_SIZE) {
const tmp = optionFile.slice(i, Math.min((i + CHUNK_SIZE), optionFile.size))
if (i === 0) {//取出第一片进行加密
fileReader.readAsArrayBuffer(tmp)
}
chunks.push(tmp)
}
fileReader.onload = e => {
resolve({ buffer: e.target.result, chunks })// 返回要加密的片段,和整体的片段集合
}
fileReader.onerror = () => {
reject(new Error('转换文件格式发生错误'))
}
})
}
ruby
html 片段
<el-upload multiple action='' ref="refName" drag :auto-upload="false" :show-file-list="false" :on-change="getPic">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
</div>
</el-upload>
await checkMd5({ md5: hash })是判断当前文件是否已经上传
- 1种已经全部上传完成,那么就直接秒传
- 已经上传过,只是部分切片上传完成,接着继续上传剩下的切片
- 没有上传过,开始切片上传
这种是当前文件已经上传完成接口返回allCompleteFlag为true 这种是已经上传过,但是只是上传了部分 allCompleteFlag=false, completeChunk: [0, 1, 2, 3, 4, 5, 6, 7, 8]为已经上传过的片段,那么剩下的chunks列表就需要上传,我们要过滤出来
scss
filterChunk (backChunk, totalChunk) {//踢出掉已经上传过的切片backChunk为已经上传的片段下标
if (backChunk.length > 0) {
for (let i = backChunk.length - 1; i >= 0; i--) {
totalChunk.splice(backChunk[i], 1)
}
}
return totalChunk //返回剔除掉已经上传的下标
},
kotlin
async getPic (file, fileList) {
let uploaList = fileList.map((item, _) => {
return {
...item,
progress: 0,
md5: '', // 标识
chunks: '', // 总切片数量
chunk: '', // 当前为第几块切片
name: '', // 当前文件名
file: '', // 当前文件
chunkSize: '',
chunkList: [],
show: false,
fileDesc: ''
}
});
this.filesList = [...this.filesList, ...uploaList];
this.$refs.refName.clearFiles()
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw;
let { buffer, chunks } = await this.fileToBuffer(fileObj);
const findIndex = this.filesList.findIndex((item, _) => item.uid == file.uid);
var suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1]; // 文件后缀名
// 根据文件内容生成 hash 值
const spark = new SparkMD5.ArrayBuffer();
const hash = spark.append(buffer).end();//MD5加密
// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
// let curChunk = 0; // 切片时的初始位置
for (let i = 0; i < chunks.length; i++) {
const item = {
// chunk: fileObj.slice(curChunk, curChunk + chunkSize),
chunk: chunks[i],
fileName: `${hash}.${suffix}`, // 文件名规则按照 hash_1.jpg 命名
dqchunk: i
}
this.filesList[findIndex].chunkList.push(item)
}
this.filesList[findIndex].name = `${hash}.${suffix}`
this.filesList[findIndex].md5 = hash;
this.filesList[findIndex].chunks = chunks.length;
let { data } = await checkMd5({ md5: hash });
// { completeChunk, allCompleteFlag, file } =data
//completeChunk=[1,2,5....]这里是返回当前md5文件已经上传的
if (data.allCompleteFlag) {//已经上传完了的,直接妙传
this.filesList[findIndex].progress = 100;
this.filesList[findIndex] = {
...this.filesList[findIndex],
...data.file,
}
} else {//
if (data.completeChunk.length > 0) {//已上传了部分切片
this.filesList[findIndex].progress = data.completeChunk.length / this.filesList[findIndex].chunks * 100;
this.filesList[findIndex].chunkList = this.filterChunk(data.completeChunk, this.filesList[findIndex].chunkList);
}
this.sendRequest(findIndex)
}
},
将所有切片循环上传放到一个请求集合种,我这里传入findIndex是方便多个文件上传,找到对应上传的文件进行progress 进度条的处理,如果已经上传部分就要将已上传的百分比先赋值给对应下标的progress,中断上传再次上传就可以显示上次上传的百分比继续进行剩余切片上传
kotlin
sendRequest (findIndex) {
const requestList = []// 请求集合
this.filesList[findIndex].chunkList.forEach((item, index) => {
const fn = () => {
const formData = new FormData()
formData.append('chunk', item.dqchunk)
formData.append('fileName', item.fileName)
formData.append('file', item.chunk)
formData.append('md5', this.filesList[findIndex].md5)
formData.append('chunks', this.filesList[findIndex].chunks)
formData.append('chunkSize', CHUNK_SIZE);
return httpSliceUpload(formData).then(({ data }) => {
this.filesList[findIndex].progress += 1 / this.filesList[findIndex].chunks * 100 // 改变进度;
data.file && (this.filesList[findIndex] = {
...this.filesList[findIndex],
...data.file
});
this.filesList[findIndex].chunkList.splice(index, 1) // 一旦上传成功就删除这一个
})
}
requestList.push(fn)
})
this.requestLimit(requestList, 3)//控制并发量
},
上传的时候控制并发数量
scss
async requestLimit (requestList, limits) {
const promises = []
// 当前的并发池,用Set结构方便删除
const pool = new Set() // set也是Iterable<any>[]类型,因此可以放入到race里
// 开始并发执行所有的任务
for (let request of requestList) {
// 开始执行前,先await 判断 当前的并发任务是否超过限制
if (pool.size >= limits) {
// 这里因为没有try catch ,所以要捕获一下错误,不然影响下面微任务的执行
await Promise.race(pool)
.catch(err => err)
}
const promise = request()// 拿到promise
// 删除请求结束后,从pool里面移除
const cb = () => {
pool.delete(promise)
}
// 注册下then的任务
promise.then(cb, cb)
pool.add(promise)
promises.push(promise)
}
Promise.allSettled(promises);
},
上传的请求,cancel函数的调用可以终止上传
javascript
import {getToken} from '@/utils/auth'
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
const {CancelToken} = axios; //CancelToken能为一次请求
let cancel函数的调用可以终止上传;
const httpSliceUpload = (formData) => {
return new Promise((resolve, reject) => {
axios({
headers: {
token: getToken(),
'Content-Type': 'multipart/form-data',
},
url: process.env.VUE_APP_BASE_API + '/client/sliceUploadFile',
method: 'post',
cancelToken: new CancelToken((c)=>{ //c是一个函数,调用c就可以关闭本次请求
cancel = c;
}),
data: formData,
}).then(({data}) => {
if(data.code>0){
if(data.code==401){
MessageBox.confirm('您已登出,您可以取消以留在此页面,或重新登录', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}else{
Message({
message: data.msg || 'Error',
type: 'error',
duration: 5 * 1000
});
return Promise.reject(new Error(data.msg || 'Error'))
}
}else{
resolve(data)
}
}).catch(err=>{
if (axios.isCancel(err)) {
console.log('请求被取消');
} else {
// 处理其他错误
reject(err)
throw new Error(err)
}
})
})
}
export {httpSliceUpload,cancel}
最终的效果,已上传的就秒传,直接显示文件,视频,图片等,没上传完的显示当前正在上传的进度,和进行剩余的切片上传。我们这里是前端告诉后端总共有几片将每个切片进行编号,就是固定下标,上传全部完成,后端通过已上传的切片数量和总切片对比是否一致,如果已经上传完后端直接合并各个切片
也有一些是前端上传完所有切片,通知后端再去合并,反正原理都是一样
取消上传,终止当前的请求