Element的upload实现分片上传和断点续传功能
最近领导安排实习生做分片上传和断点续传的功能,实习生找我,之前刚好弄过了解一些就记录一下。我们需要上传一个大文件,比如上G的视频文件,通常我们后端会对上传文件进行限制,一般不宜过大,5MB左右最好。如果文件过大,超出了http服务端请求大小限制,请求时间超时,传输中断导致上传失败,那么我们可以将文件进行分片上传。
分片上传的原理
分片上传的原理就是在前端将文件分片,然后一片一片的传给服务端,由服务端对分片的文件进行合并,从而完成大文件的上传。分片上传可以解决文件上传过程中超时、传输中断造成的上传失败,而且一旦上传失败后,已经上传的分片不用再次上传,不用重新上传整个文件,因此采用分片上传可以实现断点续传以及跨浏览器上传文件。
使用Element的upload来实现
至于Element的引入就不多做赘述,直接贴代码。
<template> <div> <el-upload action :auto-upload="false" :show-file-list="false" :on-change="changeFile" > <el-button size="small" type="primary">选择文件</el-button> <div slot="tip" class="el-upload__tip"> 1.上传文件不超过100M<br />2.只能上传一个文件<br />3.等待进度条出现√才是上传完成 </div> </el-upload> <!-- PROGRESS --> <div class="progress"> <span>上传进度:{{ total | totalText }}%</span> <el-link type="primary" v-if="total > 0 && total < 100" @click="handleBtn" >{{ btn | btnText }}</el-link > </div> </div> </template> 复制代码
上面这部分是上传的页面代码部分,有上传、暂停、继续等基础功能。
<script> import SparkMD5 from "spark-md5"; import { fileParse } from "@/public/utils.js"; import { getStorage } from "lesso-common/public/utils"; import axios from "axios"; export default { data() { return { total: 0, btn: false, abort: false, uploadSuc: false, slicesNum: null, chunkNumber: null, userInfo: { userName: getStorage("userData").user.employeeName, userId: getStorage("userData").user.userId, groupName: "AVM", }, }; }, filters: { btnText(btn) { return btn ? "继续" : "暂停"; }, totalText(total) { return total > 100 ? 100 : total; }, }, methods: { // 分片上传 async changeFile(file) { if (!file) return; file = file.raw; this.total = 0; this.abort = false; this.btn = false; let buffer = await fileParse(file, "buffer"), spark = new SparkMD5.ArrayBuffer(), hash, suffix; console.log(file, "file"); spark.append(buffer); hash = spark.end(); suffix = /\.([0-9a-zA-Z]+)$/i.exec(file.name)[1]; let multiple = []; for (let i = 1; i < file.size / 2; i++) { if (file.size % i == 0 && i <= 100) { multiple.push(i); } } // 切片个数 this.slicesNum = multiple.pop(); // 创建切片 let partList = [], partsize = file.size / this.slicesNum, cur = 0; for (let i = 1; i <= this.slicesNum; i++) { let item = { chunk: file.slice(cur, cur + partsize), filename: `${hash}_${i}.${suffix}`, chunkNumber: `${i}`, file: file.slice(cur, cur + partsize), }; cur += partsize; partList.push(item); } this.requestData = { groupName: this.userInfo.groupName, fileMd5: hash, name: file.name, size: file.size, totalChunks: this.slicesNum, chunkSize: partsize, uid: this.userInfo.userId, uname: this.userInfo.userName, }; this.partList = partList; this.sendRequest(); }, async sendRequest() { this.uploadSuc = false; // 根据100个切片创造100个请求集合 let requestList = []; this.partList.forEach((item, index) => { // 每一个函数都发送一个切片请求 let fn = async (chunkNumber) => { let formData = new FormData(), shardFile = new SparkMD5.ArrayBuffer(), shardFileBuffer = await fileParse(item.chunk, "buffer"), shardFileHash; shardFile.append(shardFileBuffer); shardFileHash = shardFile.end(); formData.append( "chunkNumber", chunkNumber ? chunkNumber : item.chunkNumber ); formData.append("groupName", this.requestData.groupName); formData.append("file", item.file); formData.append("fileMd5", this.requestData.fileMd5); formData.append("name", this.requestData.name); formData.append("size", this.requestData.size); formData.append("totalChunks", this.requestData.totalChunks); formData.append("chunkSize", this.requestData.chunkSize); formData.append("chunkMd5", shardFileHash); formData.append("uid", this.requestData.uid); formData.append("uname", this.requestData.uname); return axios .post( "http://iotupdateapi.lesso.com/uploadFastdfs/uploadPart", formData, { headers: { "Content-Type": "multipart/form-data" }, } ) .then((res) => { const { code, data } = res.data; if (code == 206) { this.total += parseInt(100 / this.slicesNum); // 传完的切片我们把它移除掉 if (chunkNumber) { this.partList.splice(data.chunkNumber - 1, 1); } return data.chunkNumber; } else if (code == 200) { this.uploadSuc = true; } }); }; requestList.push(fn); }); let i = 0; let send = async () => { if (this.abort) return; if (i >= requestList.length && !this.uploadSuc) { this.$message.warning("上传失败,请重新上传"); this.total = 0; return; } if (this.uploadSuc) { this.$message.success("上传成功"); this.total = 100; return; } await requestList[i](this.chunkNumber).then((res) => { this.chunkNumber = res; }); i++; send(); }; send(); }, handleBtn() { if (this.btn) { this.abort = false; this.btn = false; this.sendRequest(); return; } this.btn = true; this.abort = true; }, }, }; </script> 复制代码
上面这部分是分片上传和断点续传的代码逻辑实现。
本来是想每次都直接创建100个切片进行上传,后来接口的参数要求每片切片的大小必须要是整数,所有就又做了一次处理,算出文件可以整除的数字,取最接近小于等于100的的数字进行切片处理。
把每个切片转换成每个请求放进数组,根据每次调接口的返回值来处理数组实现分片上传和断点续传。
utils的fileParse方法
export function fileParse(file, type = "base64") { return new Promise(resolve => { let fileRead = new FileReader(); if (type === "base64") { fileRead.readAsDataURL(file); } else if (type === "buffer") { fileRead.readAsArrayBuffer(file); } fileRead.onload = (ev) => { resolve(ev.target.result); }; }); }; 复制代码
再贴一下接口的参数
分片上传和断点续传demo
作者:王小辉
链接:https://juejin.cn/post/7020321256582938655