上传功能需要的一些辅助库
1、使用第三方 vant 组件中的提示做 loading 来区分和提示。
2、大文件上传需要计算 MD5 值,使用 spark-md5 库。
3、上传接口,自定义的 axios接口,使用 Apis['方法'] 的方式调用接口(此处不是接口封装的重点)
功能实现
此处使用 vue 形式编写,功能可以提出使用在js方法中,注意去掉 ts 内容
html
<template>
<input ref="inputFile" type="file" hidden @change="checkFiles" />
<van-button class="upBut" color="#BF234D" @click="onClick">点击上传文件</van-button>
</template>
typescript
<script setup lang='ts'>
import Apis from '@/utils/apis';
import FileReaderUpoad from "@/utils/upoad";
import { showLoadingToast, showSuccessToast, showFailToast } from 'vant';
/** 创建上传组件实例 */
const inputFile = ref<HTMLFieldSetElement | null>(null);
/** 创建公共字段集合 */
const data = reactive({
loading: false,
});
// 点击自定义上传按钮调用上传点击事件
const onClick = () => {
// 调用 input file 的点击事件
inputFile.value && inputFile.value.click();
};
// input 获取文件成功后触发的change事件
const checkFiles = (e: Event) => {
/** 判断拦截是否正在上传 */
if (data.loading) return;
data.loading = true;
/** 获取元素内容 */
let event: any = e.target as EventTarget;
/** 获取选中的文件 */
let file = event.files[0];
/** 文件使用的验证ID */
let storageId = "";
let fileName = "", suffix = "";
if (file.name) {
let arr = file.name.split('.');
fileName = arr.filter((item: object, index: number) => arr.length - 1 != index).join(".");
suffix = arr.filter((item: object, index: number) => arr.length - 1 == index).join(".");
};
/** 提示 */
showLoadingToast({
message: `正在解析文件`,
forbidClick: true,
duration: 0,
overlay: true,
});
/** 创建文件分片并发实例(方法参数作用请向下找到自定义封装的分片处理文件中查找) */
let upoad = new FileReaderUpoad({
file,
maxNum: 4,
size: 5242880,
byteLength: file.size,
singleFileSize: 209715200,
/** 单文件上传 */
once: () => {
showLoadingToast({
message: `正在上传中`,
forbidClick: true,
duration: 0,
overlay: true,
});
// 单文件上传
let obj: any = {...};
Apis.upLoad(obj).then((res: any) => {
if (res.code == 200) {
data.loading = false;
showSuccessToast("上传成功");
};
}).catch((err) => {
showFailToast("上传出现问题");
data.loading = false;
});
},
/** 开始分片处理,创建分片信息,上传分片信息 */
start: (data: any, next: Function) => {
/** 开始创建分片信息上传信息, 具体需要什么信息,请联系自己的后端,打印 data 数据查找对应字段 */
let obj: any = {
/** 计算好的 md5 值 */
fileMd5: data.md5,
/** 文件总片数 */
sliceTotalChunks: data.total,
/** 其他字段,自己添加,从 file 文件数据,或者 data 中查找 */
...
};
/** 上传分片信息 */
Apis.start(obj).then((res: any) => {
if (res.code == 200) {
/** 这里后台可能会返回一个验证 ID 要带在每个分片上传中,不需要删除即可 */
storageId = res.obj.storageId;
/** 当前分片信息已经上传成功,通过调用 next() 函数通知执行下一个逻辑 */
next();
};
}).catch(() => {
showFailToast("创建上传信息失败");
data.loading = false;
});
},
/** 单个分片上传方法 */
upload: (val: any, next: Function) => {
/** 上传分片参数,根据需要添加参数 **/
let obj: any = {
/** 文件名称 */
name: fileName,
/** 当前是第几个分片 */
chunk: val.page,
/** 一共多少分片 */
chunks: val.total,
/** 总文件的md5哈希值 */
md5: val.totalMd5,
/** 总文件的大小 */
size: val.file.size,
/** 验证 ID 值 */
tenantId: storageId,
/** 当前分片的 md5 哈希值 */
currentFileMd5: val.md5,
/** 当前分片文件 */
file: val.blob
};
/** 上传单片文件的接口 */
Apis.burst(obj).then((res: any) => {
/** 上传文件成功,直接通过 next() 执行下一步 */
if (res.code == 200 && res.success) next();
/** 如果上传失败或者超时,则将当前分片传入 next(val),让分片重新进入队列上传分片 */
else next(val);
}).catch(() => {
/** 如果上传失败或者超时,则将当前分片传入 next(val),让分片重新进入队列上传分片 */
next(val);
});
},
/** 分片上传,加载进度返回 */
loading: (val: string) => {
showLoadingToast({
message: `已上传${val}%`,
forbidClick: true,
duration: 0,
overlay: true,
});
},
/** 分片上传结束,使用当前函数,通过接口通知后台上传结束 */
end: (res: any) => {
/** 调用结束接口 */
Apis.end({ tenantId: storageId }).then((res: any) => {
if (res.code == 200) {
showSuccessToast("上传成功");
}
}).catch(() => {
showFailToast("上传失败");
data.loading = false;
})
}
});
/** 调用函数,开始对文件进行分片处理 */
upoad.uploadStart();
};
</script>
以下是自定义库的内容,可直接使用
typescript
import SparkMD5 from 'spark-md5';
interface FileType {
file: File; // 源文件
size: number; // 单片大小
once?: Function; // 单文件上传
maxNum?: number; // 设置最大并发数
start?: Function; // 创建开始上传初始化
end?: Function; // 分片派发完结束返回
upload?: Function; // 抛出所有拆分的分片
loading?: Function; // 加载进度
byteLength: number; // 文件总大小
singleFileSize: number; // 文件超出多大值做分片处理
};
export default class FileReaderUpoad {
private option: any = []; // 所有分片存储
private Setting: FileType; // 传进来的参数
private totalFilm!: number; // 总片数
private result: string = ""; // 二进制串
private current: number = 0; // 当前是第几片
private totalMd5: string = ''; // 总文件的md5哈希值
private arrLength: number = 0; // 总长度
constructor(Setting: FileType) {
this.Setting = Setting;
};
// 开始执行
public uploadStart = () => {
let that = this;
// 判断文件是否需要分片
if (this.Setting.file.size >= this.Setting.singleFileSize) {
// 将文件转化为二进制串
let fileReader = new FileReader();
fileReader.readAsBinaryString(this.Setting.file);
fileReader.onload = function () {
// 设置并发数
that.Setting.maxNum = that.Setting.maxNum || 1;
// 获取二进制串
that.result = fileReader.result as string;
// 计算总片数
that.totalFilm = Math.ceil(that.Setting.byteLength / that.Setting.size);
// 计算md5值;
that.getDataMd5(that.result).then((res: any) => {
// 计算成功后将参数传出, start 方法调用上传创建信息
that.totalMd5 = res;
if (typeof (that.Setting.start) === 'function') that.Setting.start({ md5: res, total: that.totalFilm }, () => {
that.current = 1;
that.option = [];
that.burstParam();
});
});
};
} else {
typeof this.Setting.once == 'function' && this.Setting.once();
};
};
// 单个分片解析计算以及参数补齐
private burstParam() {
// 如果当前片数小于总片数
if (this.current < this.totalFilm) {
// 计算当前片的起始位置和结束位置
let start = (this.current - 1) * this.Setting.size;
let end = this.current * this.Setting.size;
// 对当前片进行分片处理
this.fileSlice(start, end, (val: string) => {
// 计算当前片的md5值
this.getDataMd5(val).then((res: unknown) => {
// 将当前片的参数存入option数组中
this.option.push({
md5: res,
chunkFile: val,
size: val.length,
page: this.current -1,
total: this.totalFilm,
totalMd5: this.totalMd5,
file: this.Setting.file,
blob: this.Setting.file.slice(start, end),
});
// 当前片数加1
this.current++;
// 递归调用burstParam函数
this.burstParam();
});
});
} else {
// 计算最后一片的起始位置和结束位置
let start = (this.current - 1) * this.Setting.size;
let end = this.current * this.Setting.size;
// 对最后一片进行分片处理
this.fileSlice(start, end, (val: string) => {
// 计算最后一片的md5值
this.getDataMd5(val).then((res: unknown) => {
// 将最后一片的参数存入option数组中
this.option.push({
md5: res,
chunkFile: val,
size: val.length,
page: this.current -1,
total: this.totalFilm,
totalMd5: this.totalMd5,
file: this.Setting.file,
blob: this.Setting.file.slice(start, this.Setting.byteLength),
});
// 记录option数组的长度
this.arrLength = this.option.length;
// 判断并发数量存不存在,并且要大于 0
if (this.Setting.maxNum && this.Setting.maxNum > 0) {
// 调用multiRequest函数,传入option数组和最大并发数
this.multiRequest(this.option, this.Setting.maxNum).then((res: any) => {
// 调用end函数,传入结果
typeof this.Setting.end == 'function' && this.Setting.end(res);
});
} else {
// 并发数不能为零
new Error("并发数不能为零!");
};
});
});
};
};
// 分片处理
private fileSlice = (start: number, end: number, callback: Function) => {
callback(this.result.slice(start, end));
};
// 计算md5值
private getDataMd5 = (data: string) => {
return new Promise((resolve, reject) => {
if (data) resolve(new SparkMD5().appendBinary(data).end());
else reject("计算失败");
});
};
/**
* 多个请求并发
* @param urls 请求地址数组
* @param maxNum 最大并发数
* @returns Promise
*/
private multiRequest = (urls: any, maxNum: number) => {
// 初始化队列
let queue: any = [];
// 克隆urls数组
let urlsClone = [...urls];
// 初始化结果数组
let result = new Array(urls.length);
// 初始化请求是否完成数组
let isDoneArr = new Array(urls.length).fill(0);
// 用于判断请求是否全部完成
let queueLimit = Math.min(maxNum, urls.length);
// 初始化计数器
let index = 0;
// 请求返回
const request = (queue: any, url: any) => {
// 如果有上传函数, 调用 upload 函数传参
if (typeof this.Setting.upload == 'function') this.Setting.upload(url, (val: object) => {
// 保证result结果和 urls 顺序相同
let i = urlsClone.indexOf(url);
result[i] = url;
isDoneArr[i] = 1;
// 判断当返回值val有值时,则接口请求失败,再次添加到请求列表
if(val) {
// 列表边长,说明计算 百分比总数发生变化
this.arrLength++;
// 添加失败的的接口到请求列表
urls.push(val);
// 添加到判断请求是否完毕的列表,添加请求次数
isDoneArr.push(val);
}
// 执行完一个请求后,执行出队操作
outLine(queue, url);
})
};
// 进入
const EnterTheTeam = (queue: any = [], url: object) => {
// 请求入队,并触发数据请求,返回队列长度
let len = queue.push(url);
if (len == this.Setting.maxNum) {
index++;
// 计算加载进度
let t = parseInt(String(index / this.arrLength * 100));
// 存在加载进度函数,传出当前进度
typeof this.Setting.loading == 'function' && this.Setting.loading(t);
};
request(queue, url);
return len;
};
// 初始化队列请求,在此处限制队列长度
while (EnterTheTeam(queue, urls.shift()) < queueLimit) {};
// 设置定义抛出内容
let promise: any = {
resolve: '',
reject: '',
};
// 出队
const outLine = (queue: any = [], url: object) => {
// 一个请求完成,出队,下一个队列,如果存在
queue.splice(queue.indexOf(url), 1);
if (urls.length) EnterTheTeam(queue, urls.shift());
else {
// 判断所有请求完成,再 resolve
if (isDoneArr.indexOf(0) === -1) {
promise.resolve(result);
typeof this.Setting.loading == 'function' && this.Setting.loading(100);
};
}
};
// 返回所有内容
return new Promise((resolve, reject) => {
return promise.resolve = resolve;
});
};
};