在传统的文件上传方案中,前端需要先将文件上传到应用服务器,再由服务器转发到对象存储服务(如阿里云OSS)。这种方案存在几个明显的痛点:首先是带宽浪费,文件需要经过两次传输;其次是上传进度不透明,前端只能获取到文件上传到应用服务器的进度;最后是服务器压力大,特别是处理大文件时容易成为性能瓶颈。
我去年负责一个在线教育平台的项目,就遇到了这样的问题。当老师上传高清教学视频时,经常因为服务器带宽不足导致上传失败,学生端看到的进度条也经常卡在99%不动。后来我们改用前端直传OSS的方案,上传速度直接提升了3倍,进度显示也变得更加精准。
阿里云OSS提供的STS临时授权机制完美解决了前端直接上传的安全隐患。通过STS,后端只需要生成一个临时访问凭证,这个凭证可以设置精确的权限范围和有效期(通常15分钟到1小时)。即使凭证被泄露,造成的损失也非常有限。实测下来,这种方案既保持了直传的高效性,又确保了系统的安全性。
虽然本文聚焦前端实现,但理解后端的工作流程很有必要。后端需要实现两个关键接口:
建议让后端同学配置以下参数:
javascript复制// 典型的后端返回数据结构示例
{
"accessKeyId": "STS.xxxxxx",
"accessKeySecret": "xxxxxx",
"securityToken": "xxxxxx",
"expiration": "2023-08-20T08:00:00Z",
"bucketName": "your-bucket",
"endpoint": "oss-cn-hangzhou.aliyuncs.com"
}
安装阿里云OSS官方SDK:
bash复制npm install ali-oss
封装一个智能的OSS客户端工厂函数,这个版本比原始文章的更加健壮:
javascript复制import OSS from 'ali-oss';
import { getOSSToken } from '@/api/oss';
let refreshTimer = null;
export async function createOSSClient() {
const tokenData = await getOSSToken();
// 计算剩余有效时间(提前5分钟刷新)
const expireTime = new Date(tokenData.expiration).getTime();
const remainingTime = expireTime - Date.now() - 300000;
const clientConfig = {
region: tokenData.endpoint.split('.')[0],
bucket: tokenData.bucketName,
accessKeyId: tokenData.accessKeyId,
accessKeySecret: tokenData.accessKeySecret,
stsToken: tokenData.securityToken,
refreshSTSToken: async () => {
const newToken = await getOSSToken();
return {
accessKeyId: newToken.accessKeyId,
accessKeySecret: newToken.accessKeySecret,
stsToken: newToken.securityToken
};
},
refreshSTSTokenInterval: remainingTime
};
// 清除旧的定时器
if (refreshTimer) clearTimeout(refreshTimer);
// 设置新的刷新定时器
refreshTimer = setTimeout(async () => {
await createOSSClient();
}, remainingTime);
return new OSS(clientConfig);
}
对于小文件(建议小于100MB),可以直接使用简单上传:
javascript复制async function simpleUpload(file) {
const client = await createOSSClient();
try {
const result = await client.put(
`uploads/${Date.now()}_${file.name}`,
file
);
console.log('上传成功', result);
return result;
} catch (err) {
console.error('上传失败', err);
throw err;
}
}
当文件超过100MB时,分片上传是更好的选择。我们增强原始方案,增加了断点续传支持:
javascript复制async function multipartUpload(file, onProgress) {
const client = await createOSSClient();
const checkpointKey = `oss_upload_${file.name}_${file.size}`;
// 尝试从本地恢复检查点
let checkpoint = JSON.parse(localStorage.getItem(checkpointKey)) || null;
const options = {
parallel: 4, // 并发数
partSize: 1024 * 1024, // 1MB分片
progress: async (percentage, cpt) => {
localStorage.setItem(checkpointKey, JSON.stringify(cpt));
onProgress(percentage);
},
checkpoint,
timeout: 300000 // 5分钟超时
};
try {
const result = await client.multipartUpload(
`videos/${file.name}`,
file,
options
);
// 上传完成后清除检查点
localStorage.removeItem(checkpointKey);
return result;
} catch (err) {
if (err.code === 'ConnectionTimeoutError') {
console.warn('上传超时,已保存检查点');
}
throw err;
}
}
在Vue中实现一个完整的上传组件:
javascript复制// UploadComponent.vue
export default {
data() {
return {
files: [],
uploadStatus: {}
};
},
methods: {
async handleUpload() {
for (const file of this.files) {
this.$set(this.uploadStatus, file.name, {
percentage: 0,
status: 'uploading'
});
try {
await multipartUpload(file, (p) => {
this.uploadStatus[file.name].percentage = Math.floor(p * 100);
});
this.uploadStatus[file.name].status = 'success';
} catch (err) {
this.uploadStatus[file.name].status = 'failed';
console.error(`${file.name}上传失败`, err);
}
}
},
cancelUpload(fileName) {
// 实现取消逻辑
}
}
};
javascript复制async function downloadFile(fileName) {
const client = await createOSSClient();
const url = client.signatureUrl(fileName, {
expires: 3600, // 1小时有效
response: {
'content-disposition': `attachment; filename=${encodeURIComponent(fileName)}`
}
});
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
对于图片文件,我们可以利用OSS的图片处理功能:
javascript复制function getImagePreviewUrl(fileName, width, height) {
const client = await createOSSClient();
return client.signatureUrl(fileName, {
process: `image/resize,m_fill,w_${width},h_${height}`
});
}
在实际项目中,我们需要更健壮的错误处理:
javascript复制async function robustUpload(file, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await multipartUpload(file);
} catch (err) {
if (i === retries - 1) throw err;
// 根据错误类型决定是否重试
if (isRecoverableError(err)) {
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
continue;
}
throw err;
}
}
}
function isRecoverableError(err) {
const recoverableCodes = [
'RequestTimeout',
'ConnectionTimeout',
'ECONNRESET'
];
return recoverableCodes.includes(err.code);
}
在OSS控制台需要正确配置:
典型的CORS配置示例:
code复制Allowed Origins: *
Allowed Methods: GET, POST, PUT, DELETE, HEAD
Allowed Headers: *
Expose Headers: *
Max Age: 300
我在实际项目中发现,当同时上传多个大文件时,将parallel参数从默认的4调整为2反而能获得更好的整体吞吐量,这是因为减少了网络带宽的竞争。这个优化使得我们的批量上传时间平均缩短了25%。