1.钩子方法
import _ from "lodash";
import React, { useEffect } from "react";
import axios from "axios";
// mock上传接口
const uploadMockPromise = ({ formData, i }) => {
return new Promise((resolve, reject) => {
axios
.post("/upload/" + i, formData)
.then((res) => {
return resolve({ KEY: "key-01", ID: "id-01" });
})
.catch((err) => {
// if (i === 3|| i === 4||i === 5) {
// return reject({ err: true }); // 需要测试续传的时候需要返回错误数据
// }
return resolve({ KEY: "key-01", ID: "id-01" });
});
});
};
// mock合并接口
const mockMergePromise = ({ uid, option }) => {
return new Promise((resolve) => {
axios
.post("/merge", { uid })
.then((res) => {
console.log(res, "merge 成功");
return resolve({ success: true });
})
.catch((err) => {
console.log("merge 失败", err);
return resolve({ success: true });
});
});
};
const useUpload = ({ getFileList, getUploadDataArr }) => {
const uploadObjectRef = React.useRef<any>({ current: { fileList: [] } });
useEffect(() => {
getUploadDataArr(uploadObjectRef.current.uploadDataArr);
}, [uploadObjectRef.current.uploadDataArr]);
const getFilePara = (option: any) => {
const fileSize = option.file.size;
let everyPieceSize: number = 1024 * 1024;
const fileSlitLen = Math.ceil(fileSize / everyPieceSize);
console.log(everyPieceSize, fileSlitLen);
return {
fileSlitLen,
everyPieceSize,
};
};
const splitFile = (file: Blob, pieceByte: number, index: number) => {
const chunkSum = Math.ceil(_.get(file, "size") / pieceByte); // 切片总数
const chunkSize = Math.ceil(_.get(file, "size") / chunkSum); // 除以切片总数求得平均每片size多大
const { size, type } = file;
const filePrototype = File.prototype;
let blobSlice =
filePrototype.slice ||
_.get(filePrototype, "mozSlice") ||
_.get(filePrototype, "webkitSlice");
if (size > pieceByte) {
let start = index * chunkSize; // 切片开始位置
let end = start + chunkSize; // 切片结束位置
console.log(start, end, "start---end");
const file1 = _.get(file, "originFileObj");
let item: any = null;
try {
item = blobSlice.call(file, start, end, type); // 切割的文件
} catch (error) {
// 如果file没有slice方法, 就从file.originFileObj找
item = blobSlice.call(file1, start, end, type); // 切割的文件
}
return item;
}
return file;
};
const getFormData = ({ fileBlob }: any, option: any) => {
const formData = new FormData();
formData.append("FILE", fileBlob, option.file.name);
return formData;
};
/**
* 代码的核心思路为:
1.先初始化 promiseListLimit 个 promise 实例,将它们放到 executing 数组中
2.使用 Promise.race 等待这 promiseListLimit 个 promise 实例的执行结果
3.一旦某一个 promise 的状态发生变更,就将其从 executing 中删除,然后再执行循环生成新的 promise,放入executing 中
4.重复2、3两个步骤,直到所有的 promise 都被执行完
5.最后使用 Promise.all 返回所有 promise 实例的执行结果
*/
const concurrencyPromisePool = async (
promiseListLimit,
promiseArr,
mockTimeoutDataFn
) => {
const allPromiseArr: any[] = []; // 用于存放所有的promise实例
const executing: any[] = []; // 用于存放目前正在执行的promise
for (let index = 0; index < promiseArr.length; index++) {
const promise = promiseArr[index];
const mockPromise: Promise<any> = mockTimeoutDataFn
? mockTimeoutDataFn(promise, index)
: promise; // 回调函数返回的必须是promise,否则需要使用Promise.resolve进行包裹
allPromiseArr.push(mockPromise);
if (promiseListLimit <= promiseArr.length) {
// then回调中,当这个promise状态变为fulfilled后,将其从正在执行的promise列表executing中删除
const executingItem: Promise<any> = mockPromise.then(() => {
const idx = executing.indexOf(executingItem);
return executing.splice(idx, 1); // splice: 如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。
});
executing.push(executingItem);
console.log(`并发接口数量: ${executing.length}`);
if (executing.length >= promiseListLimit) {
// 一旦正在执行的promise列表数量等于限制数,就使用Promise.race等待某一个promise状态发生变更,
// 状态变更后,就会执行上面then的回调,将该promise从executing中删除,
// 然后再进入到下一次for循环,生成新的promise进行补充
await Promise.race(executing);
}
}
}
return Promise.all(allPromiseArr);
};
// mock promise, 方便查看并发的效果
const resovePromiseFn = (promiseFn, i) => {
console.log("开始--", i);
return new Promise((resolve) => {
console.log("结束--", i);
return resolve(promiseFn());
});
};
/**
* 新增上传--当第一次成功后执行剩下的并发方法
* @param option 文件
* @param uploadDataArr 分片结果数据:包括成功和失败的
*/
function runFn({ option }: { option: any }, uploadDataArr: any[]) {
const step = 5; // 并发请求五条接口
let fnArr: any[] = [];
const { fileSlitLen, everyPieceSize } = getFilePara(option);
const fileBlob: any = splitFile(option.file, everyPieceSize, 0);
const formData = getFormData({ fileBlob }, option);
let key = null;
let uid = null;
uploadMockPromise({
i: 0,
formData,
}).then((res) => {
key = _.get(res, "KEY");
uid = _.get(res, "ID");
uploadDataArr[0] = {
KEY: _.get(res, "KEY"),
ID: _.get(res, "ID"),
PART_NUM: 1,
fileBlob,
successFLg: true,
};
go();
return res;
});
function go() {
let isError = false;
for (let i = 1; i < fileSlitLen; i++) {
const partNumber = i + 1;
const fileBlob: any = splitFile(option.file, everyPieceSize, i);
const formData = getFormData({ fileBlob }, option);
const f1 = () => {
return uploadMockPromise({
i,
formData,
})
.then((res) => {
uploadDataArr[i] = {
KEY: _.get(res, "KEY"),
ID: _.get(res, "ID"),
PART_NUM: partNumber,
fileBlob,
successFLg: true,
};
return res;
})
.catch((err) => {
uploadDataArr[i] = {
option, // 为了续传时获取文件信息
KEY: key,
ID: uid,
PART_NUM: partNumber,
fileBlob,
successFLg: false,
};
isError = true;
return err;
})
.finally(() => {
console.log(uploadDataArr, "uploadDataArr");
console.log("finally");
});
};
fnArr.push(f1);
}
concurrencyPromisePool(step, fnArr, resovePromiseFn)
.then((res) => {
console.log(res, "concurrencyPromisePool", fileSlitLen);
uid = _.get(res, "uid");
// 如果有上传未成功的切片, 就不让合并
if (isError) return;
return mockMergePromise({ uid, option })
.then((res) => {
console.log("mockMergePromise--success");
const fileList = _.get(uploadObjectRef.current, "fileList") || [];
const fileName = _.get(option, "file.name")
uploadObjectRef.current.fileList = [
...fileList,
{ name: fileName },
];
getFileList(uploadObjectRef.current.fileList);
uploadObjectRef.current.uploadDataArr = [];
})
.catch((err) => {
console.log("mockMergePromise--errors");
});
})
.catch((err) => {
console.log(err);
});
}
}
const uploadFn = async({ option, index }) => {
const arr0 = _.get(option, 'fileList');
await fn({ file: _.get(arr0, [index]) });
function fn(option) {
// 想办法控制必须一个文件成功之后再开始上传下一个文件, 否则uploadObjectRef.current.uploadDataArr这个一维数组就不够用了!!!
uploadObjectRef.current.uploadDataArr = [];
return runFn({ option }, uploadObjectRef.current.uploadDataArr);
}
};
/**
* 新增上传--当第一次成功后执行剩下的并发方法
* @param option 文件
* @param uploadDataArr 分片结果数据:包括成功和失败的
*/
function continueFn({ option }: { option: any }, uploadDataArr: any[]) {
// 筛选出没有成功上传的数据
const errArr = _.filter(uploadDataArr, (ee) => !_.get(ee, "successFLg"));
const step = 5; // 并发请求五条接口
let fnArr: any[] = [];
let key = _.get(errArr, "[0].KEY");
let uid = _.get(errArr, "[0].ID");
go();
function go() {
let isError = false;
for (let i = 0; i < errArr.length; i++) {
const fileItem = errArr[i];
const partNumber = _.get(fileItem, "PART_NUM");
const index = partNumber - 1;
const fileBlob: any = _.get(fileItem, "fileBlob");
const formData = getFormData({ fileBlob }, option);
const f1 = () => {
return uploadMockPromise({
i: index,
formData,
})
.then((res) => {
uploadDataArr[index] = {
KEY: _.get(res, "KEY"),
ID: _.get(res, "ID"),
PART_NUM: partNumber,
successFLg: true,
};
return res;
})
.catch((err) => {
uploadDataArr[index] = {
KEY: key,
ID: uid,
PART_NUM: partNumber,
successFLg: false,
};
isError = true;
return err;
})
.finally(() => {
console.log(uploadDataArr, "uploadDataArr");
console.log("finally");
});
};
fnArr.push(f1);
}
concurrencyPromisePool(step, fnArr, resovePromiseFn)
.then((res) => {
console.log(res, "concurrencyPromisePool");
uid = _.get(res, "uid");
// 如果有上传未成功的切片, 就不让合并
if (isError) return;
return mockMergePromise({ uid, option }).then((res) => {
const fileList = _.get(uploadObjectRef.current, "fileList") || [];
const fileName = _.get(option, "file.name");
uploadObjectRef.current.fileList = [
...fileList,
{ name: fileName },
];
getFileList(uploadObjectRef.current.fileList);
uploadObjectRef.current.uploadDataArr = [];
});
})
.catch((err) => {
console.log(err);
});
}
}
const continueUploadFn = async({ option }) => {
await continueFn({ option }, uploadObjectRef.current.uploadDataArr);
};
return {
uploadFn,
continueUploadFn,
};
};
export default useUpload;
2. 组件使用:
import React, { useState, useEffect } from "react";
import _ from "lodash";
import { UploadOutlined } from "@ant-design/icons";
import type { UploadProps } from "antd";
import { Button, message, Upload } from "antd";
import useUpload from "./use/useUpload";
const UploadBtn: React.FC = () => {
const [fileList, setFileList] = useState([]);
const getFileList = arr =>{
setFileList(arr)
}
const { uploadFn, continueUploadFn } = useUpload({getFileList});
let [option, setOption] = useState({});
const props: UploadProps = {
name: "file",
// action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188',
headers: {
authorization: "authorization-text",
},
beforeUpload(file, fileList) {
console.log(file, fileList);
return true;
},
onChange(info) {
option = info;
console.log(info, 333);
if (info.file.status !== "uploading") {
console.log(info.file, info.fileList);
}
if (info.file.status === "done") {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === "error") {
message.error(`${info.file.name} file upload failed.`);
}
},
customRequest: () => {
console.log(option);
return uploadFn({ option });
},
};
return (
<div>
<Upload
beforeUpload={props.beforeUpload}
customRequest={props.customRequest}
onChange={props.onChange}
showUploadList={false}
>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
<ul>
{_.map(fileList, (item) => (
<li>{_.get(item, "name")}</li>
))}
</ul>
<p>{JSON.stringify(fileList)}</p>
<button
onClick={() => {
continueUploadFn({ option });
}}
>
续传
</button>
</div>
);
};
export default UploadBtn;
2.新增上传
1) 第一个切片上传必须单独处理, 因为需要获取到该文件的唯一标识后再执行后续切片的上传
2) 从第二个开始并发上传, 同时允许最多5个接口并行
3) 支持多个文件上传
3.续传
点击相应的未完成续传的数据旁边的续传按钮, 进行续传操作