首先解释一下什么是分片上传
分片上传就是把一个大的文件分成若干块,一块一块的传输。这样做的好处可以减少重新上传的开销。比如:如果我们上传的文件是一个很大的文件,那么上传的时间应该会比较久,再加上网络不稳定各种因素的影响,很容易导致传输中断,用户除了重新上传文件外没有其他的办法,但是我们可以使用分片上传来解决这个问题。通过分片上传技术,如果网络传输中断,我们重新选择文件只需要传剩余的分片。而不需要重传整个文件,大大减少了重传的开销。
但是我们要如何选择一个合适的分片呢?因此我们要考虑如下几个事情:
1.分片越小,那么请求肯定越多,开销就越大。因此不能设置太小。
2.分片越大,灵活度就少了。
3.服务器端都会有个固定大小的接收Buffer。分片的大小最好是这个值的整数倍。
分片上传的步骤
1.先对文件进行md5加密。使用md5加密的优点是:可以对文件进行唯一标识,同样可以为后台进行文件完整性校验进行比对。
2.拿到md5值以后,服务器端查询下该文件是否已经上传过,如果已经上传过的话,就不用重新再上传。
3.对大文件进行分片。比如一个100M的文件,我们一个分片是5M的话,那么这个文件可以分20次上传。
4.向后台请求接口,接口里的数据就是我们已经上传过的文件块。(注意:为什么要发这个请求?就是为了能断点续传,比如我们使用百度网盘对吧,网盘里面有续传功能,当一个文件传到一半的时候,突然想下班不想上传了,那么服务器就应该记住我之前上传过的文件块,当我打开电脑重新上传的时候,那么它应该跳过我之前已经上传的文件块。再上传后续的块)。
5.开始对未上传过的文件块进行上传。(这个是第二个请求,会把所有的分片合并,然后上传请求)。
6.上传成功后,服务器会进行文件合并。最后完成。
话不多说,直接开始干代码
将文件拖到此处,或点击上传
文件上传时,会走http-request方法,如果定义了这个方法,组件的submit
方法就会被拦截掉(注意别在这个方法里面调用组件的submit
方法,会造成死循环),在这个方法里面我就可以搞我想搞的事情了。
http-request 这个传入的回调函数应该返回一个Promise
,所以我自己定义了一个无用的Promise进去
const prom = new Promise((resolve, reject) => {})prom.abort = () => {}return prom
如果要实现断点续传,需要和后端确定好,如何配合。
我这里的方案是,在我把所有的分片全部上传一遍后,会请求一个查询接口,后端在这个接口里面返回给我哪些分片没有上传成功(会给我序号),我这个时候,再去重新上传那些没有被上传成功的分片
直接贴完整代码,注释都在里面,看不懂的可以直接联系我,博客上有联系方式(依赖element-ui、axios、spark-md5)
将文件拖到此处,或点击上传import axios from "axios";import SparkMD5 from "spark-md5";export default {data() {return {maxSize: 5 * 1024 * 1024 * 1024, // 上传最大文件限制最小单位是bmultiUploadSize: 100 * 1024 * 1024, // 大于这个大小的文件使用分块上传(后端可以支持断点续传)100mbeachSize: 100 * 1024 * 1024, // 每块文件大小 100mbrequestCancelQueue: [], // 请求方法队列(调用取消上传url: "/tools/upload_test/", //上传进度progress: 0,showProgress: false,// 每上传一块的进度eachProgress: 0,// 总共有多少块。断点续传使用chunksKeep:0,// 切割后的文件数组fileChunksKeep:[],// 这个文件,断点续传fileKeep:null};},mounted() {},methods: {async checkedFile(options) {console.log(options);const {maxSize,multiUploadSize,getSize,splitUpload,singleUpload} = this; // 解构赋值const { file, onProgress, onSuccess, onError } = options; // 解构赋值if (file.size > maxSize) {return this.$message({message: `您选择的文件大于${getSize(maxSize)}`,type: "error"});}this.fileKeep = fileconst uploadFunc =file.size > multiUploadSize ? splitUpload : singleUpload; // 选择上传方式try {await uploadFunc(file, onProgress);this.$message({message: "上传成功",type: "success"});this.showProgress = false;this.progress = 0;onSuccess();} catch (e) {console.error(e);this.$message({message: e.message,type: "error"});this.showProgress = false;this.progress = 0;onError();}const prom = new Promise((resolve, reject) => {}); // 上传后返回一个promiseprom.abort = () => {};return prom;},// 格式化文件大小显示文字getSize(size) {return size > 1024? size / 1024 > 1024? size / (1024 * 1024) > 1024? (size / (1024 * 1024 * 1024)).toFixed(2) + "GB": (size / (1024 * 1024)).toFixed(2) + "MB": (size / 1024).toFixed(2) + "KB": size.toFixed(2) + "B";},// 单文件直接上传 async singleUpload(file, onProgress) {await this.postFile({ file, uid: file.uid, fileName: file.fileName ,chunk:0},onProgress);var spark = new SparkMD5.ArrayBuffer();spark.append(file);var md5 = spark.end();console.log(md5);const isValidate = await this.validateFile({fileName: file.name,uid: file.uid,md5:md5,chunks:1});},// 大文件分块上传splitUpload(file, onProgress) {console.log('file11')console.log(file)return new Promise(async (resolve, reject) => {try {const { eachSize } = this;const chunks = Math.ceil(file.size / eachSize);this.chunksKeep = chunksconst fileChunks = await this.splitFile(file, eachSize, chunks);this.fileChunksKeep = fileChunksconsole.log('fileChunks,文件数组切割后')console.log(fileChunks)//判断每上传一个文件,进度条涨多少,保留两位小数this.eachProgress = parseInt(Math.floor(100 / chunks * 100) / 100);this.showProgress = true;let currentChunk = 0;for (let i = 0; i 100 ? 100 : this.progress;}}var spark = new SparkMD5.ArrayBuffer();spark.append(file);var md5 = spark.end();console.log(md5);const isValidate = await this.validateFile({chunks: fileChunks.length,// chunk: fileChunks.length,fileName: file.name,uid: file.uid,md5:md5,// task_id:file.uid});// if (!isValidate) {// throw new Error("文件校验异常");// }resolve();} catch (e) {reject(e);}});},againSplitUpload(file, array) {console.log('file,array')console.log(file)console.log(array)return new Promise(async (resolve, reject) => {try {const { eachSize , fileKeep } = this;const chunks = this.chunksKeepconst fileChunks = this.fileChunksKeepthis.showProgress = true;// let currentChunk = 0;for (let i = 0; i 100 ? 100 : this.progress;}var spark = new SparkMD5.ArrayBuffer();spark.append(fileKeep);var md5 = spark.end();console.log(md5);const isValidate = await this.validateFile({chunks: fileChunks.length,// chunk: fileChunks.length,fileName: file.fileName,uid: file.uid,md5:md5,// task_id:file.uid});// if (!isValidate) {// throw new Error("文件校验异常");// }resolve();} catch (e) {reject(e);}});},// 文件分块,利用Array.prototype.slice方法splitFile(file, eachSize, chunks) {return new Promise((resolve, reject) => {try {setTimeout(() => {const fileChunk = [];for (let chunk = 0; chunks > 0; chunks--) {fileChunk.push(file.slice(chunk, chunk + eachSize));chunk += eachSize;}resolve(fileChunk);}, 0);} catch (e) {console.error(e);reject(new Error("文件切块发生错误"));}});},removeFile(file) {this.requestCancelQueue[file.uid]();delete this.requestCancelQueue[file.uid];return true;},// 提交文件方法,将参数转换为FormData, 然后通过axios发起请求postFile(param, onProgress) {console.log(param);const formData = new FormData();// for (let p in param) {// formData.append(p, param[p]);// }formData.append('file', param.file)//改了formData.append('uid',param.uid)formData.append('chunk',param.chunk)const { requestCancelQueue } = this;const config = {cancelToken: new axios.CancelToken(function executor(cancel) {if (requestCancelQueue[param.uid]) {requestCancelQueue[param.uid]();delete requestCancelQueue[param.uid];}requestCancelQueue[param.uid] = cancel;}),onUploadProgress: e => {if (param.chunked) {e.percent = Number((((param.chunk * (param.eachSize - 1) + e.loaded) /param.fullSize) *100).toFixed(2));} else {e.percent = Number(((e.loaded / e.total) * 100).toFixed(2));}onProgress(e);}};// return axios.post('/api/v1/tools/upload_test/', formData, config).then(rs => rs.data)return this.$http({url: "/tools/upload_test/",method: "POST",data: formData// config}).then(rs => rs.data);},// 文件校验方法validateFile(file) {// return axios.post('/api/v1/tools/upload_test/', file).then(rs => rs.data)console.log(2)console.log(file)returnthis.$http({url: "/tools/upload_test/upload_success/",method: "POST",data: file}).then(res => {if(res && res.status == 1){this.againSplitUpload(file,res.data.error_file)return true}});}}};.progress{/* 在当前页面居中 */position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);/* 宽度 */}
更新代码,上面的代码使用md5加密后,与后端加密的MD5值不一样,下面的加密过后是一样的
将文件拖到此处,或点击上传<!-- -->import axios from "axios";import SparkMD5 from "spark-md5";export default {data() {return {maxSize: 5 * 1024 * 1024 * 1024, // 上传最大文件限制最小单位是bmultiUploadSize: 100 * 1024 * 1024, // 大于这个大小的文件使用分块上传(后端可以支持断点续传)100mbeachSize: 100 * 1024 * 1024, // 每块文件大小 100mbrequestCancelQueue: [], // 请求方法队列(调用取消上传url: "/tools/upload_chunk/",//上传进度progress: 0,showProgress: false,// 每上传一块的进度eachProgress: 0,// 总共有多少块。断点续传使用chunksKeep: 0,// 切割后的文件数组fileChunksKeep: [],// 这个文件,断点续传fileKeep: null,// 断点续传,文件md5fileMd5Keep: ""};},mounted() { },methods: {async checkedFile(options) {// console.log(options);const {maxSize,multiUploadSize,getSize,splitUpload,singleUpload} = this; // 解构赋值const { file, onProgress, onSuccess, onError } = options; // 解构赋值if (file.size > maxSize) {return this.$message({message: `您选择的文件大于${getSize(maxSize)}`,type: "error"});}this.fileKeep = file;const uploadFunc =file.size > multiUploadSize ? splitUpload : singleUpload; // 选择上传方式try {await uploadFunc(file, onProgress);onSuccess();} catch (e) {console.error(e);this.$message({message: e.message,type: "error"});this.showProgress = false;this.progress = 0;onError();}const prom = new Promise((resolve, reject) => { }); // 上传后返回一个promiseprom.abort = () => { };return prom;},// 格式化文件大小显示文字getSize(size) {return size > 1024? size / 1024 > 1024? size / (1024 * 1024) > 1024? (size / (1024 * 1024 * 1024)).toFixed(2) + "GB": (size / (1024 * 1024)).toFixed(2) + "MB": (size / 1024).toFixed(2) + "KB": size.toFixed(2) + "B";},// 单文件直接上传async singleUpload(file, onProgress) {await this.postFile({ file, uid: file.uid, fileName: file.fileName, chunk: 0 },onProgress);// var spark = new SparkMD5.ArrayBuffer();// spark.append(file);// var md5 = spark.end();// console.log(md5);const reader = new FileReader();reader.readAsArrayBuffer(file);let hashMd5 = "";console.log(hashMd5);const that = this;function getHash(cb) {console.log("进入单个上传的getHash");reader.onload = function (e) {console.log("进入单个上传的getHash的函数2");console.log(hashMd5);console.log(this);// console.log(e)const hash = SparkMD5.ArrayBuffer.hash(e.target.result);// const hash = SparkMD5.ArrayBuffer.hash(file);console.log(hash);that.hashMd5 = hash;console.log(that.hashMd5);that.fileMd5Keep = hash;cb(hash);};}await getHash(function (hash) {console.log(hash);console.log(that);// 请求接口that.validateFile({name: file.name,uid: file.uid,md5: hash,chunks: 1,filter_type: "user_data_file"});});},// getMd5(file, chunkCount) {// const spark = new SparkMD5.ArrayBuffer();// let currentChunk = 0;// const reader = new FileReader();// reader.onload = function(e) {// spark.append(e.target.result);// currentChunk++;// if (currentChunk = file.size ? file.size : start + chunkSize;// reader.readAsArrayBuffer(file.slice(start, end));// }// loadNext();// },// 大文件分块上传splitUpload(file, onProgress) {return new Promise(async (resolve, reject) => {try {const { eachSize } = this;const chunks = Math.ceil(file.size / eachSize);this.chunksKeep = chunks;const fileChunks = await this.splitFile(file, eachSize, chunks);this.fileChunksKeep = fileChunks;console.log("fileChunks,文件数组切割后");console.log(fileChunks);//判断每上传一个文件,进度条涨多少,保留两位小数this.eachProgress = parseInt(Math.floor((100 / chunks) * 100) / 100);this.showProgress = true;let currentChunk = 0;for (let i = 0; i 100 ? 100 : this.progress;}}// this.getMd5(file, chunks);// var spark = new SparkMD5.ArrayBuffer();// spark.append(file);// var md5 = spark.end();// console.log(md5);const spark = new SparkMD5.ArrayBuffer();let currentChunkMd5 = 0;const that = this;const reader = new FileReader();reader.onload = async function (e) {spark.append(e.target.result);currentChunkMd5++;if (currentChunkMd5 = file.size ? file.size : start + eachSize;await reader.readAsArrayBuffer(file.slice(start, end));}this.$message({message: "正在进行文件加密校验",type: "info"});await loadNext();// let hashMd5 = "";// // console.log(hashMd5)// const that = this;// console.log("进入分片上传的getHash");// function getHash(cb) {// reader.onload = function(e) {// console.log("进入分片上传的getHash的函数");// const hash = SparkMD5.ArrayBuffer.hash(e.target.result);// // const hash = SparkMD5.ArrayBuffer.hash(file);// console.log(hash);// that.hashMd5 = hash;// console.log(that.hashMd5);// that.fileMd5Keep = hash;// cb(hash);// };// reader.readAsArrayBuffer(file);// }// await getHash(function() {// console.log(that);// that.validateFile({// name: file.name,// uid: file.uid,// md5: that.hashMd5,// chunks: fileChunks.length// // chunk: fileChunks.length,// });// });// 请求接口// console.log('fileChunks.length')// 请求接口// this.validateFile({// fileName: file.name,// uid: file.uid,// md5:md5,// chunks:1// });resolve();} catch (error) {reject(error);}});},// 断点续传againSplitUpload(file, array) {console.log("file,array");console.log(file);console.log(array);return new Promise(async (resolve, reject) => {try {const { eachSize, fileKeep } = this;const chunks = this.chunksKeep;const fileChunks = this.fileChunksKeep;this.showProgress = true;// let currentChunk = 0;for (let i = 0; i 100 ? 100 : this.progress;}// var spark = new SparkMD5.ArrayBuffer();// spark.append(fileKeep);// var md5 = spark.end();// console.log(md5);var fileMd5KeepTwo = this.fileMd5Keep;const isValidate = await this.validateFile({chunks: fileChunks.length,// chunk: fileChunks.length,name: file.name,uid: file.uid,md5: fileMd5KeepTwo,filter_type: "git_secret_file"// task_id:file.uid});// if (!isValidate) {// throw new Error("文件校验异常");// }// 关闭进度条this.showProgress = false;// 重置进度条this.progress = 0;resolve();} catch (e) {reject(e);}});},// 文件分块,利用Array.prototype.slice方法splitFile(file, eachSize, chunks) {return new Promise((resolve, reject) => {try {setTimeout(() => {const fileChunk = [];for (let chunk = 0; chunks > 0; chunks--) {fileChunk.push(file.slice(chunk, chunk + eachSize));chunk += eachSize;}resolve(fileChunk);}, 0);} catch (e) {console.error(e);reject(new Error("文件切块发生错误"));}});},removeFile(file) {this.requestCancelQueue[file.uid]();delete this.requestCancelQueue[file.uid];return true;},// 提交文件方法,将参数转换为FormData, 然后通过axios发起请求postFile(param, onProgress) {// console.log(param);const formData = new FormData();// for (let p in param) {// formData.append(p, param[p]);// }formData.append("file", param.file); //改了formData.append("uid", param.uid);formData.append("chunk", param.chunk);formData.append("filter_type", "git_secret_file");const { requestCancelQueue } = this;const config = {cancelToken: new axios.CancelToken(function executor(cancel) {if (requestCancelQueue[param.uid]) {requestCancelQueue[param.uid]();delete requestCancelQueue[param.uid];}requestCancelQueue[param.uid] = cancel;}),onUploadProgress: e => {if (param.chunked) {e.percent = Number((((param.chunk * (param.eachSize - 1) + e.loaded) /param.fullSize) *100).toFixed(2));} else {e.percent = Number(((e.loaded / e.total) * 100).toFixed(2));}onProgress(e);}};// return axios.post('/api/v1/tools/upload_chunk/', formData, config).then(rs => rs.data)return this.$http({url: "/tools/upload_chunk/",method: "POST",data: formData// config}).then(rs => rs.data);},// 文件校验方法validateFile(file) {// return axios.post('/api/v1/tools/upload_chunk/', file).then(rs => rs.data)return this.$http({url: "/tools/upload_chunk/upload_success/",method: "POST",data: file}).then(res => {if (res && res.status == 1) {this.againSplitUpload(file, res.data.error_file);this.$message({message: "有文件上传失败,正在重新上传",type: "warning"});} else if (res && res.status == 0) {this.$message({message: "上传成功",type: "success"});this.showProgress = false;this.progress = 0;} else if (res && res.status == 40008) {this.$message.error(res.message);this.showProgress = false;this.progress = 0;}});}}};.loading {/* 整体页面置灰 *//* background: rgba(0, 0, 0, 0.5); */}.progress {/* 在当前页面居中 */position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);margin-top: 40px;/* 宽度 */}/deep/ .el-dialog {position: relative;height: 500px;}