vue+core 断点续传-上传-前后台实现
整体逻辑如下
前台引用spark-md5获取文件唯一ID值,即md5值,前台将文件进行分片,通过该值进行后台校验,后台以MD5值生成文件夹,以断点生成子文件,以此实现断点续传。
前台计算MD5,前台计算MD5快慢与文件大小相关,成功后方可请求后台,以防无值出现上传文件错乱。
后台用MD5值进行校验,有则跳过,无则追加,文件分片完全上传后,再请求合并接口将文件进行合并。
代码实现逻辑,前台将文件进行分片执行for循环,循环的每片值为该断点位置,将每一片进行记录请求到后台breakpoint参数,如果出现意外结束,再次上传时请求后台方法CheckPoint进行校验(校验逻辑,取出MD5文件夹下最大的文件名作为断点,存储时按断点传就直接取文件名,如果有内置逻辑,就按逻辑截取),如果存在则进行从该断点处上传,如果不存在从头开始,暂停按钮同理,所有分片数据请求成功后,请求FileMerge进行合并。
相应注释见代码,具体前后台代码如下:
前台:
<template> <div id="app"> <!-- 上传组件 --> <el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange"> <i class="el-icon-upload"></i> <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> <div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div> </el-upload> <!-- 进度显示 --> <div class="progress-box"> <span>上传进度:{{ percent.toFixed() }}%</span> <el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button> </div> <!-- 展示上传成功的视频 --> <div v-if="videoUrl"> <video :src="videoUrl" controls /> </div> </div> </template> <script> import SparkMD5 from "spark-md5"; import axios from "axios"; export default { name: "App3", filters: { btnTextFilter(val) { return val ? "暂停" : "继续"; } }, data() { return { percent: 0, videoUrl: "", upload: true, percentCount: 0, CurFileName: "", baseurllocal: window.localStorage.getItem("rooturl"), curPoint: 0, calchunk: 0 }; }, methods: { ReadFileMd5(fileObj) { return new Promise((resolve, reject) => { let hash; const slicelength = 10; const rfmchunkSize = Math.ceil(fileObj.size / slicelength); const fileReader = new FileReader(); const md5 = new SparkMD5(); let index = 0; const loadFile = () => { const slice = fileObj.slice(index, index + rfmchunkSize); fileReader.readAsBinaryString(slice); }; loadFile(); fileReader.onload = e => { md5.appendBinary(e.target.result); if (index < fileObj.size) { index += rfmchunkSize; loadFile(); } else { hash = md5.end(); this.hash = hash; console.log(hash); resolve(hash); } }; }); }, async handleChange(file) { if (!file) return; this.percentCount = 0; this.percent = 0; this.videoUrl = ""; // 获取文件并转成 ArrayBuffer 对象 const fileObj = file.raw; this.hash = ""; this.ReadFileMd5(fileObj).then(response => { const hash = this.hash; // 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量 const chunkSize = 10485760, //2097152, chunkList = [], // 保存所有切片的数组 chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片 suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1]; // 文件后缀名 // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName) let curChunk = 0; // 切片时的初始位置 for (let i = 0; i < chunkListLength; i++) { const item = { breakPoint: i.toString(), chunk: fileObj.slice(curChunk, curChunk + chunkSize), fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名 }; curChunk += chunkSize; chunkList.push(item); } console.log(chunkList); this.chunkList = chunkList; this.CurFileName = file.raw.name; console.log(this.CurFileName); console.log("chunkListLength" + "------" + chunkListLength); this.CheckPoint().then(res => { if (this.percentCount === 0) { this.percentCount = 100 / this.chunkList.length; } if (this.calchunk > 0) { this.percent += this.percentCount * this.calchunk; // 改变进度 this.reqChuck(this.calchunk); } else { this.reqChuck(0); } }); }); }, async reqChuck(curchunk) { let vm = this; await vm.reqChuckPoint(curchunk); }, async reqChuckPoint(index) { this.curPoint = index; if (!this.upload) { return; } else { if (index > this.chunkList.length - 1) { this.complete(); // this.percent = 100; console.log("循环结束"); return; } else { const item = this.chunkList[index]; const formData = new FormData(); formData.append("md5", this.hash); formData.append("breakPoint", item.breakPoint); formData.append("chunk", item.chunk); formData.append("fileName", item.fileName); let url = this.baseurllocal + "/api/weatherforecast/UploadEfilePoint"; console.log(url); await this.$api.post(url, formData, res => { if (res.code === 200) { // 成功 if (this.percentCount === 0) { // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值 this.percentCount = 100 / this.chunkList.length; } this.percent += this.percentCount; // 改变进度 console.log(this.percent); // this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传 index++; this.reqChuck(index); } else { this.upload = !this.upload; console.log("code不为200" + index); } }); } } }, complete() { if (!this.upload) return; console.log(this.baseurllocal + "/api/weatherforecast/FileMerge"); this.$api.post( this.baseurllocal + "/api/weatherforecast/FileMerge", { hash: this.hash, filename: this.CurFileName }, res => { if (res.code === 200) { // 请求发送成功 console.log("合并成功"); // this.videoUrl = res.data } } ); }, // 按下暂停按钮 handleClickBtn() { this.upload = !this.upload; if (this.upload) { // 如果不暂停则继续上传 if (this.percent >= 100) { this.percentCount = 0; this.percent = 0; } this.CheckPoint().then(res => { if (this.percentCount === 0) { this.percentCount = 100 / this.chunkList.length; } console.log(this.calchunk); console.log(this.curPoint); if (this.calchunk > 0 && this.calchunk >= this.curPoint - 1) { this.percent = this.percentCount * (this.curPoint - 1); // 改变进度 this.reqChuck(this.curPoint - 1); } else { this.reqChuck(0); } }); } }, CheckPoint() { this.calchunk = 0; return new Promise((resolve, reject) => { this.$api.get( "api/weatherforecast/CheckPoint", { hash: this.hash }, res => { if (res.code == 200) { this.calchunk = res.data; resolve(res.data); } else { this.calchunk = 0; resolve(); } } ); }); } } }; </script> <style scoped> .progress-box { box-sizing: border-box; width: 360px; display: flex; justify-content: space-between; align-items: center; margin-top: 10px; padding: 8px 10px; background-color: #ecf5ff; font-size: 14px; border-radius: 4px; } </style>
后台net core代码如图
本文来自博客园,作者:zwbsoft,转载请注明原文链接:https://www.cnblogs.com/zwbsoft/p/16710044.html
电话微信:13514280351