一、初衷、想法
今年上半年的时候写了一个文件切片的库(凭空想象写的,没有结合实际项目开发),近期在使用的时候发现有些功能并没有考虑周全,然鹅花了这周六日来重写🥺
之前的思路是,传入文件及对应的fileKey,然后根据byte来进行切片,切一片发送一片,直到全部发送完毕,执行callback,当时只考虑到了单个文件切片上传。
现在的思路,先全部切片完后再上传,对比后端返回的当前文件还有哪些切片未被上传(实现断点续传),多个文件上传,如果单个切片上传失败单个切片自动进行三次上传请求,所有文件全部上传成功后,再执行callback。over。
其实文件切片及断点续传并不难,最重要的是有思路,然后将自己的思路用代码实现。over。(编程中的任何事情都一样,最重要的是要有思路。)
这篇博客只会抛出代码及部分重要注释,拒绝做复制侠,里面的逻辑并不复杂。
二、后端需要提供的两个接口
2.1 查询当前文件是否已经存在
对前端用户而已来说:提升用户体验、加快文件传输速度
对后端服务来说:减少不必要的带宽占用和磁盘空间的浪费
如果已经存在就不再继续上传该文件,如何判断当前文件是否已存在。唯一id: md5
这里的md5转成了62进制,因为16进制比较长。
2.2 上传文件
将切片文件上传给服务器,服务器进行合成。
三、生成md5+文件切片库
import Vue from 'vue';
import SparkMD5 from 'spark-md5'
// 思路
/**
* 首先将对传入的文件做MD5加密,根据加密后的MD5查询这个文件是否已被上传过,
* 查询这个文件是否“部分”切片被上传,还有哪些切片未被上传,对文件进行切片(根据大小或数量切片)
* 返回切片数组
*/
class SectionFileNew {
// 文件md5加密
getFileMD5 = (file: File, callback?: Function) => {
const spark = new (SparkMD5 as any)(),
fileReader = new FileReader();
const ops = () => new Promise(resolve => {
fileReader.readAsBinaryString(file)
fileReader.onload = function (e: any) {
spark.appendBinary(e.target.result)
const md5key = spark.end()
resolve(md5key)
callback && callback(md5key)
}
})
async function invoke() {
return await ops()
}
return invoke()
}
// 16进制转62进制
string16to62 = (val: any) => {
val = parseInt(val, 16)
let chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split(''),
radix = chars.length,
qutient = +val,
arr = [];
do {
const mod = qutient % radix;
qutient = (qutient - mod) / radix;
arr.unshift(chars[mod]);
} while (qutient);
return arr.join('');
}
// 文件切片
/**
* chunkByteSize 字节 默认0.5兆
*/
sectionFile = async (file: File, callback: Function, chunkByteSize = 524288, callbackFileType = "file") => {
chunkByteSize = chunkByteSize ? chunkByteSize : 524288;
// 计算该文件的可分为多少块
const chunks: any = Math.ceil(file.size / chunkByteSize);
let sectionFileArray = []
if (chunks === 1) {
sectionFileArray.push(file)
return callback && callback(sectionFileArray, chunks, this.string16to62(await this.getFileMD5(file)))
}
// 当前切片
for (let i = 0; i < chunks; i++) {
const start = i * chunkByteSize,
end = Math.min(start + chunkByteSize, file.size)
const blob = file.slice(start, end);
// blob 转 file
let res = callbackFileType === "file" ? new window.File([blob], file.name, { type: file.type }) : blob;
sectionFileArray.push(res)
}
callback && callback(sectionFileArray, chunks, this.string16to62(await this.getFileMD5(file)))
}
}
export default function () {
Vue.prototype.$sectionFileNew = SectionFileNew
}
export const sectionFileFnNew = SectionFileNew;
四、结合项目封装库
import Vue from "vue"
import { sectionFileFnNew } from "./indexNew"
import { sectionToUpload, checkHash } from "@/api/http/sectionToUpload";
export default function () {
class ProjectFileUploadNew {
private section = new (sectionFileFnNew as any)()
constructor() { }
/**
* 项目级单个文件上传
* file:文件
* fileKey:对应上传的key
* callback:回调
*/
projectFileSection = async (file: File, fileKey: string, callback?: Function): Promise<object> => {
return new Promise(async resolve => {
await this.section.getFileMD5(file, (md5: string) => {
// 检查该文件是否存在此hash
checkHash({
hash: this.section.string16to62(md5),
fnRes: (res: any) => {
// 接口返回正常
if (res.code == 0 && res.data) {
const { spiltChunkSize, uploadedSuccess, supportChunkUpload, nextChunkIndexs } = res.data;
// 文件未存在 并且支持分片上传
this.fileSection(file, async (fileArr: Array<File>, allChunks: number, md5Keyto62: string) => {
// 不支持分片上传 且 该文件未被上传
if (!supportChunkUpload && !uploadedSuccess) {
}
// 需要文件上传的切片数组
let uploadArr = this.needUploadSectionFileParameters(file, fileArr, allChunks, md5Keyto62)
// 断点续传
if (nextChunkIndexs) {
uploadArr = this.breakpointResume(uploadArr, nextChunkIndexs)
}
// 文件已存在 无需再次上传
if (uploadedSuccess) {
const res = {
md5Keyto62, // md5
fileKey,
nextChunkIndexs: [] // 未上传的切片
}
resolve(res)
return callback && callback(res)
}
// 开始上传
await this.toupload(uploadArr, fileKey, (arr: Array<object>) => {
const res = {
md5Keyto62,
fileKey,
nextChunkIndexs: arr
}
resolve(res)
callback && callback(res)
})
}, spiltChunkSize)
}
}
});
})
})
}
// 项目级多个文件上传
projectFileSectionMultiple = async (fileArr: Array<any>, callback?: Function) => {
if (fileArr.length === 0) return;
const res: any = []
for (let i = 0; i < fileArr.length; i++) {
res.push(await this.projectFileSection(fileArr[i].file, fileArr[i].filekey))
}
return new Promise(resolve => resolve(res));
}
/**
* 文件切片
* file:文件
* callback:回调 参数(切片后的数组、总共切片数量、62进制的md5)
* chunkByteSize:切片大小
*/
fileSection = (file: File, callback: Function, chunkByteSize?: number | null) => {
this.section.sectionFile(file, callback, chunkByteSize)
}
/**
* 需要上传的切片文件参数整理
* sectionFile:切片文件
* allChunks:总切片数
* md5Keyto62:文件加密后的md5(已转62进制)
*/
needUploadSectionFileParameters = (file: File, sectionFile: Array<File>, allChunks: number, md5Keyto62: string): Array<object> => {
const resParametersArr: Array<object> = []
// 整理请求参数
sectionFile.forEach((v: any, i: number) => {
const parameter: any = {}
parameter.fileKey = md5Keyto62;
parameter.file = v;
parameter.fileInfo = {
// 文件名称
name: v.name,
// 文件后缀
suffix: v.type.substring(v.type.lastIndexOf("/") + 1).toLowerCase(),
// 文件大小
size: file.size,
use: md5Keyto62,
// 分片索引
shardIndex: i + 1,
// 分片大小
shardSize: v.size,
// 总分片数
shardTotal: allChunks,
// 文件md5处理后的key 已转62进制
key: md5Keyto62
}
resParametersArr.push(parameter)
})
return resParametersArr
}
/**
* 断点续传 整理已经上传过的文件
* file:文件数组
* nextChunkIndexs:剩余需要上传的文件块索引
*/
breakpointResume = (fileArr: Array<object>, nextChunkIndexs: Array<number>) => {
const resArr: Array<object> = []
fileArr.forEach((v: any) => {
if (nextChunkIndexs.find(index => index == (v.fileInfo.shardIndex))) {
resArr.push(v)
}
})
return resArr;
}
// 切片开始上传
toupload = async (uploadArr: Array<object>, filekey: string, callback: Function,) => {
// 反转数组
uploadArr.reverse();
for (let i = uploadArr.length - 1; i >= 0; i--) {
// 当前上传未成功 执行三遍 如果三次未成功跳出失败循环 执行下一次
for (let j = 0; j < 3; j++) {
const res: any = await this.sendToUpload(uploadArr[i], filekey)
if (res.code == 0) {
uploadArr.splice(i, 1)
break;
}
}
}
callback && callback(uploadArr)
}
// 发送上传请求
sendToUpload = (data: object, filekey: string) => {
return new Promise(resolve => {
sectionToUpload({
data,
filekey,
fnRes: (res: any) => {
resolve(res);
}
});
});
}
}
Vue.prototype.$projectFileUploadNew = ProjectFileUploadNew;
}
五、使用
- 单个文件使用
const section = new (this as any).$projectFileUploadNew();
section
.projectFileSection((this.$refs as any).file.files[0], "imageForTest")
.then((val: string) => {
// 获取md5 key
console.log("md5", val);
});
- 多个文件使用
const fileArr: Array<object> = [];
(this.$refs as any).fileMultiple.files.forEach((v: any, i: number) => {
fileArr.push({
filekey: "test" + (i + 1),
file: v,
});
});
const section = new (this as any).$projectFileUploadNew();
section.projectFileSectionMultiple(fileArr).then((val: Array<object>) => {
console.log("多张文件一起上传", val);
});