一、整体功能概述
这个文件封装了一个阿里云 OSS 上传工具类
Upload,主要能力有:
- 初始化 OSS 客户端(带 STS token 自动刷新)
- 支持小文件直接上传(
put) - 支持大文件分片上传 + 断点续传(
multipartUpload+localStorage记录 checkpoint) - 支持取消上传
- 检查断点是否在 OSS 上仍有效(
listUploads) - 根据 OSS 对象路径生成带签名的下载链接,支持触发浏览器下载
- 提供一个简化入口 getUploadInstance(busiType, isCostRight) 返回已初始化好的实例
二、具体实现
import { getOssSign } from '@/api/common'; const nanoid = require('nanoid'); import { useUserStoreWithOut } from '@/store'; import { format } from 'date-fns';
const defaultConf = { region: 'oss-cn-hangzhou', //todo 是否要改名 bucket: 'yx-local', }; const dateStr = format(new Date(), 'yyyy_MM_dd');
const tenantId = useUserStoreWithOut().vuex_tenantId ||localStorage.getItem('vuex_tenantId');
const env = process.env.npm_config_env || 'test';
const buckPathBase = my/${env}/${tenantId}/${dateStr}/;
const uploadOptions = {
parallel: 5,// 设置并发上传的分片数量。
partSize: 2 * 1024 * 1024,// 设置分片大小。默认值为1 MB,最小值为100 KB。
// headers,
// 自定义元数据,通过HeadObject接口可以获取Object的元数据。
};
export default class Upload {
constructor(options) {
const { busiType, isCostRight } = options;
if (!busiType) {
throw new Error('请传入业务类型');
}
const bucketPath = `${buckPathBase}${busiType}/`;
this.options = { ...defaultConf, ...options, bucketPath, isCostRight };
}
// 初始化oss实例
async initOssClient() {
try {
const accessConf = await this.getStsToken();
const { region, bucket } = this.options;
this.client = new OSS({
// yourRegion填写Bucket所在地域。以华东1(杭州)为例,yourRegion填写为oss-cn-hangzhou。
region,
bucket,
timeout: 120000, // 2min
...accessConf,
refreshSTSToken: async () => {
return await this.getStsToken();
},
refreshSTSTokenInterval: 300000, // 5min刷一次
});
} catch (error) {
console.log('初始化client oss失败', error);
this.client = null;
}
}
async getStsToken() {
const { accessKeyId, accessKeySecret, securityToken } = await getOssSign({
isUse: this.options.isCostRight,
});
return {
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
// 从STS服务获取的安全令牌(SecurityToken)。
stsToken: securityToken,
};
}
// 取消上传
cancel() {
this.client.cancel();
}
/**
*
* @param {File} file 待上传的文件
* @param {object} param1 上传配置信息
* @param {number} retryTimes 可重试上传次数
* @returns
*/
getFileExtension(file) {
const { name, type } = file;
const dotIndex = name.lastIndexOf('.');
if (dotIndex === -1 || dotIndex === 0) return type?.split('/')?.[1]; // 没有后缀或文件名以点开头
return name.substring(dotIndex + 1);
}
async upload(file, { progressCb, ...restOption }) {
const extName = this.getFileExtension(file);
const uniquKey = nanoid.nanoid(16);
let bucketUniqueFileName = `${uniquKey}.${extName}`;
if (file.size < uploadOptions.partSize) {
if (restOption?.customPath) {
//自定义上传路径
return await this.commonUpload(file, this.options.bucketPath + restOption.customPath);
} else {
return await this.commonUpload(file, this.options.bucketPath + bucketUniqueFileName);
}
} else {
return new Promise(async (resolve, reject) => {
try {
const storageKey = `${file.name}-${file.lastModified}-${file.size}`;
const option = {
...uploadOptions,
...restOption,
progress: (p, cpt, res) => {
// 缓存记录每个断点分片信息。
const checkpointMapInfo = {
bucketUniqueFileName,
cpt,
};
// todo 设置localstorage过期时间
localStorage.setItem(storageKey, JSON.stringify(checkpointMapInfo));
// 上传完成之后晴空断点信息。
if (p * 100 == 100) {
localStorage.removeItem(storageKey);
}
!!progressCb && progressCb(p * 100);
},
};
if (localStorage.getItem(storageKey)) {
// 命中断点续传缓存
const checkpointMapInfo = JSON.parse(localStorage.getItem(storageKey));
bucketUniqueFileName = checkpointMapInfo.bucketUniqueFileName;
const hasUpload = await this.checkOssHasUploads(checkpointMapInfo?.cpt?.uploadId);
if (hasUpload) {
// 注意:碎片过期时间是3天,如果当前断点信息无对应上面id, 则重新上传
option.checkpoint = checkpointMapInfo.cpt;
}
}
let loadPath = this.options.bucketPath + bucketUniqueFileName;
if (option.customPath) {
//自定义上传路径
loadPath = this.options.bucketPath + option.customPath;
}
this.client
.multipartUpload(loadPath, file, option)
.then(res => {
resolve(res);
})
.catch(e => {
console.log(e, 777777);
Object.keys(e).forEach(key => {
console.log(e[key], key);
});
if (e.name.indexOf('ConnectionTimeoutError') > -1 || e.name.indexOf('RequestError') > -1) {
reject({
message: '网络超时或者中断,请检查当前网络',
type: 'timeout',
});
} else if (e.name.indexOf('abort') > -1) {
localStorage.removeItem(storageKey);
reject({
message: '上传过程出现异常中断,请重试',
type: 'abort',
});
}
});
} catch (error) {
console.log(error);
}
});
}
}
// 普通上传
async commonUpload(file, bucketPath) {
return new Promise((resolve, reject) => {
this.client
.put(bucketPath, file)
.then(res => {
resolve(res);
})
.catch(e => {
console.log(e, '===commonUpload====');
if (e.name.indexOf('ConnectionTimeoutError') > -1) {
reject({
message: '网络超时或者中断,请检查当前网络',
type: 'timeout',
});
}
});
});
}
/**
*
* @param {number} uploadId 上传文件id
* @returns oss上是否有对应(断点)碎片信息
*/
async checkOssHasUploads(uploadId) {
try {
const result = await this.client.listUploads({
'max-uploads': 1,
'upload-id-marker': uploadId,
});
return result.uploads?.length > 0;
} catch (error) {
console.log(error);
return false;
}
}
// 下载文件 后端存bucket路径,不存完整路径,通过签名生成完整路径(实效性),防止资源盗刷
getSignatureUrl(filePath, filename, isDownload) {
// 配置响应头实现通过URL访问时自动下载文件,并设置下载后的文件名。
const response = {
'content-disposition': `attachment; filename=${encodeURIComponent(filename)}`,
};
// 填写Object完整路径。Object完整路径中不能包含Bucket名称。
const url = this.client.signatureUrl(filePath, { response });
if (isDownload) {
const link = document.createElement('a');
link.style.display = 'none';
// 设置下载地址
link.setAttribute('href', url);
// 设置文件名
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
return url;
}
}
}
export const getUploadInstance = async (busiType, isCostRight = true) => {
const uploadInstance = new Upload({
busiType,
isCostRight,
});
// 工具upload类构造函器不许使用await
await uploadInstance.initOssClient();
return uploadInstance;
};