腾讯云COS实战优化上传性能与成本控制技巧
我的写法,亲测靠谱
最近三个项目都在用腾讯云COS传文件,前端这块我基本固定了一套流程。先说结论:别直接在前端生成签名,也别把SecretKey扔进代码里,真有人这么干,我见过生产环境挂着SecretKey跑半年的,太吓人了。
我现在通用做法是:前端只负责上传逻辑和SDK调用,所有签名、token、临时密钥全部走自己后端接口拿。这样虽然多一次请求,但安全得多,而且后续权限控制也灵活。
核心代码其实就这几行:
import { CosSdk } from 'cos-js-sdk-v5';
async function uploadToCos(file, onProgress) {
const { credentials, bucket, region } = await fetch('https://jztheme.com/api/cos/sts').then(res => res.json());
const cos = new CosSdk({
getAuthorization: async (options, callback) => {
// 这里再请求一次签名,确保每次操作都有独立签名
const { authorization } = await fetch('https://jztheme.com/api/cos/sign', {
method: 'POST',
body: JSON.stringify({ pathname: options.Pathname || options.Key })
}).then(res => res.json());
callback(authorization);
}
});
return new Promise((resolve, reject) => {
cos.putObject({
Bucket: bucket,
Region: region,
Key: uploads/${Date.now()}_${file.name},
Body: file,
onProgress: (info) => {
onProgress?.(info.percent);
}
}, (err, data) => {
if (err) reject(err);
else resolve(https://${bucket}.cos.${region}.myqcloud.com/${data.Location});
});
});
}
这里的关键是 getAuthorization 不直接返回临时密钥,而是每次操作都向后端要一个签名。很多人图省事直接把临时密钥填进去,像这样:
// ❌ 危险写法,别学
const cos = new CosSdk({
SecretId: credentials.tmpSecretId,
SecretKey: credentials.tmpSecretKey,
SecurityToken: credentials.sessionToken
});
这种写法问题很大。一旦用户F12翻出来,临时凭证泄露,攻击者就能拿着它去调用COS API,哪怕你设置了过期时间(一般15分钟),在这期间也能干不少坏事——删文件、读私有资源、疯狂上传刷你费用。
而用 getAuthorization 每次动态获取签名,至少前端看不到完整密钥,后端还能做频率限制、路径白名单、IP校验等风控。
这几种错误写法,别再踩坑了
我见过最离谱的是把永久SecretKey写在前端代码里,部署到线上,GitHub上一搜一大把。你以为是private repo?但浏览器里明文暴露,等于公开。
还有一种常见误区:以为用了临时密钥就绝对安全。错。临时密钥如果权限太大,比如给了PutObject和DeleteObject全bucket权限,那照样能被滥用。
举个真实案例:我们合作方的一个H5上传头像功能,后端给的临时密钥权限是整个user/目录可读写,结果被人写了个脚本批量上传垃圾文件,一个月刷了几百G存储,账单直接爆了。
正确姿势是:后端按需授权,比如这次上传只允许user/${uid}/avatar.jpg这个路径写入,别的都不行。可以用 CAM 策略控制:
{
"version": "2.0",
"statement": [
{
"effect": "allow",
"action": ["name/cos:PutObject"],
"resource": ["qcs::cos:ap-beijing:uid/123456789:prefix/appid/987654321/user/10086/avatar.jpg"]
}
]
}
虽然配置麻烦点,但出问题的概率小多了。
大文件上传,别卡界面
一开始我们没处理分片,用户传个100M视频,页面直接卡死,进度条不动,Network里一堆pending。后来加上分片上传,体验好太多了。
COS SDK 默认支持分片,但得手动设阈值。我一般这样初始化:
const cos = new CosSdk({
getAuthorization: getAuthFromBackend,
ChunkSize: 1024 * 1024 * 5, // 5MB分片
AsyncLimit: 3 // 并发3个请求
});
注意不要设太高并发,不然用户网络差的时候容易集体超时。实测3-5个比较稳。另外建议加个本地缓存记录已上传的chunk,防止用户刷新页面后重头再来——不过这个SDK不自带,得自己实现。
我当时折腾了半天想用IndexedDB记状态,最后发现太重了,干脆用内存对象 + 刷新提醒更实际:“检测到上次上传未完成,刷新将重新开始”。
移动端拍照上传,坑比想象多
iOS Safari有个经典问题:直接从相机拍的照片,orientation信息会丢,导致竖着拍的照片显示成横的。解决方案是在上传前先用canvas draw一下,自动修正方向。
我封装了个函数:
function fixOrientation(file, callback) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
canvas.toBlob(callback, file.type);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 使用
fixOrientation(rawFile, processedBlob => {
uploadToCos(processedBlob, updateProgress);
});
这个步骤加了之后,安卓iOS都能正常显示了。不然用户拍完一看歪了,以为是你们bug,其实不是。
上传进度条别瞎搞
很多人直接用onProgress返回的percent当UI进度,但你会发现它跳得很奇怪,一会儿90%,然后掉回30%。这是因为分片上传时,每个part的状态是异步返回的,SDK计算方式是“已确认上传的字节数 / 总大小”,而某些part可能延迟确认。
我现在的做法是:UI上显示“稳定增长型”进度。维护一个内部变量,只增不减:
let lastReportedPercent = 0;
cos.putObject({
// ...config
onProgress(info) {
const current = info.percent;
if (current > lastReportedPercent) {
lastReportedPercent = current;
updateUI(lastReportedPercent); // UI只会看到稳步上升
}
}
});
用户体验反而更好,毕竟没人喜欢进度倒退。
实际项目中的坑
- 跨域问题:记得在COS控制台配好CORS规则,允许你的域名、GET/PUT、Content-Type等。不然连签名都拿不到。
- 文件名中文乱码:Key里别直接拼中文名,最好用UUID代替,或者用encodeURIComponent处理。
- 上传后立刻访问404:COS有缓存,特别是CDN加速场景下,建议上传完成后延迟几秒再展示,或者主动刷新CDN缓存。
- STS token过期:默认15分钟,上传超大文件要注意。可以在
getAuthorization里判断是否快过期,提前刷新。
还有一个细节:上传成功后的URL,不要拼接https://bucket.cos.region.myqcloud.com/key,应该直接用返回的data.Location,避免手误拼错。
结语
以上就是我这几个项目下来总结的一套COS上传实践。改完之后没再出现过大面积上传失败或安全事件。当然也不是完美方案,比如断点续传还没完全自动化,但目前够用。
如果有更好的实现方式,比如怎么优雅处理断点、如何结合Web Worker提升性能,欢迎评论区交流。

暂无评论