基于iView Upload组件实现文件分片上传

198 阅读2分钟

目标

表单中实现文件分片上传

实现思路

阻止Upload组件的默认上传,将文件流按照大小分割成数组,再逐条将其创建成单个文件进行上传。

具体实现

阻止默认上传

iView的Upload组件上传文件之前的钩子是before-upload,参数是上传的文件,通过返回 false 或者 Promise 停止上传。

Upload组件默认生成的uid为时间戳,可以使用md5加密为文件的唯一标识。

<Upload :before-upload="handleUpload">
</Upload>

......

handleUpload(file){
    file.id = CryptoJs.MD5(file.uid).toString(); // 生成id
    this.fileCutting(file); // 文件切片
    return false; // 阻止默认上传
},

文件切片

使用file.slice()方法将文件按照分片的大小切片,并存入数组中。

fileCutting(file){
    let chunkList = []; // 存储分片的文件流数据
    let chunkSize = 1 * 1024 * 1024; // 分片大小1M
    let startIndex = 0, endIndex = Math.min(chunkSize, file.size); // 切片开始位置,切片结束位置
    do {    // 文件分片
        let blob = file.slice(startIndex, endIndex); // 文件流截取
        startIndex += chunkSize; // 开始位置后移
        endIndex = Math.min(endIndex + chunkSize, file.size); // 结束位置后移
        if(!blob.size){ // 如果截取不到了则说明已完成分片
          break;
        }
        chunkList.push(blob)
    } while (true);
    this.fileChunkHandle(chunkList, file); // 分片完成,对每个切片的参数进行处理
},

文件上传

  1. 处理接口传参

将二进制文件流转换成File格式,并将接口所需参数转换成FromData格式;遍历切片数组,逐条发送请求。

async fileChunkHandle(chunkList, file){
    let chunks = chunkList.length;// 切片个数
    let fileName = file.name;// 文件名
    let fileType = this.getFileType(fileName); // 根据后缀获取文件类型
    let retry = 3; // 失败重试次数
    
    let formData = new FormData();// 参数表单
    let params = {
        ...this.uploadData,// 接口所需的其他参数
        chunks,
        chunk: null,
        name: file.name,
        uuid: file.id,
        file: null,
    }
    for(let key in params){
        formData.append(key, params[key]);
    };
    
    for(let i = 0; i < chunkList.length; i++){
        let fileChunk = new File(chunkList[i], fileName, {type: fileType});
        formData.set("file",fileChunk);
        formData.set("chunk",i);
        await this.uploadFile(formData, i, chunks, retry + 1);
    }
},
// 通过文件后缀获取文件类型
getFileType(name){
    let splitNameList = name.split(".");
    return splitNameList[splitNameList.length - 1];
},
  1. 调用接口上传文件
uploadFile(formData, chunk, chunks, tryCount){ // 接口参数,切片索引,切片总数,失败尝试次数
    return new Promise(resolve =>{
        $.ajax({
            url: this.url,
            data: formData,
            contentType: false,
            cache: false,
            processData: false,
            type: 'post',
            success: res =>{
                if(chunk < chunks - 1){
                    resolve()
                } else if(chunk == chunks - 1){// 已全部上传完毕
                    console.log("success"); // 成功
                }
            },
            error: err =>{
                if(tryCount <= 1){
                    console.log("error"); // 失败
                } else {
                    this.uploadFile(formData, chunk, chunks, tryCount-1)
                }
            }
        })
    })
}

完整代码

<template>
  <div id="app">
    <Upload ref="upload" 
        :before-upload="handleUpload"
        :action="url">
        <Button>文件上传</Button>
    </Upload>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      url: "http://localhost:8081/file",
      uploadData: {},
    };
  },
  methods: {
    handleUpload(file){
        file.id = CryptoJs.MD5(file.uid).toString(); // upload组件默认生成的uid为时间戳,使用md5加密为唯一id
        this.fileCutting(file); // 文件切片
        return false; // 阻止默认上传
    },
    fileCutting(file){
        let chunkList = []; // 存储分片的文件流数据
        let chunkSize = 1 * 1024 * 1024; // 分片大小1M
        let startIndex = 0, endIndex = chunkSize; // 切片开始位置,切片结束位置
        do {    // 文件分片
            let blob = file.slice(startIndex, endIndex); // 文件流截取
            startIndex += chunkSize; // 开始位置后移
            endIndex += chunkSize; // 结束位置后移
            if(!blob.size){ // 如果截取不到了则说明已完成分片
              break;
            }
            chunkList.push(blob)
        } while (true);
        this.fileChunkHandle(chunkList, file); // 分片完成,对每个切片的参数进行处理
    },
    async fileChunkHandle(chunkList, file){
        let chunks = chunkList.length;// 切片个数
        let fileName = file.name;// 文件名
        let fileType = this.getFileType(fileName); // 根据后缀获取文件类型
        let retry = 3; // 失败重试次数
        
        let formData = new FormData();// 参数表单
        let params = {
            ...this.uploadData,// 接口所需的其他参数
            chunks,
            chunk: null,
            name: file.name,
            uuid: file.id,
            file: null,
        }
        for(let key in params){
            formData.append(key, params[key]);
        };
        
        for(let i = 0; i < chunkList.length; i++){
            let fileChunk = new File([chunkList[i]], fileName, {type: fileType});
            formData.set("file",fileChunk);
            formData.set("chunk",i);
            await this.uploadFile(formData, i, chunks, retry + 1);
        }
    },
    // 通过文件后缀获取文件类型
    getFileType(name){
        let splitNameList = name.split(".");
        return splitNameList[splitNameList.length - 1];
    },
    uploadFile(formData, chunk, chunks, tryCount){
        return new Promise(resolve =>{
            $.ajax({
                url: this.url,
                data: formData,
                contentType: false,
                cache: false,
                processData: false,
                type: 'post',
                success: res =>{
                    if(chunk < chunks - 1){
                        resolve()
                    } else if(chunk == chunks - 1){
                        console.log("success")
                    }
                },
                error: err =>{
                    if(tryCount <= 1){
                        console.log("error")
                    } else {
                        this.uploadFile(formData, chunk, chunks, tryCount-1)
                    }
                }
            })
        })
    }
  }
};
</script>