【vue2+js】通过js实现大文件分片上传和合并上传

为什么要分片上传

在这里插入图片描述
上传大文件(比如3GB)时,一般不能调用一次接口上传完,接口会超时或报错,这时候需要前端把大文件分片处理一下,再分别调用接口上传分片文件,全部分片上传完后,由后端将所有分片合并和返回最终的文件地址。

实现思路

  • 在进行分片上传前,需要对文件进行md5加密,生成md5码,在后面每次调用接口时以formData格式上传给接口
  • 然后定义初始化信息,比如分片大小、开始和结束的文件大小、索引
  • 接着执行定义好的分片函数,传入初始化好的文件信息:uploadChunk(file, start, end, index);
  • 最后执行uploadChunk(file, start, end, index),先调用分片接口上传分片文件,所有分片文件全部上传后调用合并分片接口,该接口会返回整个文件上传后最终的地址
    • file.slice(start, end)直接对文件进行分片,第一次分片的start是0,也就是文件开始位置,end是分片的大小(如果文件没有分片大就是文件大小)
    • 在文件分片后,将分片信息以formData格式作为参数传给分片接口
    • 在接口成功响应后,更新开始和结束位置、index自增:开始位置更新为上次的结束位置大小,结束位置更新为上次的结束位置加一个分片后的大小(如果加起来比文件大,结束位置就是整个文件大小),
    • 判断是不是最后一个分片:比较start 和 file.size大小,如果更新后的文件开始大小比整个文件小,递归调用uploadChunk,传入更新后的参数,直到所有分片都上传;如果更新后的文件开始大小比整个文件大(即上次end结束位置已经上传全部分片了),说明分片已经全部上传完了,不再调用分片接口,调用合并分片接口,获取最终的整个文件地址
  async handleAvatarUploadDemo({ file }) {
     .....
     //MD5加密
     const md5 = await this.handleFile(file);
     const chunkSize = 10 * 1024 * 1024; // 10MB 每个分片的大小
     let start = 0;
     let end = Math.min(chunkSize, file.size);
     let index = 0;

     const uploadChunk = (file, start, end, index) => {
       const chunk = file.slice(start, end); // 切割文件为分片
       let formData = new FormData();
       formData.append('file', chunk);
       formData.append('index', index);
       formData.append('totalChunks', Math.ceil(file.size / chunkSize));
       formData.append('md5', md5);
       axios
         .post(
           '/api/spang-system/oss/endpoint/put-file-flake',
           formData, // 直接传递formData对象
           {
             headers: { 'Content-Type': 'multipart/form-data' },
             timeout: 600000,
           }
         )
         .then(res => {
           // 处理分片上传成功的响应

           index++;
           start = end;
           end = Math.min(start + chunkSize, file.size);

           if (file.size - start < 5 * 1024 * 1024 && start !== 0) {
             // 最后一个切片大小小于5MB且不是第一个切片
             start = end;
             end = file.size; // 设置结束位置为文件末尾start = Math.max(0, end - chunkSize); // 重新计算起始位置        
           }

           if (start < file.size) {
             uploadChunk(file, start, end, index); // 递归上传下一个分片,并传递file参数
           } else {
             // 所有分片上传完成
             // 合并分片等操作
             const param = { md5: md5, originalFilename: fileName };
             axios
               .get(`/api/spang-system/oss/endpoint/file-merge`, {
                 params: param,
                 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                 timeout: 600000, //十分钟超时
               })
               .then(res => {
                 ......
                 loading.close();
               })
               .catch(e => {
                 loading.close();
                 // 处理分片上传失败
                 console.error('文件上传失败:', e);
               });
           }
         })
         .catch(e => {
           loading.close();
           // 处理分片上传失败
           console.error('文件上传失败:', e);
         });
     };

     uploadChunk(file, start, end, index);
    },

注意:判断是不是最后一个分片前的这段代码,不是必须的,后端要求最后一个分片小于5MB的话,不要调分片接口单独传一次,要求和最后一个分片(原来倒数第二个分片)一起传给接口,所以加了这个判断,具体业务具体分析

   if (file.size - start < 5 * 1024 * 1024 && start !== 0) {
    // 最后一个切片大小小于5MB且不是第一个切片
       start = end;
       end = file.size; // 设置结束位置为文件末尾start = Math.max(0, end - chunkSize); // 重新计算起始位置        
     }

几种文件常见的格式介绍

Blob 对象

用于表示不可变的、原始数据的类文件对象。它通常用于处理二进制数据,比如文件内容或者从其他数据源获取的数据。

// 创建一个包含文本的Blob对象
const blob = new Blob(["Hello, world!"], { type: 'text/plain' });

// 读取Blob对象的内容
const reader = new FileReader();
reader.onload = function(event) {
  console.log(event.target.result); // "Hello, world!"
};
reader.readAsText(blob);

File对象

new File() 是一个构造函数,用于创建一个新的 File 对象,代表有关用户文件的信息。这个对象可以用来读取文件内容或上传文件到服务器。
注意:可以将Blob格式转化成File文件

