主要流程:
- 首先,上传分片文件前,将文件分片信息发送给服务器 。
- 其次, 服务器返回成功后,上传所有的分片文件
- 最后,当所有分片都上传成功之后,请求服务器合并上传的分片文件。
例子:
let uploadFile = async (file)=> {
let onProgress = (progress) => {
let percent = Math.round((progress.loaded / progress.total) * 100); // 文件上传进度回调
// do something here ...
};
// @required file 文件 ,onProgress 进度回调 ,splitSize 分片大小 默认5MB, splitCount 一次上传分片个数 默认5
const res = await multipartUpload(file, onProgress, 5, 5);
},
multipartUpload 上传文件方法
multipartUpload 方法中,首先会简单检查一下参数,防止因参数导致程序报错。除了 file ( 文件)参数是必填 ,onProgress (上传进度回调),splitSize( 分片大小),splitCount( 一次性发送多少个分片上传请求) ,都是可选参数 。 为什么是简单检查一下参数呢,因为就目前的条件判断还是不能够避免所有出现异常的情况,比如file 参数传个null ,splitCount 传个小数......在这里参数条件判断写得太多会影响代码的可读性,抽离成一个方法出去的话,我个人觉得是可以有,但是没必要。因为按照正常人的正常逻辑来使用的话,目前的判断还是可以避免编码中的参数错误的。
检查参数之后 ,对文件进行分片,获取文件分片信息。 将分片信息发送给服务器,服务返回成功后,上传分片文件,为了避免上传文件过大,会分批次,一次性发送指定个(splitCount 默认为 5) 分片上传请求。在每一个切片上传成功之后,则回调用一次 onProgress 文件上传进度回调函数,模拟出一个文件上传进度。 等所有的分片文件都上传成功之后,请求服务器合并分片文件,服务器合并成功则返回合并成功的文件路径 。
/**
* @param {*} file 上传文件
* @param {*} onProgress 切片上传进度
* @param {*} splitSize 每个切片文件的大小
* @param {*} splitCount 一次发送多少个切片请求
* @returns
*/
// 文件切片上传
export const multipartUpload = async (
file, // 切片原文件
onProgress = () => {}, // 切片回调参数
splitSize = 5, // 切片大小 默认5 MB
splitCount = 5 // 一次性发送切片请求的个数 默认 5
) => {
// 简单检查一下file 参数
if (!file || typeof file != "object" || Array.isArray(file))
throw new Error(" a required parameter 'file' missing .");
// 简单判断一下 splitSize 必须是大于或等于 1 的 数字
if (typeof splitSize != "number" || splitSize < 1)
throw new Error(
" the type of 'splitSize' must be 'number' and greater than 1 ."
);
// 简单判断一下 splitCount 必须是大于或等于 1 的 数字
if (typeof splitCount !== "number" || splitCount < 1)
throw new Error(
" the type of 'splitCount' must be 'number' and greater than 1 ."
);
let _file = file.file || file;
const { size, name, type, lastModified } = _file; // 获取文件大小 和名称
let identifier = await getIdentifier(_file); // 根据文件md5 和当前时间戳生成的唯一标识符
const totalChunks = Math.ceil(size / splitSize / 1024 / 1024); // 切片总块数 文件大小/ 切片大小向上取整
const splitFileList = splitFile(_file, splitSize); // 返回切片数组
let groupFileList = groupFileListByCount(splitFileList, splitCount); // 将 切片数组根据count 分组
let splitInfo = { totalChunks, identifier, fileName: name, type }; // 文件切片信息
// 上传分片文件件 告诉服务器准备上传的切片文件信息
let beforeUploadFlag = await beforeUpload(splitInfo);
if (!beforeUploadFlag) return false; // 当服务器没准备好切片上传
// 切片上传的结果
const uploadFlag = await uploadGroupFileList(
groupFileList,
splitInfo,
onProgress
);
if (!uploadFlag) return false;
// 请求服务器合并切片文件
const mergeResult = await mergeFileFlag(identifier);
if (!mergeResult) return false;
// 请求服务器合并切片成功后 调一次上传进度回调接口 让上传进度100%
onProgress({
total: totalChunks + 1,
loaded: totalChunks + 1,
});
return mergeResult;
};
getIdentifier 根据文件MD5 + timestamp 生成一个上传文件的唯一标识
// 根据文件MD5 + timestamp 生成的唯一标识
export const getIdentifier = (file) => {
return new Promise((resolve, reject) => {
let reader = new FileReader();
let spark = new SparkMD5();
reader.readAsBinaryString(file);
reader.onload = (e) => {
spark.appendBinary(e.target.result);
const md5 = spark.end();
resolve(md5 + Date.now());
};
});
};
splitFile 根据splitSize 文件进行分片
export const splitFile = (file, splitSize = 5) => {
const _5M = 1024 * 1024 * splitSize; // 默认分割成5M
const { size, type } = file;
let tempFileList = []; // 盛放切割后的切片
for (let index = 0; index < size; index += _5M) {
let start = index; // 切片开始位置
let end = start + _5M < size ? index + _5M : size; // 切片结束位置
let item = file.slice(start, end, type); // 切割的文件
tempFileList.push(item);
}
return tempFileList;
};
groupFileListByCount根据splitCount 对分片文件数组进行分组
// 将fileList 分组
export const groupFileListByCount = (fileList, count) => {
let group = Math.ceil(fileList.length / count); // 将 fileList 分为多少个组
let resultList = new Array(group); // 根据组数创建一个空的 list
fileList.map((item, index) => {
let i = Math.floor(index / count); // index / count 向下取整 为组数编号
if (Array.isArray(resultList[i])) resultList[i].push(item);
else resultList[i] = [item];
});
return resultList;
};
beforeUpload 上传分片文件前 将文件的分片信息发送给服务器 服务器返回成功上传分片
export const beforeUpload = async (splitInfo) => {
try {
const res = await that.$httpRequest({
url: "/sys-file/prepareUpload",
method: "post",
data: splitInfo,
isForm: true,
});
if (res.code == 0) return true;
return false;
} catch (err) {
return false;
}
};
uploadGroupFileList 上传分组中的分片
// 上传分组的切片文件
export const uploadGroupFileList = async (
list = [],
splitInfo = {},
onProgress // 每一个切片上传成功之后 调一次上传回调 模拟一个进度条
) => {
/**
* 模拟一个进度条
* total+1 避免当所有的切片都上传完成后进度100% 。而还没有请求服务器合并切片
* 或者请求服务器合并分片失败 而上传进度为100% 对用户不友好
*/
let progress = {
total: splitInfo.totalChunks + 1,
loaded: 0,
};
for (let i = 0; i < list.length; i++) {
let itemList = Array.isArray(list[i]) ? list[i] : [];
let itemHttpUpload = itemList.map((item, index) => {
return new Promise(async (resolve, reject) => {
let chunkNumber = i * list[i].length + index + 1; // 当前是所有文件切片中的第几个
let { totalChunks, identifier, fileName } = splitInfo;
let params = { totalChunks, chunkNumber, identifier, fileName };
uploadFile(item, params, "/sys-file/multiUpload")
.then((res) => {
// 上传进度回调
progress.loaded++;
onProgress(progress);
resolve(chunkNumber);
})
.catch((err) => {
reject(chunkNumber);
});
});
});
// Promise.allSettled 等待上一组 upload http请求所有都完成后 返回结果
const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
.then((res) => {
return Promise.resolve(res);
})
.catch((err) => {
return Promise.resolve(err);
});
// 将上传失败的分片文件存储起来
let errorChunkList = lastGroupUpload
.filter((item) => item.status === "rejected")
.map((item) => item.reason);
// 判断上一组的http 中是否有上传失败的分片 存在则不继续上传剩余的分片 返回 false
if (errorChunkList.length > 0) return false;
}
return true;
};
mergeFileFlag 请求合并分片
合并成功 则文件上传完成 。
// 分片全部上传完成之后 请求服务器合并 上传的分片文件
export const mergeFileFlag = async (identifier) => {
try {
const params = { identifier, convert: 0 };
const res = await that.$httpRequest({
url: "/sys-file/complete",
method: "post",
isForm: true,
data: params,
});
if (res.code == 0 && res.data) {
return res.data;
}
return false;
} catch (err) {
return false;
}
};
完整代码
import Vue from "vue";
import $ from "jquery";
import SparkMD5 from "spark-md5"; // 获取文件MD5
import store from "@/store";
let that = new Vue(); // that 指向一个新Vue 实例 ,用于调用 Vue prototype 的一些一些方法 例如 deepClone 、httpRequest 等
const is_Dev = process.env.NODE_ENV == "development" ? true : false;
let baseUrl = is_Dev
? window.globalConfig.DEV_BASE_API
: window.globalConfig.PRO_BASE_API;
// 单独封装一个分片上传的请求
export const uploadFile = (file, config = {}, url = "/sys-file/upload") => {
if (!file || typeof file != "object" || Array.isArray(file))
throw new Error(" a required parameter 'file' missing . ");
let _file = file.file || file;
let _para = new FormData();
_para.append("file", _file);
// 除文件外的其他参数
if (typeof config == "object" && !Array.isArray(config)) {
const keyList = Object.keys(config);
keyList.map((item) => {
_para.append(item, config[item]);
});
}
return new Promise((resolve, reject) => {
let options = {
url: baseUrl + url,
method: "POST",
data: _para,
timeout: 60000,
contentType: false,
processData: false,
headers: {
Authorization: "Bearer " + store.state.user.access_token,
},
};
$.ajax(options)
.then((res) => {
resolve(res);
})
.catch((err) => {
if (err.statusText == "timeout") err.abort();
reject(err);
});
});
};
// 根据splitSize 对文件进行切片
export const splitFile = (file, splitSize = 5) => {
const _5M = 1024 * 1024 * splitSize; // 默认分割成5M
const { size, type } = file;
let tempFileList = []; // 盛放切割后的切片
for (let index = 0; index < size; index += _5M) {
let start = index; // 切片开始位置
let end = start + _5M < size ? index + _5M : size; // 切片结束位置
let item = file.slice(start, end, type); // 切割的文件
tempFileList.push(item);
}
return tempFileList;
};
// 根据文件MD5 + timestamp 生成的唯一标识
export const getIdentifier = (file) => {
return new Promise((resolve, reject) => {
let reader = new FileReader();
let spark = new SparkMD5();
reader.readAsBinaryString(file);
reader.onload = (e) => {
spark.appendBinary(e.target.result);
const md5 = spark.end();
resolve(md5 + Date.now());
};
});
};
// 分片上传前 告诉服务器 分片文件信息
export const beforeUpload = async (splitInfo) => {
try {
const res = await that.$httpRequest({
url: "/sys-file/prepareUpload",
method: "post",
data: splitInfo,
isForm: true,
});
if (res.code == 0) return true;
return false;
} catch (err) {
return false;
}
};
// 分片全部上传完成之后 请求服务器合并 上传的分片文件
export const mergeFileFlag = async (identifier) => {
try {
const params = { identifier, convert: 0 };
const res = await that.$httpRequest({
url: "/sys-file/complete",
method: "post",
isForm: true,
data: params,
});
if (res.code == 0 && res.data) {
return res.data;
}
return false;
} catch (err) {
return false;
}
};
// 将fileList 分组
export const groupFileListByCount = (fileList, count) => {
let group = Math.ceil(fileList.length / count); // 将 fileList 分为多少个组
let resultList = new Array(group); // 根据组数创建一个空的 list
fileList.map((item, index) => {
let i = Math.floor(index / count); // index / count 向下取整 为组数编号
if (Array.isArray(resultList[i])) resultList[i].push(item);
else resultList[i] = [item];
});
return resultList;
};
// 上传分组的切片文件
export const uploadGroupFileList = async (
list = [],
splitInfo = {},
onProgress // 每一个切片上传成功之后 调一次上传回调 模拟一个进度条
) => {
/**
* 模拟一个进度条
* total+1 避免当所有的切片都上传完成后进度100% 。而还没有请求服务器合并切片
* 或者请求服务器合并分片失败 而上传进度为100% 对用户不友好
*/
let progress = {
total: splitInfo.totalChunks + 1,
loaded: 0,
};
for (let i = 0; i < list.length; i++) {
let itemList = Array.isArray(list[i]) ? list[i] : [];
let itemHttpUpload = itemList.map((item, index) => {
return new Promise(async (resolve, reject) => {
let chunkNumber = i * list[i].length + index + 1; // 当前是所有文件切片中的第几个
let { totalChunks, identifier, fileName } = splitInfo;
let params = { totalChunks, chunkNumber, identifier, fileName };
uploadFile(item, params, "/sys-file/multiUpload")
.then((res) => {
// 上传进度回调
progress.loaded++;
onProgress(progress);
resolve(chunkNumber);
})
.catch((err) => {
reject(chunkNumber);
});
});
});
// Promise.allSettled 等待上一组 upload http请求所有都完成后 返回结果
const lastGroupUpload = await Promise.allSettled(itemHttpUpload)
.then((res) => {
return Promise.resolve(res);
})
.catch((err) => {
return Promise.resolve(err);
});
// 将上传失败的分片文件存储起来
let errorChunkList = lastGroupUpload
.filter((item) => item.status === "rejected")
.map((item) => item.reason);
// 判断上一组的http 中是否有上传失败的分片 存在则不继续上传剩余的分片 返回 false
if (errorChunkList.length > 0) return false;
}
return true;
};
/**
* @param {*} file 上传文件
* @param {*} onProgress 切片上传进度
* @param {*} splitSize 每个切片文件的大小
* @param {*} splitCount 一次发送多少个切片请求
* @returns
*/
// 文件切片上传
export const multipartUpload = async (
file, // 切片原文件
onProgress = () => {}, // 切片回调参数
splitSize = 5, // 切片大小 默认5 MB
splitCount = 5 // 一次性发送切片请求的个数 默认 5
) => {
// 简单检查一下file 参数
if (!file || typeof file != "object" || Array.isArray(file))
throw new Error(" a required parameter 'file' missing .");
// 简单判断一下 splitSize 必须是大于或等于 1 的 数字
if (typeof splitSize != "number" || splitSize < 1)
throw new Error(
" the type of 'splitSize' must be 'number' and greater than 1 ."
);
// 简单判断一下 splitCount 必须是大于或等于 1 的 数字
if (typeof splitCount !== "number" || splitCount < 1)
throw new Error(
" the type of 'splitCount' must be 'number' and greater than 1 ."
);
let _file = file.file || file;
const { size, name, type, lastModified } = _file; // 获取文件大小 和名称
let identifier = await getIdentifier(_file); // 根据文件md5 和当前时间戳生成的唯一标识符
const totalChunks = Math.ceil(size / splitSize / 1024 / 1024); // 切片总块数 文件大小/ 切片大小向上取整
const splitFileList = splitFile(_file, splitSize); // 返回切片数组
let groupFileList = groupFileListByCount(splitFileList, splitCount); // 将 切片数组根据count 分组
let splitInfo = { totalChunks, identifier, fileName: name, type }; // 文件切片信息
// 上传分片文件件 告诉服务器准备上传的切片文件信息
let beforeUploadFlag = await beforeUpload(splitInfo);
if (!beforeUploadFlag) return false; // 当服务器没准备好切片上传
// 切片上传的结果
const uploadFlag = await uploadGroupFileList(
groupFileList,
splitInfo,
onProgress
);
if (!uploadFlag) return false;
// 请求服务器合并切片文件
const mergeResult = await mergeFileFlag(identifier);
if (!mergeResult) return false;
// 请求服务器合并切片成功后 调一次上传进度回调接口 让上传进度100%
onProgress({
total: totalChunks + 1,
loaded: totalChunks + 1,
});
return mergeResult;
};
分组上传分片文件
将分片文件根据splitCount 分组,一次只发送一组请求,等待上一组请求全部完成,再发送下一组请求。1是因为如果文件过大的话,同时发送过多的请求,服务器可能会处理不了,服务异常导致上传文件失败。2是因为同时发送的请求,最后一个请求可能会等待很久才返回,可能会出现请求超时,导致上传文件失败。3.如果一个文件上传完,再上传后一个文件的话,不能够减少上传文件的时间,一次性,传多个请求可以减少上传时间 。 4.可以根据不同服务器的带宽能力,调试一次性上传多少个文件合适。
分组上传效果图
上传进度及效果图