前后端大文件上传,断点续传、分片上传、秒传的完整实例
1、前言
文件上传在项目开发很常见,很多项目都会涉及到图片,音频,文件的上传。在现代技术中,基本都是通过组件库封装的组件进行上传,比如 element-ui ,antdesign等。这些ui库封装了上传的一些基础方法,可以满足大多的上传场景。但是如果遇到很大的文件,比如1G,2G这种大体量的文件,ui库封装的一些组件就难以满足实际的场景。有很多的问题需要我们去关注比如:
痛点1: 1G的文件按照正常的20M的带宽(上行20M已经属于很快的带宽),我们可能需要在页面停留大约9分钟(大概时间,甚至更久)。正常用户都无法忍受9分钟什么都不做,等着文件上传
痛点2: 由于文件过大,上传时间过长,中途断网或者浏览器崩溃,都会导致上传中断。可能就差几M就成功的文件,导致下次又要继续从头开始上传
痛点3:同一个文件多次上传,浪费服务器资源
本文主要通过一个Demo从前端(vue3)、后端用实战代码演示大文件分片上传、断点续传、秒传的开发原理。
前端代码:
<template>
<div>
<div @click.native="hanldeClick" class="upload_container">
<input name="请上传文件" type="file" ref="uploadRef" @change="handleChange" :multiple="multiple" :accept="accept"/>
</div>
<div ref="uploadSubmit" @click="handleUpload()">上传</div>
<div><span ref="uploadResultRef"></span></div>
<div>md5Value:{{fileSparkMD5}}</div>
</div>
</template>
<script setup>
import { ref,onMounted } from 'vue'
import { ElMessage, } from 'element-plus'
import SparkMD5 from 'spark-md5';
import { makePostRequest } from './axios.js';
defineProps({
multiple:{
type:Boolean,
default:true
},
accept:{
type:Array,
default:[]
}
})
const uploadRef = ref(); // input 的ref
const uploadResultRef = ref(null); //上传结果展示
const fileSparkMD5 = ref([]); // 文件MD5 唯一标识
const fileChuncks = ref([]); // 文件分片list
const chunckSize = ref(1*1024*1024); // 分片大小
const promiseArr = []; // 分片上传promise集合
const isUploadChuncks = ref([]); // 返回 [1,1,1,0,0,1] 格式数组(这里需要服务端返回的值是按照索引正序排列),标识对应下标上传状态 已上传:1 ,未上传:0
const uploadProgress = ref(0); // 上传进度
const uploadQuantity = ref(0); // 总上传数量
//检测文件是否上传过,
const checkFile = async (md5) => {
const data = await makePostRequest('http://127.0.0.1:3000/checkChuncks', {md5});
if (data.length === 0) {
return false;
}
const {file_hash:fileHash,chunck_total_number:chunckTotal} = data[0]; // 文件的信息,hash值,分片总数,每条分片都是一致的内容
if(fileHash === md5) {
const allChunckStatusList = new Array(Number(chunckTotal)).fill(0); // 文件所有分片状态list,默认都填充为0(0: 未上传,1:已上传)
const chunckNumberArr = data.map(item => item.chunck_number); // 遍历已上传的分片,获取已上传分片对应的索引 (chunck_number为每个文件分片的索引)
chunckNumberArr.forEach((item,index) => { // 遍历已上传分片的索引,将对应索引赋值为1,代表已上传的分片 (注意这里,服务端返回的值是按照索引正序排列)
allChunckStatusList[item] = 1
});
isUploadChuncks.value = [...allChunckStatusList];
return true; // 返回是否上传过,为下面的秒传,断点续传做铺垫
}else {
return false;
}
}
//文件上传function
const handleUpload = async () => {
const fileInput = uploadRef.value;
const file = fileInput.files[0];
// 未选择文件
if (!file) {
ElMessage({message:'请选择文件',type:'warning'});
return
}
//循环计算文件MD5,多文件上传时候
const fileArr = fileInput.files;
for(let i = 0; i < fileArr.length; i++){
const data = await getFileMD5(fileArr[i]);
fileSparkMD5.value.push({md5Value:data,fileKey:fileArr[i].name});
sliceFile(fileArr[i]);
}
//检测文件是否已上传过
const {md5Value} = fileSparkMD5.value[0]; // 这里已单文件做示例,默认取第一个文件
const isFlag = await checkFile(md5Value); //是否上传过
if(isFlag) {
const hasEmptyChunk = isUploadChuncks.value.findIndex(item => item === 0); //在所有的分片状态中找到未上传的分片,如果没有表示已完整上传
//上传过,并且已经完整上传,直接提示上传成功(秒传)
if(hasEmptyChunk === -1) {
ElMessage({message:'上传成功',type:'success'});
return;
}else {
//上传缺失的分片文件,注意这里的索引,就是文件上传的序号
for(let k = 0; k < isUploadChuncks.value.length; k++) {
if(isUploadChuncks.value[k] !== 1) {
const {md5Value,fileKey} = fileSparkMD5.