// 假设有一个Blob对象,代表一些数据
const blob = new Blob(["Hello, world!"], { type: 'text/plain' });

// 创建一个新的File对象
const file = new File([blob], "helloWorld.txt", {
  type: "text/plain",
  lastModified: new Date()
});

// 现在可以将这个File对象用于读取或上传等操作
console.log(file.name); // "helloWorld.txt"

FormData对象

new FormData() 是一个构造函数,用于创建一个新的 FormData 对象,它是一种表示表单数据键值对的方式,可以用于异步上传表单数据。

// 创建一个新的FormData对象
const formData = new FormData();

// 可以添加键值对,这里的file是一个File对象
formData.append('file', file, file.name);

// 现在可以将FormData对象用于XMLHttpRequest或fetch来异步上传表单数据
fetch('/upload', {
  method: 'POST',
  body: formData
});

区别和作用

  • File 对象代表单个文件的内容和属性,通常用于读取或上传文件。

  • FormData 对象用于构建一组键值对,表示表单字段和其值,可以包含 File 对象。它通常用于通过HTTP请求发送表单数据,特别是当表单包含文件上传时。

    简而言之,File 对象用于表示文件,而 FormData 对象用于打包表单数据,包括文件,以便发送到服务器。

完整代码

//文件分片和上传
  async handleAvatarUploadDemo({ file }, key = '', idx = null) {
    const isMp3 = file.type === 'audio/mp3' || file.type === 'audio/mpeg';
     const isMp4 = file.type === 'video/mp4';   
     const isPdf = file.type === 'application/pdf';
     const fileType = isMp3 ? 'mp3' : isMp4 ? 'mp4' : isPdf ? 'pdf' : '';
     const fileName = file.name;

     const loading = this.$loading({
       lock: true,
       text: '文件上传中, 请稍候...',
       spinner: 'el-icon-loading',
       target: '#centerLoading',
     });

     //MD5加密
     const md5 = await this.handleFile(file);
     const chunkSize = 10 * 1024 * 1024; // 10MB 每个分片的大小
     let start = 0;
     let end = Math.min(chunkSize, file.size);
     let index = 0;

     const uploadChunk = (file, start, end, index) => {
       const chunk = file.slice(start, end); // 切割文件为分片
       let formData = new FormData();
       formData.append('file', chunk);
       formData.append('index', index);
       formData.append('totalChunks', Math.ceil(file.size / chunkSize));
       formData.append('md5', md5);
       axios
         .post(
           '/api/spang-system/oss/endpoint/put-file-flake',
           formData, // 直接传递formData对象
           {
             headers: { 'Content-Type': 'multipart/form-data' },
             timeout: 600000,
           }
         )
         .then(res => {
           // 处理分片上传成功的响应

           index++;
           start = end;
           end = Math.min(start + chunkSize, file.size);

           if (file.size - start < 5 * 1024 * 1024 && start !== 0) {
             // 最后一个切片大小小于5MB且不是第一个切片
             start = end;
             end = file.size; // 设置结束位置为文件末尾start = Math.max(0, end - chunkSize); // 重新计算起始位置
             // start = Math.max(0, end - chunkSize); // 重新计算起始位置
           }

           if (start < file.size) {
             uploadChunk(file, start, end, index); // 递归上传下一个分片,并传递file参数
           } else {
             // 所有分片上传完成
             // 合并分片等操作
             const param = { md5: md5, originalFilename: fileName };
             axios
               .get(`/api/spang-system/oss/endpoint/file-merge`, {
                 params: param,
                 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                 timeout: 600000, //十分钟超时
               })
               .then(res => {
                 if (key == 'cover1') {
                   this.ruleForm.coursewarePath = res.data.data.link;
                   this.ruleForm.coursewareType = fileType;
                   this.ruleForm.coursewareName = res.data.data.originalName;
                   this.coursewarePath1 = [{ name: res.data.data.originalName, url: res.data.data.link }];
                 } else if (key == 'cover2') {
                   this.ruleForm.multipleChapters[idx].coursewarePath = res.data.data.link;
                   this.ruleForm.multipleChapters[idx].coursewareType = fileType;
                   this.ruleForm.multipleChapters[idx].coursewareName = res.data.data.originalName;
                   this.ruleForm.multipleChapters[idx].fileList = [
                     { name: res.data.data.originalName, url: res.data.data.link },
                   ];
                 }
                 loading.close();
               })
               .catch(e => {
                 loading.close();
                 // 处理分片上传失败
                 console.error('文件上传失败:', e);
               });
           }
         })
         .catch(e => {
           loading.close();
           // 处理分片上传失败
           console.error('文件上传失败:', e);
         });
     };

     uploadChunk(file, start, end, index);
    },
     // md5文件加密
    handleFile(file) {
      return new Promise((resolve, reject) => {
        const chunkSize = 2097152; // 2MB
        const fileReader = new FileReader();
        const spark = new SparkMD5.ArrayBuffer();

        let cursor = 0;

        fileReader.onerror = function () {
          reject('Error reading file');
        };

        fileReader.onload = function (e) {
          spark.append(e.target.result); // Append array buffer
          cursor += e.target.result.byteLength;

          if (cursor < file.size) {
            readNext();
          } else {         
            resolve(spark.end());
          }
        };

        function readNext() {
          const fileSlice = file.slice(cursor, cursor + chunkSize);
          fileReader.readAsArrayBuffer(fileSlice);
        }

        readNext();
      });
    },

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
这里给出一个简单的实现思路,代码可能需要根据实际情况进行适当的修改。 前端实现: 1. 在前端页面中,使用 `<input type="file" />` 选择需要上传文件。 2. 将文件进行分片,每个分片的大小可以根据实际情况进行调整,一般建议在 1MB - 5MB 之间。 3. 使用 XMLHttpRequest 对每个分片进行上传上传时需要注意设置正确的 Content-Range 头信息。 4. 上传完成后,前端需要将每个分片上传结果记录下来,可以使用一个数组来保存。 后端实现: 1. 在后端中,需要提供一个接口用于接收每个分片上传请求。 2. 对于每个分片上传请求,需要将其保存到一个临时文件中,文件名可以根据上传文件的唯一标识进行命名。 3. 当所有分片上传完成后,需要将这些分片合并成一个完整的文件。 代码实现前端代码: ```javascript const CHUNK_SIZE = 1024 * 1024; // 每个分片的大小,这里设置为 1MB function upload(file) { const totalChunks = Math.ceil(file.size / CHUNK_SIZE); // 总分片数 const chunks = []; // 保存每个分片上传结果 let uploadedChunks = 0; // 已经上传成功的分片数 // 将文件进行分片 for (let i = 0; i < totalChunks; i++) { const start = i * CHUNK_SIZE; const end = Math.min((i + 1) * CHUNK_SIZE, file.size); const chunk = file.slice(start, end); chunks.push(chunk); } // 上传每个分片 for (let i = 0; i < totalChunks; i++) { const chunk = chunks[i]; const xhr = new XMLHttpRequest(); xhr.open('POST', '/uploadChunk'); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('Content-Range', `bytes ${i * CHUNK_SIZE}-${(i + 1) * CHUNK_SIZE - 1}/${file.size}`); xhr.onload = function() { if (xhr.status === 200) { uploadedChunks++; chunks[i] = true; // 标记当前分片上传成功 if (uploadedChunks === totalChunks) { // 所有分片上传完成,触发合并文件的操作 mergeChunks(file.name, totalChunks); } } }; xhr.send(chunk); } // 合并分片的函数 function mergeChunks(filename, totalChunks) { const xhr = new XMLHttpRequest(); xhr.open('POST', '/mergeChunks'); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = function() { if (xhr.status === 200) { console.log(`文件 ${filename} 上传成功!`); } }; xhr.send(JSON.stringify({ filename, totalChunks })); } } ``` 后端代码: ```java @RestController public class UploadController { // 临时文件存放目录 private static final String TEMP_DIR = "/temp"; // 上传分片的接口 @PostMapping("/uploadChunk") public ResponseEntity<Void> uploadChunk(@RequestParam("file") MultipartFile file, @RequestHeader("Content-Range") String range) { // 解析 Content-Range 头信息,获取当前分片的起始位置和结束位置 long start = Long.parseLong(range.substring(range.indexOf(" ") + 1, range.indexOf("-"))); long end = Long.parseLong(range.substring(range.indexOf("-") + 1, range.indexOf("/"))); // 将分片保存到临时文件中 String filename = UUID.randomUUID().toString(); String tempFilePath = TEMP_DIR + "/" + filename; File tempFile = new File(tempFilePath); try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile, true))) { out.write(file.getBytes()); } catch (IOException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } return ResponseEntity.ok().build(); } // 合并分片的接口 @PostMapping("/mergeChunks") public ResponseEntity<Void> mergeChunks(@RequestBody MergeRequest mergeRequest) { String filename = mergeRequest.getFilename(); int totalChunks = mergeRequest.getTotalChunks(); // 检查所有分片是否已经上传完成 boolean allChunksUploaded = true; for (int i = 0; i < totalChunks; i++) { File chunkFile = new File(TEMP_DIR + "/" + filename + "." + i); if (!chunkFile.exists()) { allChunksUploaded = false; break; } } // 如果所有分片已经上传完成,进行合并操作 if (allChunksUploaded) { String filePath = "/upload/" + filename; File file = new File(filePath); try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file))) { for (int i = 0; i < totalChunks; i++) { File chunkFile = new File(TEMP_DIR + "/" + filename + "." + i); try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(chunkFile))) { byte[] buffer = new byte[1024]; int len; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } } chunkFile.delete(); // 删除临时分片文件 } } catch (IOException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } return ResponseEntity.ok().build(); } else { return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build(); } } } ``` 需要注意的是,这里的代码只是一个简单的实现,实际使用时可能需要进行一些优化和改进,例如增加断点续传的支持、限制上传文件的大小等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值