文件上传是 Web 应用的基础功能,但很多团队的实现方式是:前端上传到后端服务器 → 后端再转存到对象存储。这种方案简单直接,但文件流量全部经过后端,带宽成本高、服务器压力大。我们在实际项目中采用了浏览器直传 + CDN 的混合架构,大幅降低了服务端负担。
传统方案的问题
用户浏览器 → 后端服务器 → 对象存储(OSS/COS)
一个 10MB 的图片要经过后端中转,意味着:
- 后端需要处理 10MB 的请求体,占用内存和带宽
- 上传速度受限于「用户→后端」和「后端→OSS」两段中较慢的那段
- 并发上传时后端可能成为瓶颈
浏览器直传方案
核心思路是让浏览器直接上传到对象存储,后端只负责签发临时凭证:
用户浏览器 → 对象存储(直传)
↑
后端签发 STS 临时凭证
后端:签发临时凭证
以腾讯云 COS 为例,后端通过 STS(Security Token Service)生成有限制的临时密钥:
// API: GET /api/upload/credentials
export async function GET() {
const credentials = await getStsCredential({
secretId: process.env.COS_SECRET_ID,
secretKey: process.env.COS_SECRET_KEY,
durationSeconds: 1800, // 30 分钟有效
policy: {
statement: [{
effect: 'allow',
action: ['cos:PutObject', 'cos:PostObject'],
resource: [`qcs::cos:${region}:uid/${appId}:${bucket}/uploads/*`],
}],
},
});
return Response.json(credentials);
}
关键安全设计:
- 临时凭证有效期只有 30 分钟
- 权限限定为
PutObject和PostObject,不能删除或列举 - 资源路径限定在
/uploads/*目录下,不能写到其他位置
前端:直传上传
前端拿到临时凭证后,直接上传到 COS:
async function uploadFile(file: File) {
// 1. 获取临时凭证
const creds = await fetch('/api/upload/credentials').then(r => r.json());
// 2. 生成存储路径(按日期分目录)
const date = new Date().toISOString().slice(0, 10);
const key = `uploads/${date}/${generateId()}_${file.name}`;
// 3. 直传到 COS
await cos.putObject({
Bucket: bucket,
Region: region,
Key: key,
Body: file,
Headers: {
'x-cos-security-token': creds.sessionToken,
},
});
// 4. 返回相对路径(不带域名)
return key;
}
数据库存相对路径,展示时拼 CDN 域名
这是一个很容易被忽视但极其重要的设计:数据库中只存储相对路径,不存完整 URL。
✅ 数据库存储: uploads/2026-01-24/abc123_photo.jpg
❌ 数据库存储: https://cdn.example.com/uploads/2026-01-24/abc123_photo.jpg
为什么?因为 CDN 域名可能会变。切换云服务商、更换 CDN 加速域名、甚至同一个存储桶绑定多个域名(国内/海外分开加速),如果数据库存了完整 URL,迁移时要批量更新所有记录。
展示时再拼接:
function getImageUrl(relativePath: string): string {
const cdnBase = process.env.CDN_BASE_URL; // https://cdn.example.com
return `${cdnBase}/${relativePath}`;
}
CDN 图片处理
主流云服务商的 CDN 都支持 URL 参数实时处理图片,不需要提前生成缩略图:
function getThumbnail(path: string, width: number): string {
const base = getImageUrl(path);
// 腾讯云万象:缩放到指定宽度,自动 WebP
return `${base}?imageMogr2/thumbnail/${width}x/format/webp`;
}
// 阿里云 OSS 风格:
// ${base}?x-oss-process=image/resize,w_${width}/format,webp
列表页用小图(200px 宽),详情页用中图(800px),点击放大用原图。同一张图片根据场景返回不同尺寸,带宽节省非常明显。
富文本中的 URL 处理
如果项目有富文本编辑器(如文章编辑),上传的图片会以完整 URL 嵌入 HTML 中。保存时需要将完整 URL 转回相对路径,读取时再转回完整 URL:
// 保存前:完整 URL → 相对路径
function normalizeContent(html: string): string {
return html.replace(
new RegExp(`${CDN_BASE_URL}/`, 'g'),
''
);
}
// 读取后:相对路径 → 完整 URL
function renderContent(html: string): string {
return html.replace(
/(src=["'])(uploads\/)/g,
`$1${CDN_BASE_URL}/$2`
);
}
总结
浏览器直传 + CDN 的架构核心是三个分离:上传流量不经后端、存储路径不含域名、图片处理交给 CDN。这套方案在我们的项目中将文件上传相关的服务器带宽降低了 90% 以上,同时图片加载速度因为 CDN 缓存和就近访问提升了 3-5 倍。对于任何有文件上传需求的 Web 应用,都值得采用这种架构。