原创:前端利用切片实现大文件断点续传 - 掘金 (juejin.cn))
我这里仔细阅读了原创的代码实现方式,对代码做出了改进,通过vue框架改了一下
完整代码gitee地址: gitee.com/z2695836546…
一.上传文件组件:
- el-upload组件需要添加
:auto-upload="false",禁止选择文件后自动上传
<template>
<div class="box">
<el-upload
v-model:file-list="fileList"
class="upload-demo"
action=""
multiple
:on-change="change"
:auto-upload="false"
:show-file-list="false"
>
<el-button type="primary" class="upload_btn" :loading="uploadBtnLoading"
>点击上传</el-button
>
</el-upload>
<el-progress
:percentage="percentage"
:indeterminate="true"
v-if="isShowProgress"
/>
</div>
</template>
<script setup>
import { ref } from "vue";
import instance from "../utils/request.js";
import SparkMD5 from "spark-md5";
const isShowProgress = ref(false); //是否显示进度条
const uploadBtnLoading = ref(false); //上传按钮是否处于加载状态
const percentage = ref(0); //进度条数值
/**
* 传入文件对象,返回文件生成的HASH值,后缀,buffer,以HASH值为名的新文件名
* @param file
* @returns {Promise<unknown>}
*/
const changeBuffer = (file) => {
return new Promise((resolve) => {
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = (ev) => {
let buffer = ev.target.result,
spark = new SparkMD5.ArrayBuffer(),
HASH,
suffix;
spark.append(buffer);
HASH = spark.end();
suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1];
resolve({
buffer,
HASH,
suffix,
filename: `${HASH}.${suffix}`,
});
};
});
};
//选择文件后调用
const change = async (uploadFile) => {
//文件状态处于待上传状态
if (uploadFile.status === "ready") {
let file = uploadFile.raw;
if (!file) return;
uploadBtnLoading.value = true;
isShowProgress.value = true;
// 获取文件的HASH
let already = [], //已经上传过的切片的切片名
data = null,
{ HASH, suffix } = await changeBuffer(file); //得到原始文件的hash和后缀
// 获取已经上传的切片信息
try {
data = await instance.get("/upload_already", {
params: {
HASH,
},
});
if (+data.code === 0) {
already = data.fileList;
}
} catch (err) {}
// 实现文件切片处理 「固定数量 & 固定大小」
let max = 1024 * 100, //切片大小先设置100KB
count = Math.ceil(file.size / max), //得到应该上传的切片
index = 0, //存放切片数组的时候遍历使用
chunks = []; //用以存放切片值
if (count > 100) {
//如果切片数量超过100,那么就只切成100个,因为切片太多的话也会影响调用接口的速度
max = file.size / 100;
count = 100;
}
while (index < count) {
//循环生成切片
//index 0 => 0~max
//index 1 => max~max*2
//index*max ~(index+1)*max
chunks.push({
file: file.slice(index * max, (index + 1) * max),
filename: `${HASH}_${index + 1}.${suffix}`,
});
index++;
}
index = 0;
//每一次上传一个切片成功的处理[进度管控&切片合并]
const complate = async () => {
// 管控进度条:每上传完一个切片,就将进度条长度增加一点
index++;
percentage.value = ((index / count) * 100).toFixed(1);
if (index < count) return;
// 当所有切片都上传成功,就合并切片
percentage.value = 100;
try {
//调用合并切片方法
data = await instance.post(
"/upload_merge",
{
HASH,
count,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (+data.code === 0) {
alert(
`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`
);
clear();
return;
}
throw data.codeText;
} catch (err) {
alert("切片合并失败,请您稍后再试~~");
clear();
}
};
// 循环上传每一个切片
chunks.forEach((chunk) => {
// 已经上传的无需在上传
//后台返回的already格式为['HASH_1.png','HASH_2.png'],既已经上传的文件的切片名
if (already.length > 0 && already.includes(chunk.filename)) {
//已经上传过了的切片就无需再调用接口上传了
complate(); //动进度条或合并所有切片
return;
}
let fm = new FormData();
fm.append("file", chunk.file);
fm.append("filename", chunk.filename);
instance
.post("/upload_chunk", fm)
.then((data) => {
//使用form data格式上传切片
if (+data.code === 0) {
complate(); ////动进度条或合并所有切片
return;
}
return Promise.reject(data.codeText);
})
.catch(() => {
alert("当前切片上传失败,请您稍后再试~~");
clear();
});
});
}
};
const clear = () => {
//上传完成后,将状态回归
isShowProgress.value = false;
uploadBtnLoading.value = false;
percentage.value = 0;
};
</script>
<style scoped>
.box {
width: 20vw;
margin: 20% auto;
}
.upload_btn {
margin-bottom: 50px;
}
</style>
二,request.js组件(对axios做出的简单封装)
/*把axios发送请求的公共信息进行提取*/
//创建一个单独的实例,不去项目全局的或者其他的axios冲突
import axios from "axios";
import Qs from "qs";
let instance = axios.create();
instance.defaults.baseURL = "http://127.0.0.1:8888";
//默认是multipart/form-data格式
instance.defaults.headers["Content-Type"] = "multipart/form-data";
instance.defaults.transformRequest = (data, headers) => {
//兼容x-www-form-urlencoded格式的请求发送
const contentType = headers["Content-Type"];
if (contentType === "application/x-www-form-urlencoded")
return Qs.stringify(data);
return data;
};
//统一结果的处理
instance.interceptors.response.use(
(response) => {
return response.data;
},
(reason) => {
//统一失败的处理
return Promise.reject(reason);
}
);
export default instance;