微信小程序文件,图片直传阿里云 OSS 的最佳实践
微信小程序直传阿里云 OSS 的实践总结
做小程序图片上传时,最容易踩坑的不是选图 UI,而是鉴权、签名、实际上传、回调收尾这一整条链路。下面整理一套在生产环境跑通过的做法,接口路径和业务字段已做脱敏,重点讲流程和容易翻车的地方。
整体思路
小程序端不持有 AccessKey,也不自己算 OSS 签名。正确姿势是:
- 前端选文件、算 MD5、组装元信息
- 调自家后端「上传鉴权」接口,拿到 OSS 上传凭证
- 根据后端返回的上传方式,走表单上传 / 预签名 PUT / 分片 PUT
- OSS 上传成功后,再调后端「完成上传」接口,拿到可读 URL 和 fileId
这样密钥只在服务端,小程序只拿临时 policy、signature 或预签名 URL。
选图 → 算 MD5 → 鉴权接口 → 直传 OSS → 完成上传接口 → 回写业务表单
第一步:选文件与本地预处理
选图
用 wx.chooseImage,注意 count 要扣掉已选数量,别让用户超上限。
wx.chooseImage({
count: remainCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success(res) {
// 把 tempFiles 转成内部 fileList 结构
}
});
本地文件建议单独标记 isLocal: true,和已上传的远程图分开管理,避免重复上传。
算 MD5
上传前先调 wx.getFileInfo,digestAlgorithm 设为 md5:
wx.getFileInfo({
filePath,
digestAlgorithm: 'md5',
success(res) {
const fileMd5 = res.digest;
}
});
MD5 有两个用处:
- 传给后端做秒传 / 去重(后端若发现文件已存在,可直接返回成功,不用再传 OSS)
- 拼进文件名,降低重名概率
生成 OSS 对象名
别直接用微信临时路径当文件名。常见做法:
{业务前缀}_{md5前12位}_{时间戳}_{三位随机数}.{后缀}
后缀从原路径截取,MIME 类型也按后缀推断(jpg/png/gif/webp)。
第二步:调鉴权接口
鉴权请求体大致包含这些字段(字段名按你们后端约定来):
| 字段 | 说明 |
|---|---|
| bucket_pre | 公有/私有桶标识,如 public / private |
| file_dir | OSS 目录,如 weixin/images |
| file_name | 上一步生成的对象名 |
| file_md5 | 文件 MD5 |
| file_size | 字节大小 |
| type | MIME,如 image/jpeg |
| part_size / chunk_count | 分片相关,小图通常 chunk_count=1 |
鉴权响应里重点关注:
| 字段 | 含义 |
|---|---|
| upload_status | 上传状态码,决定是否还要传 OSS |
| server_type | 存储类型,值为 oss 时走 OSS 直传 |
| file_path | OSS 对象 key |
| file_id | 业务文件 ID,收尾接口要用 |
| host / bucket / region | 拼 OSS 上传域名 |
| policy / signature / access_id | 表单上传凭证(V1) |
| security_token | STS 临时凭证 |
| chunk_urls | 分片预签名 URL 列表 |
upload_status 的快速路径
生产里后端常会返回这几种状态(数字仅作示例,以你们接口为准):
- 已存在、待收尾:文件在 OSS 有了,前端跳过直传,直接调「完成上传」
- 完全秒传:连收尾都省了,直接返回 read_path / file_id
这两种情况能省不少流量和时间,前端一定要判,别无脑再传一遍。
第三步:实际上传 OSS
后端返回 type === 'oss' 时,按优先级依次尝试:
方式一:表单 POST(推荐,小文件首选)
条件:host + file_path + policy + signature + accessKeyId 齐全,且不是 OSS V4 签名。
用 wx.uploadFile,不要用 wx.request:
wx.uploadFile({
url: 'https://{bucket}.{region}.aliyuncs.com',
filePath: tempPath,
name: 'file',
formData: {
key: file_path,
OSSAccessKeyId: access_id,
policy: policy,
signature: signature,
success_action_status: '200',
'x-oss-security-token': security_token // STS 场景必传
},
timeout: 30000
});
成功状态码一般是 200 或 204。uploadFile 自带进度回调,UI 好做。
V4 签名字段不同,formData 要换成:
key
policy
x-oss-signature-version
x-oss-credential
x-oss-date
x-oss-signature
x-oss-security-token(如有)
success_action_status
V4 和 V1 不能混用字段,判错了一直 403。
方式二:预签名 PUT
鉴权接口直接给了带 signature= / x-oss-signature= / security-token= 的 URL 时,用 PUT 整文件上传:
const fs = wx.getFileSystemManager();
fs.readFile({
filePath: tempPath,
success(readRes) {
wx.request({
url: presignedPutUrl,
method: 'PUT',
data: readRes.data,
header: {
'Content-Type': 'image/jpeg',
'x-oss-security-token': security_token // URL 里没带 token 时才加 header
}
});
}
});
注意:
- PUT 要先
readFile读进内存,大图会占内存,图片场景一般还能接受,视频就要考虑分片 Content-Type要和签名时一致,否则验签失败- 域名必须是 https
方式三:二次要签名
鉴权响应里没有完整 policy,但有 file_path,再调一次「获取签名」接口,拿到 policy/signature 后仍走表单上传。这是兜底路径,别把它当主流程,否则多一次 RTT。
方式四:分片 PUT(非 OSS 或后端自建分片)
server_type 不是 oss,或返回了 chunk_urls 数组时,按分片顺序 PUT:
fs.readFile({
filePath,
position: start,
length: chunkSize,
success(readRes) {
wx.request({
url: chunkItem.upload_url,
method: 'PUT',
data: readRes.data,
header: { 'Content-Type': 'application/octet-stream' }
});
}
});
分片大小常见 15MB,按 file_size / chunk_size 算片数,顺序上传,每片成功再传下一片,最后调完成接口。
第四步:完成上传
OSS 返回 200/204 只代表对象进去了,业务库里的文件记录通常还没落。必须再调「完成上传」接口,把 file_id、md5、路径等传回去,拿到:
read_path/url:前端展示、表单提交用file_id:业务关联用view_path:有时和 read_path 不同(CDN、鉴权访问等)
漏掉这一步,后台看不到文件,表单提交也会缺 fileId。
小程序侧必配项
1. 服务器域名白名单
微信公众平台 → 开发 → 开发管理 → 开发设置 → 服务器域名:
- uploadFile 合法域名:OSS 的 bucket 域名,如
https://xxx.oss-cn-hangzhou.aliyuncs.com - request 合法域名:自家 API 域名 + 若用 PUT 直传 OSS,OSS 域名也要加
域名不对的表现:uploadFile:fail url not in domain list,不是代码 bug。
2. 只用 HTTPS
OSS 和小程序都要求 HTTPS,代码里把 http:// 统一替换成 https://。
3. 超时
单文件上传建议设 30s 超时,超时后标记失败、允许重试,别一直 loading。
容易踩的坑
1. 用 wx.request 模拟 multipart 表单
表单上传请用 wx.uploadFile。自己拼 multipart body 在微信里又麻烦又容易错 boundary。
2. V1 / V4 签名混用
鉴权响应里如果出现 x-oss-signature-version 且含 OSS4,就不能再走 OSSAccessKeyId + policy + signature 那套 V1 formData。
3. STS 漏传 token
临时凭证场景,x-oss-security-token 表单字段或 PUT header 缺了必 403。预签名 URL 里已带 token 就别重复加 header。
4. Content-Type 不一致
PUT 上传时 header 里的类型必须和签名时一致,jpg 就传 image/jpeg,别默认 octet-stream(分片除外)。
5. 大文件 readFile 爆内存
整文件 PUT 会把文件读进内存。图片压缩后一般 OK;大视频要分片或走后端中转。
6. 只传 OSS 不调完成接口
对象在桶里有了,业务系统没记录,等于白传。
7. 文件名 / key 重复
纯时间戳命名高并发会撞名。MD5 + 随机数能缓解,最终仍以 OSS 返回为准。
8. 进度条假死
uploadFile 有 onProgressUpdate;PUT 分片要自己按已传字节算百分比。
9. 秒传逻辑没接
后端都返回「文件已存在」了,前端还在传 OSS,浪费用户流量和时间。
组件层的一些实用细节
如果封装成上传组件,这几件事值得做:
- 本地 / 远程分状态:
isLocal标记待传文件,已有fileId的跳过 - 外部值同步加锁:
value/netImgs/netFileIds多个 prop 互相触发 observer 时,用isSyncing防循环 setData - 批量顺序上传:
for...await一张一张传,比并发更稳,OSS 和小程序连接数都有限 - 统一 emit:上传完抛
change(urls + fileIds)和uploadComplete,表单页只关心结果 - 失败可重试:失败把
uploading置 false、progress归零,别卡在半成功状态
后端需要配合什么
前端能跑通,后端至少提供三个能力:
- 鉴权:根据 md5/大小/目录生成 OSS key,返回上传凭证或秒传结果
- 签名(可选):V4 或动态 policy 时单独签发
- 完成上传:OSS 对象落库,返回可读地址和 fileId
密钥、RAM 角色、STS 有效期、bucket 策略都在服务端配,别下发到小程序。
最小可用流程(伪代码)
async function uploadOne(tempPath, meta) {
const md5 = await getFileMd5(tempPath);
const fileName = buildFileName(tempPath, md5);
const auth = await api.post('/upload/auth', {
file_md5: md5,
file_name: fileName,
file_size: meta.size,
file_dir: 'wx_app/images',
type: meta.mime
});
if (auth.upload_status === 'INSTANT') {
return auth; // 秒传
}
if (auth.server_type === 'oss') {
if (hasFormPolicy(auth)) {
await ossFormUpload(tempPath, auth);
} else if (hasPresignedUrl(auth)) {
await ossPutUpload(tempPath, auth);
} else {
const sign = await api.post('/upload/sign', { file_path: auth.file_path });
await ossFormUpload(tempPath, { ...auth, ...sign });
}
} else {
await chunkPutUpload(tempPath, auth.chunk_urls);
}
return api.post('/upload/finish', { file_id: auth.file_id, file_md5: md5 });
}
小结
微信小程序传 OSS,核心就一句话:后端签发、前端直传、完成后回调。
选型上,小图优先 wx.uploadFile 表单 POST;有预签名 URL 再用 PUT;大文件走分片。上线前把 OSS 域名加进白名单、STS token 传对、完成接口接上,这三件做到位,大部分 403 和「传了但保存不了」的问题都能避开。
如果你也在做类似组件,建议先把鉴权响应打日志(脱敏后)看清楚后端到底走哪条分支,再写上传逻辑,比一上来硬套官方 demo 省时间得多。
更多推荐
所有评论(0)