文件上传-基于S3协议的通用对象存储方案文件上传-基于S3协议的通用对象存储OSS方案:分片上传,并发控制,失败重试等等
前言
什么S3协议
S3协议指的是Amazon Simple Storage Service(亚马逊简单存储服务)的接口规范,它是一种基于HTTP协议的RESTful API,用于访问Amazon Web Services(AWS)提供的对象存储服务。S3协议已经成为云存储领域的事实标准,许多云服务提供商都支持这一协议,包括阿里云的OSS、腾讯云的COS、华为云的OBS等。
使用S3协议的原因包括:
- 高度可扩展性:S3协议设计为高度可扩展,能够轻松应对数据量的增长。
- 可靠性:采用分布式架构,实现高可用性和数据冗余,确保数据的可靠性和安全性。
- 灵活性:支持多种数据传输方式,如客户端SDK、API、HTTP和FTP等。
- 高效性:采用高效的数据结构和算法,提供快速的数据读写速度。
- 丰富的接口:提供包括AWS SDK、API和第三方库在内的多种编程接口。
- 安全性:提供多种安全机制,包括访问控制、加密等,保障数据安全。
- 成本效益:按使用量付费,无需预先投资大量硬件资源。
S3协议适用于大规模数据存储、数据共享、数据备份和恢复等场景,并且可以通过S3 API和其他AWS服务(如Amazon Athena和Amazon Redshift Spectrum)集成,直接在存储数据上运行大数据分析。
总的来说,S3协议为开发者提供了一个强大、灵活且可靠的云存储解决方案,适用于各种规模和需求的应用。
使用对象存储的方案
方案一: 调用各个厂商的sdk,基于sdk二次封装 一般我们项目需要使用某个厂商的对象存储时,可以直接基于sdk完成业务,但是如果需要切换云存储或者需要本地服务的上传使用同一个逻辑时,我们的做法是:二次封装各个厂家的sdk,MyUpload为入口
优点:各个厂家sdk封装的可能更多,使用起来更简单
方案二: 使用S3协议封装: 基于该协议提供的接口,封装成我们业务需要的上传管理类,如果遇到需要切换云存储厂商时,只需切换对应的region,endponint等,如果配置参数也从接口获取,则完全不用修改代码就能实现切换厂商;
优点:无需手动去兼容各个厂家sdk,开发自己的上传类,只用遵循S3协议的接口实现即可
缺点: 返回的信息可能会被“吞掉”(同样的操作,原生sdk返回,S3的接口无返回,但可以手动兼容即可)
代码中通用字段,事件解释
basePath:上传目录统一前缀
Region(区域) :
- Region是地理位置上的一个区域,它代表了数据中心的物理位置。例如,一个Region可能包括一个国家或一个特定的城市。
- 云服务提供商通常会在不同的Region部署数据中心,以减少延迟、提高数据访问速度,并满足数据驻留和合规性要求。
- 用户在选择存储数据的Region时,通常需要考虑延迟、成本、合规性和数据主权等因素。
Bucket(存储桶) :
- Bucket是对象存储的基本容器,用于存储对象(Objects)。它是在特定Region中创建的,并且每个Bucket在AWS账户中必须是唯一的。
- Bucket可以看作是文件系统中的文件夹,但它不存储文件,而是存储对象。每个对象由数据和一组元数据组成。
- Buckets用于组织数据,并且可以配置不同的访问权限和策略,以控制谁可以访问存储在其中的对象。
Endpoint(端点) :
- Endpoint是访问特定Region中Bucket的URL。它是服务访问点,用于与存储服务进行通信。
- 每个Region都有一个或多个Endpoint,用于访问该Region中的S3服务。例如,Amazon S3的Endpoint可能看起来像这样:`s3.us-west-2.amazonaws.com`,其中`us-west-2`是Region的名称。
- Endpoint对于确保数据请求被路由到正确的Region和Bucket至关重要。
signUrlExpireTime: 签名有效期(版本4的签名最大时长不能超过1周)
通用宏定义:
// 文件大于多少时使用分片上传
export const USE_PART_UPLOAD_LIMIT = 100 * 1024 * 1024; // 100M
// 分片上传,分片大小
export const CHUNK_PART_SIZE = 50 * 1024 * 1024; // 50M
// 分片并发上传数量
export const PARALLEL_NUMBER = 2;
// 过期时间,s3签名最大不能超过1周
export const EXPIRE_TIME = 3600 * 24 * 6;
事件定义:
// 分片上传时触发的事件
export const MULTI_UPLOAD_EVENT = {
UPLOAD_ID: 'UPLOAD_ID', // 上传id事件
UPLOAD_PROGRESS: 'UPLOAD_PROGRESS', // 进度事件
};
实现步骤
上传管理类
1.创建上传类
- 将上传功能模块化,创建S3Upload class
- 继承eventemitter3,用于自定义事件的管理
EventEmitter3 is a high performance EventEmitter. It has been micro-optimized for various of code paths making this, one of, if not the fastest EventEmitter available for Node.js and browsers. The module is API compatible with the EventEmitter that ships by default with Node.js but there are some slight differences:
EventEmitter3 是一个高性能的事件发射器库,它被设计为 Node.js 和浏览器环境中的快速且轻量级的事件处理模块。它提供了一个简单而高效的 API,用于在各种 JavaScript 项目中实现事件驱动的编程模式。
// EventEmitter 基础用法
const EventEmitter = require('eventemitter3');
const emitter = new EventEmitter();
emitter.on('myEvent', function(data) {
console.log(data);
});
emitter.emit('myEvent', 'Hello, World!');
创建上传类代码如下:
import EventEmitter from 'eventemitter3';
class S3Upload extends EventEmitter {
logPrefix = 'S3Upload';
// 上传配置
config = {
basePath: '',
region: '',
bucket: '',
endpoint: '',
signUrlExpireTime: EXPIRE_TIME,
};
ossClient = null;
constructor(config = {}) {
super();
// 配置覆盖
Object.assign(this.config, config);
this.log('constructor', this.config);
}
log(subKey, info = {}) {
console.log(`${this.logPrefix}, ${subKey}, ====>`, JSON.stringify(info));
}
}
2.创建S3Client
- getStsTokenCache方法是调用后端接口,接口返回三个参数,代码参考文档: 使用STS临时访问凭证访问OSS - 对象存储 OSS - 阿里云 (alibabacloud.com)
- accessKeyId: 创建客户端需要的key;
- accessKeySecret:密钥;
- securityToken:临时token;
import { S3Client } from '@aws-sdk/client-s3';
class S3Upload extends EventEmitter {
...
/**
* 获取上传实例
* @return Promise<object>
*/
async getInstance() {
// getStsTokenCache方法需要后端提供,返回对应的信息
const { securityToken, accessKeyId, accessKeySecret } = await getStsTokenCache();
// @ts-ignore
this.ossClient = new S3Client({
region: this.config.region,
endpoint: this.config.endpoint,
credentials: {
accessKeyId,
secretAccessKey: accessKeySecret,
sessionToken: securityToken,
},
});
return this.ossClient;
}
}
3.普通上传
- 小文件推荐使用普通上传方式
重要
通常在文件大于100 MB的情况下,建议采用分片上传的方法,通过断点续传和重试,提高上传成功率。如果在文件小于100 MB的情况下使用分片上传,且partSize设置不合理的情况下,可能会出现无法完整显示上传进度的情况。对于小于100 MB的文件,建议使用简单上传的方式。 Browser.js分片上传 - 对象存储 OSS - 阿里云 (alibabacloud.com)
代码如下
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
class S3Upload extends EventEmitter {
...
/**
* 普通上传
* @param {object} file 文件对象
* @return Promise<object>
*/
async uploadFile(file) {
const originalName = file.name;
try {
const fileName = this.getUploadFileName(file);
const raw = file.raw || file;
// 当前桶
const currentBucket = this.config.bucket;
// @ts-ignore
const command = this.basePutObjectCommand({
// 填写Bucket名称。
Bucket: currentBucket,
// 填写OSS文件完整路径和本地文件的完整路径。OSS文件完整路径中不能包含Bucket名称。
Key: fileName,
// 指定文件内容或Buffer。
Body: raw,
// meta信息,非必选
Metadata: {
filePath: encodeURI(fileName),
bucket: currentBucket,
},
});
const result = await this.ossClient.send(command);
this.log('uploadFile:ok', result);
// 返回格式适配t-design的upload组件
return { status: 'success', response: { url: this.mockCompleteUrl(fileName) }, originalName };
} catch (error) {
this.log('uploadFile:error', error);
return { status: 'fail', error: '上传失败', originalError: error, response: { url: '' }, originalName };
}
}
/**
* 基础命令-PutObjectCommand通用
* @param {object} input 输入对象信息
* @param {string} input.Bucket 桶
* @param {string} input.key 存储路径
* @return object
*/
basePutObjectCommand(input: PutObjectCommandField) {
return new PutObjectCommand(input);
}
/**
* 返回完成的地址
*/
mockCompleteUrl(fileName) {
// `https://${this.config.bucket}.${this.config.region}.aliyuncs.com/${encodeURI(fileName)}`;
return `${this.config.endpoint}/${encodeURI(fileName)}`;
}
}
4.分片上传
4.1 如何分片
思路:
- 计算总分片数:根据上传文件大小和配置的宏定义来计算
- 循环创建上传分片promise:
算法如下:
// 辅助函数:计算分片总数
export function calculateTotalChunks(file, chunkSize) {
return Math.ceil(file.size / chunkSize);
}
// 创建分片伪代码:
for (let i = 0; i < totalChunks; i++) {
// 计算当前分片的起始和结束位置
const start = i * CHUNK_PART_SIZE;
const end = Math.min(start + CHUNK_PART_SIZE, file.size);
const chunk = await sliceFile(file.raw, start, end);
...
}
4.2 如何截取当前分片流
// 辅助函数:创建分片
export function sliceFile(file, start, end) {
const chunk = file.slice(start, end);
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(chunk);
});
}
4.3 分片后如何控制并发
控制分片的并发上传有两种实现方案:
- 按并发配置分组后,逐个放入promise.all,完成后进行下一组
- 基于队列
这里不展开讲了,后续有时间可以专门出一篇文章手写两种方式
本文采用p-limit来完成业务(基于队列) 基础用法:
import pLimit from 'p-limit';
const limit = pLimit(1);
const input = [
limit(() => fetchSomething('foo')),
limit(() => fetchSomething('bar')),
limit(() => doSomething())
];
// Only one promise is run at once
const result = await Promise.all(input);
console.log(result);
4.4 计算进度算法
step1: 模拟生成进度算法:所有分片上传占用90%;合成10%;完成一个分片时的进度(successNumber / totalChunks) * 0.9
step2: 文件合成完成进度为100%
整体代码代码如下
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
ListPartsCommand,
PutObjectCommand,
S3Client,
UploadPartCommand,
} from '@aws-sdk/client-s3';
import pLimit from 'p-limit';
class S3Upload extends EventEmitter {
...
/**
* 分片上传
* @param {object} file 文件对象
* @return Promise<object>
*/
async multipartUpload(file) {
try {
// 当前桶
const currentBucket = this.config.bucket;
// 文件名
const fileName = this.getUploadFileName(file);
// 上传任务
const multipartUploadResult = await this.ossClient.send(
this.baseCreateMultipartUploadCommand(currentBucket, fileName),
);
const uploadId = multipartUploadResult.UploadId;
// 触发生成uploadId事件
this.emit(MULTI_UPLOAD_EVENT.UPLOAD_ID, { uploadId, fileName });
// 生成所有分片promise
const uploadPromises = await this.mockChunkUploadPromise(file, { currentBucket, fileName, uploadId });
const uploadResults = await Promise.all(uploadPromises);
// 所有分片信息
const partsInfo = uploadResults.map(({ ETag, PartNumber }) => ({
ETag,
PartNumber: Number(PartNumber),
}));
// 合成
const completePartCommand = this.baseCompleteMultipartUploadCommand(currentBucket, fileName, uploadId, partsInfo);
const res = await this.ossClient.send(completePartCommand);
// 触发完成
this.emit(MULTI_UPLOAD_EVENT.UPLOAD_PROGRESS, 1);
this.log('multipartUpload:ok', res);
return { status: 'success', response: { url: res.Location }, originalName: file.name };
} catch (error) {
this.log('multipartUpload:error', error);
return { status: 'fail', error: '上传失败', originalError: error, response: {}, originalName: file.name };
}
}
/**
* 生成切片上传promise集合
* @param {object} file 文件对象
* @param {object} partCommandInfo 上传分片需要的参数
* @param {string} partCommandInfo.currentBucket
* @param {string} partCommandInfo.fileName
* @param {string} partCommandInfo.uploadId
* @param {array} abortPartInfo 断点续传时会使用到
* @return Promise<array>
*/
async mockChunkUploadPromise(file, partCommandInfo, abortPartInfo = []) {
// 上传分片需要的参数
const { currentBucket, fileName, uploadId } = partCommandInfo;
// 计算分片数量
const totalChunks = calculateTotalChunks(file, CHUNK_PART_SIZE);
const uploadPromises = [];
// 成功数量
let successNumber = abortPartInfo?.length || 0;
// 限制并发数量
const limit = pLimit(PARALLEL_NUMBER);
// Upload each part.
for (let i = 0; i < totalChunks; i++) {
// 计算当前分片的起始和结束位置
const start = i * CHUNK_PART_SIZE;
const end = Math.min(start + CHUNK_PART_SIZE, file.size);
const chunk = await sliceFile(file.raw, start, end);
// 当前分片number
const tempPartNumber = i + 1;
// 判断当前是否已经上传过:断点续传时跳过已经上传成功的
if (abortPartInfo.some((item) => `${item.PartNumber}` === `${tempPartNumber}`)) {
continue;
}
// 上传命令
const uploadPartCommand = this.baseUploadPartCommand(currentBucket, fileName, uploadId, chunk, tempPartNumber);
uploadPromises.push(
limit(() =>
this.ossClient.send(uploadPartCommand).then((d) => {
this.log(`Part ${i + 1} uploaded`, d);
// 触发进度事件
successNumber += 1;
// 模拟生成进度算法:所有分片上传占用90%;合成10%;完成一个分片时的进度(successNumber / totalChunks) * 0.9
this.emit(MULTI_UPLOAD_EVENT.UPLOAD_PROGRESS, (successNumber / totalChunks) * 0.9);
return { ...d, PartNumber: tempPartNumber };
}),
),
);
}
return uploadPromises;
}
/**
* 基础命令-生成分片上传任务
* @param {string} bucket 桶
* @param {string} key 存储路径
* @return object
*/
baseCreateMultipartUploadCommand(bucket, key) {
return new CreateMultipartUploadCommand({
Bucket: bucket,
Key: key,
});
}
/**
* 基础命令-上传分片
* @param {string} bucket 桶
* @param {string} key 存储路径
* @param {string} uploadId 上传id
* @param {buffer} chunk 切片数据
* @param {number} tempPartNumber 切片number
* @return object
*/
baseUploadPartCommand(bucket, key, uploadId, chunk, tempPartNumber) {
return new UploadPartCommand({
Bucket: bucket,
Key: key,
UploadId: uploadId,
Body: chunk,
PartNumber: tempPartNumber,
});
}
/**
* 基础命令-合成分片
* @param {string} bucket 桶
* @param {string} key 存储路径
* @param {string} uploadId 上传id
* @param {array} partsInfo 切片数据
* @return object
*
* partsInfo格式信息:
* [{ETag: '"EF64BB0A7689214E08D108B1EB76B642"', PartNumber: 1},...]
*/
baseCompleteMultipartUploadCommand(bucket, key, uploadId, partsInfo) {
return new CompleteMultipartUploadCommand({
Bucket: bucket,
Key: key,
UploadId: uploadId,
MultipartUpload: {
Parts: partsInfo,
},
});
}
}
5.断点续传
如何续传?
思路:
1.根据中断时的uploadId去查询,已经有哪些分片上传完了
2.文件切换时,跳过步骤1中已经完成的分片
3.讲步骤1和步骤2中的分片信息合并,然后进行文件合成
代码如下
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
ListPartsCommand,
PutObjectCommand,
S3Client,
UploadPartCommand,
} from '@aws-sdk/client-s3';
import pLimit from 'p-limit';
class S3Upload extends EventEmitter {
...
/**
* 断点续传(追加上传)
* @param {object} file 文件对象
* @param {object} abortInfo 断点续传
* @param {string} abortInfo.breakUploadId 本次上传id
* @param {string} abortInfo.breakKey 上传路径
* @return Promise<object>
*/
async breakPointResume(file, abortInfo: AbortInfoField) {
try {
// 初始化断点信息
const breakKey = abortInfo?.breakKey || '';
const breakUploadId = abortInfo?.breakUploadId || '';
// 当前桶
const currentBucket = this.config.bucket;
// 如果是断点续传,直接使用记录的名称
const fileName = breakKey;
// 如果是断点续传,不重新生成id
const uploadId = breakUploadId;
// 断点信息:获取已经上传的片段
const abortPartInfo = await this.getMultiPartList(breakUploadId, breakKey);
// 生成所有分片promise
const uploadPromises = await this.mockChunkUploadPromise(
file,
{ currentBucket, fileName, uploadId },
abortPartInfo,
);
const uploadResults = await Promise.all(uploadPromises);
// 合并当前完成的和之前完成的,用于最后一步合成文件
const partsInfo = this.mergePartInfo(uploadResults, abortPartInfo);
// 合成
const completePartCommand = this.baseCompleteMultipartUploadCommand(currentBucket, fileName, uploadId, partsInfo);
const res = await this.ossClient.send(completePartCommand);
// 触发完成
this.emit(MULTI_UPLOAD_EVENT.UPLOAD_PROGRESS, 1);
this.log('breakPointResume:ok', res);
return { status: 'success', response: { url: res.Location }, originalName: file.name };
} catch (error) {
this.log('breakPointResume:error', error);
return { status: 'fail', error: '上传失败', originalError: error, response: {}, originalName: file.name };
}
}
/**
* 合并分片信息,并按照切片顺序排序
* @param {array} uploadResult 当前上传完成的信息
* @param {array} abortPartInfo 中断之前已经上传的信息
* @return array
*/
mergePartInfo(uploadResult = [], abortPartInfo = []) {
const list = uploadResult.concat(abortPartInfo);
return sort2DArray(
list.map(({ ETag, PartNumber }) => ({
ETag,
PartNumber: Number(PartNumber),
})),
'PartNumber',
true,
);
}
/**
* 基础命令-获取当前任务已经上传的分片信息
* @param {string} bucket 桶
* @param {string} key 存储路径
* @param {string} uploadId 上传id
* @return object
*/
baseListPartsCommand(bucket, key, uploadId) {
return new ListPartsCommand({
Bucket: bucket,
Key: key,
UploadId: uploadId,
});
}
}
6.取消上传(只支持取消分片)
为何只支持取消分片上传?
上传文件和取消上传属于异步操作,小文件(非分片)上传可能速度非常快,取消上传的接口可能还没执行,文件已经传完了
如果需要支持普通上传怎么处理?
如果需要保证产品功能统一,非分片文件也需要支持取消的话,可以通过业务侧来实现: 点击取消时,记录当前文件唯一id,上传成功后再调用删除文件的方法
功能代码如下
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
ListPartsCommand,
PutObjectCommand,
S3Client,
UploadPartCommand,
} from '@aws-sdk/client-s3';
class S3Upload extends EventEmitter {
...
/**
* 取消分片上传
* @param {string} uploadId 上传id
* @param {string} key 存储路径
* @return Promise<void>
*/
async abortMultipartUpload(uploadId, key) {
if (uploadId) {
const abortCommand = this.baseAbortMultipartUploadCommand(this.config.bucket, key, uploadId);
await this.ossClient.send(abortCommand);
} else {
this.log('abortMultipartUpload:error', 'no uploadId');
}
}
/**
* 基础命令-取消分片上传
* @param {string} bucket 桶
* @param {string} key 存储路径
* @param {string} uploadId 上传id
* @return object
*/
baseAbortMultipartUploadCommand(bucket, key, uploadId) {
return new AbortMultipartUploadCommand({
Bucket: bucket,
Key: key,
UploadId: uploadId,
});
}
}
7.预签名方法
预签名方法遇到的问题
1.签名时间不能超过1周,修改配置的宏定义小于1周
2.报access Id 不在record中(服务端问题)
注意事项:临时签名生成的url如果直接存到数据库,会到了签名时间后就过期了,无法访问; 需要后端入库时不存带签名,查询返回数据时去签名
功能代码代码如下
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
ListPartsCommand,
PutObjectCommand,
S3Client,
UploadPartCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
class S3Upload extends EventEmitter {
...
/**
* 基础方法-预签名文件
* @param {string} key 存储路径
* @param {boolean} isEndPoint 是否为完成的地址
* @return Promise<string>
*/
async createPreSignedUrlWithClient(key, isEndPoint = false) {
try {
// 携带了endpoint
if (isEndPoint) {
key = key.replace(`${this.config.endpoint}/`, '');
}
const command = this.basePutObjectCommand({ Bucket: this.config.bucket, Key: key });
const result = await getSignedUrl(this.ossClient, command, { expiresIn: this.config.signUrlExpireTime });
this.log('createPreSignedUrlWithClient:ok', result);
return result;
} catch (err) {
this.log('createPreSignedUrlWithClient:error', err);
return '';
}
}
/**
* 基础命令-PutObjectCommand通用
* @param {object} input 输入对象信息
* @param {string} input.Bucket 桶
* @param {string} input.key 存储路径
* @return object
*/
basePutObjectCommand(input: PutObjectCommandField) {
return new PutObjectCommand(input);
}
}
上传组件(这个跟项目技术栈有关,以下都为伪代码,用于分享思路)
封装上传组件OssUploadFile.vue
- 这里可以基于自己项目用到的UI库,来封装;
- 实例化一个S3Upload上传类
<t-upload
ref="uploadRef"
v-model="fileList"
class="oss-upload"
:multiple="multiple"
:disabled="disabled"
:auto-upload="autoUpload"
:draggable="isDraggable"
:before-upload="beforeUpload"
:request-method="requestMethod"
:accept="fileTypeObj.mimeType"
@fail="handleError"
@success="handleSuccess"
@drop="handleDrop"
/>
....
// 分片上传返回的信息
const multiUploadInfo = reactive({
uploadId: '',
progress: 0,
key: '',
});
// 初始化
const initMethod = async () => {
// 获取配置
const ossConfig = await getOssConfig();
uploadInstance.value = new S3Upload(ossConfig);
const ossClient = await uploadInstance.value.getInstance();
// 注册分片上传事件
registerMultiUploadEvent();
};
// 监听分片上传信息
const registerMultiUploadEvent = () => {
uploadInstance.value.on(MULTI_UPLOAD_EVENT.UPLOAD_ID, ({ uploadId, fileName }) => {
multiUploadInfo.uploadId = uploadId;
multiUploadInfo.key = fileName;
});
uploadInstance.value.on(MULTI_UPLOAD_EVENT.UPLOAD_PROGRESS, (p) => {
updatePercentage(p * 100);
});
};
onMounted(()=>{
initMethod();
})
基于当前上传的文件判断使用普通上传,还是分片上传
根据定义的USE_PART_UPLOAD_LIMIT来判断是分片还是普通上传
....
// 自己的ui组件触发的自定义上传:
const requestMethod = async (file) => {
// 是否需要分片
isMultiPart.value = calculateIsMultipart(file.size);
// 开始上传
await startUploadMethod(file);
}
// 计算是否需要分片
const calculateIsMultipart= (fileSize)=> {
return fileSize > USE_PART_UPLOAD_LIMIT;
}
// 开始上传
const startUploadMethod = async (file) => {
if (isMultiPart.value) {
uploadResult.value = await uploadInstance.value.multipartUpload(file);
} else {
uploadResult.value = await uploadInstance.value.uploadFile(file);
}
};
普通上传自己mock进度
开始上传是就开始走进度,未成功前最大90%; 等文件上传成功则更新为100%;
....
// 普通上传模拟进度
const mockProcess = () => {
stopTick();
// 控制上传进度
let percent = 0;
mockInterval.value = setInterval(() => {
if (percent + 10 < 99) {
percent += 10;
updatePercentage(percent);
} else {
stopTick();
}
}, 100);
};
上传失败时如何进行重试(触发断点续传)
思路:
1.上传失败时记录失败的上下文
2.如果是失败了,就进行重试
3.循环执行上传,直到成功或者最大重试次数
代码如下:
...
// 自己的ui组件触发的自定义上传:
const requestMethod = async (file) => {
// 是否需要分片
isMultiPart.value = calculateIsMultipart(file.size);
// 更新状态
updateStatusHook(UPLOAD_VARS.progress, 'uploadProgress');
// 开始上传
await startUploadMethod(file);
// 失败并且重试次数小于3
if (isUploadResultError(uploadResult.value) && retryContext.count < RetryMaxNumber) {
// 如果是手动,直接返回失败
if (retryContext.isHand) {
retryContext.isHand = false;
return uploadResult.value;
}
// 循环
while (retryContext.count < RetryMaxNumber) {
// 开始上传
await startUploadMethod(file);
// 如果成功,跳出循环
if (isUploadResultOk(uploadResult.value)) {
break;
}
// 如果失败,延迟执行
// 这里可以做的更好一点,根据失败次数来定义延迟的时间,比如第一次失败后延迟5s,第二次10s;
if (isUploadResultError(uploadResult.value)) {
await sleep(retryContext.delayTime);
}
// 更新次数
retryContext.count += 1;
}
// 清空重试上下文
clearRetryContext();
return uploadResult.value;
}
return uploadResult.value;
};
// 开始上传
const startUploadMethod = async (file) => {
// 是否为重试
const isRetry = retryContext.count > 0;
if (isMultiPart.value) {
// 为断点续传
if (isRetry) {
uploadResult.value = await uploadInstance.value.breakPointResume(file, {
breakUploadId: multiUploadInfo.uploadId,
breakKey: multiUploadInfo.key,
});
} else {
uploadResult.value = await uploadInstance.value.multipartUpload(file);
}
} else {
// 开始模拟进度,
// 如果是重试,不模拟,防止进度从0开始
if (!isRetry) {
mockProcess();
}
uploadResult.value = await uploadInstance.value.uploadFile(file);
}
};
项目中应用成功效果图:
完整代码见如下git仓库:
yc-lm/document-helper (github.com)
参考文档
- Amazon S3官方文档地址:
docs.aws.amazon.com/zh_cn/code-…
- 阿里云 Browser.js分片上传 - 对象存储 OSS - 阿里云 (alibabacloud.com)
- EventMitter3 github.com/primus/even…
- p-limit github.com/sindresorhu…
转载自:https://juejin.cn/post/7413557963313397795