在微信小程序中接入 Cloudbase 云存储上传
一句话定义:登录之后,用
wx.chooseMedia选图片或视频,经app.uploadFile推到 Cloudbase 云存储,把返回的fileID存数据库,渲染时再用app.getTempFileURL换成可直接<image>渲染的临时链接,完整链路。预计耗时:30 分钟 | 难度:进阶
适用场景
- 适用:已经接完 add-auth-wechat-miniprogram 的小程序,要做头像 / 商品图 / 订单凭证 / 短视频上传
- 适用:用
@cloudbase/js-sdk+@cloudbase/adapter-wx_mp接独立 Cloudbase 环境的玩法。本文不是给微信·云开发(wx.cloud)用的,那一套有自己的wx.cloud.uploadFile,接口形似但凭据体系不同 - 不适用:大文件分片上传(超过 100MB 的视频),建议先压缩或者分片上传到 COS 再回写 fileID
- 不适用:需要前端直接拿到永久 URL 的场景(默认是公有读才永久)。私有读文件每次访问都要换临时链接
环境要求
| 依赖 | 版本 |
|---|---|
@cloudbase/js-sdk | 2.27.3 |
@cloudbase/adapter-wx_mp | 1.3.1 |
| 微信开发者工具 | ≥ 1.06.x |
另外需要:
- 已完成 add-auth-wechat-miniprogram,
auth.hasLoginState()返回 true - 控制台「云存储」已开通,默认会有一个空 bucket
app.json里requiredPrivateInfos加上chooseMedia(必要),否则在审核版本上选媒体会失败
第一步:选媒体
wx.chooseMedia 是微信官方推荐的统一选媒体接口,替代了老的 chooseImage / chooseVideo。
// pages/upload/upload.js
async function pickMedia() {
const res = await new Promise((resolve, reject) => {
wx.chooseMedia({
count: 9, // 最多 9 张
mediaType: ['image'], // 只选图,要视频改成 ['video'] 或 ['mix']
sourceType: ['album', 'camera'], // 相册 + 拍照
sizeType: ['compressed'], // 压缩版,节省流量
success: resolve,
fail: reject,
});
});
return res.tempFiles;
}
返回的每个 tempFiles[i] 形如:
{
tempFilePath: 'http://tmp/...', // 小程序本地临时路径
size: 12345, // 字节
fileType: 'image',
width: 1080,
height: 1920,
}
第二步:上传到云存储
每个临时文件调一次 app.uploadFile。这里的 app 是 add-auth-wechat-miniprogram 第四步初始化的那个 Cloudbase app。
import app from '../../libs/cloudbase';
import { ensureLogin } from '../../libs/login';
async function uploadOne(tempFilePath, openid) {
const ext = tempFilePath.split('.').pop() || 'jpg';
// cloudPath 用「openid 路径 + 时间戳 + 随机数」组合,防冲突 + 后面好做权限隔离
const cloudPath = `users/${openid}/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
const res = await app.uploadFile({
cloudPath,
filePath: tempFilePath, // 小程序里直接传 tempFilePath
});
return res.fileID; // 'cloud://your-env.bucket/users/openid/..../xxx.jpg'
}
export async function uploadAll(tempFiles) {
const user = await ensureLogin();
const openid = user.customUserId; // 或 user.uid
// 串行上传,易于错误处理。要并行可换 Promise.all,但要注意小程序对并发请求数有上限
const fileIDs = [];
for (const f of tempFiles) {
const id = await uploadOne(f.tempFilePath, openid);
fileIDs.push(id);
}
return fileIDs;
}
cloudPath 命名上几个易疏漏的点:
- 不要以
/开头。Cloudbase 文件名规范禁止根目录起斜杠 - 不要出现
//。两个连续斜杠会被拒 - 中文文件名能存,但访问时会被 URL Encode,排查问题时不直观,建议统一英文 + 数字
- 用
openid/uid做目录前缀,后面配安全规则才好写「用户只能动自己目录下的文件」
第三步:把 fileID 存数据库
上传成功后拿到 fileID,业务侧通常把它写到对应的业务集合:
import { db } from '../../libs/cloudbase';
async function saveOrderImages(orderId, fileIDs) {
await db.collection('orders').doc(orderId).update({
images: fileIDs, // 直接存数组
updatedAt: db.serverDate(),
});
}
为什么存 fileID 不存 URL:
- URL 可能过期(私有读文件的临时链接有有效期),fileID 是永久不变的
- 公有读 / 私有读权限切换时,fileID 不动,业务代码不用改
- 不同环境(测试 / 正式)迁移时,fileID 也好替换
第四步:渲染时换临时链接
读出来的是一组 fileID,直接放到 <image src> 里不行——<image> 只认 https/http,不认 cloud://。要先调 getTempFileURL 换成 https 链接。
import app from '../../libs/cloudbase';
export async function resolveImages(fileIDs) {
if (!fileIDs?.length) return [];
// 一次最多换 50 个
const res = await app.getTempFileURL({
fileList: fileIDs,
});
// res.fileList[i] 形如:
// { fileID: 'cloud://...', tempFileURL: 'https://...', maxAge: 7200 }
// 公有读权限的 fileList[i] 中 tempFileURL 永久有效
return res.fileList.map((f) => f.tempFileURL);
}
页面侧:
Page({
data: { imageUrls: [] },
async onLoad({ orderId }) {
const order = await db.collection('orders').doc(orderId).get();
const urls = await resolveImages(order.data[0].images);
this.setData({ imageUrls: urls });
},
});
WXML:
<image
wx:for="{{imageUrls}}"
wx:key="index"
src="{{item}}"
mode="aspectFill"
/>
注意点:
getTempFileURL一次最多 50 个 fileID,超过分批- 私有读文件的
tempFileURL是限期的,有效期可以在控制台或安全规则里设置,前端不要做长时间缓存(超过有效期再用会 403)。简单做法是每次进页面重新换一次 - 公有读文件返回的链接通常不会过期,可以前端缓存
第五步:用安全规则限制权限
默认权限模式下,经过身份认证的用户都能上传到自己目录,但别人也能下载读取(默认公有读)。如果是私密内容(凭证、对话图片等),要把权限收紧。
控制台 → 云存储 → 权限设置 → 自定义安全规则,贴一段类似的:
{
"read": "auth != null && resource.openid == auth.openid",
"write": "auth != null && resource.openid == auth.openid"
}
含义:必须登录,且文件路径里的 openid 段(users/{openid}/...)等于当前登录用户的 openid 才能读 / 写。
切换到「自定义安全规则」之后:
- 私有文件
getTempFileURL返回的 URL 有效期就生效了,链接到期失效 - 直接复制 URL 给别人也访问不到(没带身份)
- 业务侧记得把 cloudPath 一定带上
openid段,别用temp/{timestamp}.jpg这种没用户标识的路径,否则规则匹配不上
详细规则语法见 data-permission 和 secure-database-multi-tenant-rules。
运行验证
- 微信开发者工具编译,登录态就绪后进入上传页
- 调
pickMedia→uploadAll,Console 应输出一组cloud://开头的 fileID - 控制台 → 云存储 → 浏览,在
users/{openid}/目录下能看到刚上传的文件 - 控制台 → 数据库 → orders 集合,对应的订单
images字段是 fileID 数组 - 把订单详情页打开,图片应该能正常渲染。Console 输出
tempFileURL全部是https://开头
常见错误
| 错误码 / 现象 | 原因 | 修复 |
|---|---|---|
STORAGE_REQUEST_FAIL / 上传超时 | 网络弱 / 文件大 | 用 sizeType: ['compressed'] 让 chooseMedia 返回压缩版;视频建议加大超时 |
INVALID_FILE_NAME | cloudPath 以 / 开头,或者出现 //,或者太长 | 检查命名,见第二步规范 |
| 上传成功但控制台看不到 | bucket 选错了 | 一个环境只有一个默认 bucket,确认控制台左上角环境 ID 是当前 SDK 用的那个 |
<image src="cloud://..."> 不显示 | 直接拿 fileID 去渲染了,小程序 image 标签不认 | 先 getTempFileURL 换成 https |
| 临时链接 403 / 过期 | 私有读文件链接有有效期,过了再用会失效 | 进页面重新换;不要把 URL 入库,只入 fileID |
| 别的用户也能下载我的私密文件 | 没切自定义安全规则,默认公有读 | 见第五步,把 read 限制成 resource.openid == auth.openid |
审核期 chooseMedia 报错 | app.json 里没声明 requiredPrivateInfos | 加上 "requiredPrivateInfos": ["chooseMedia"],重新提审 |
错误码定义参考 error-code。
相关文档
- SDK 管理文件 —
uploadFile / downloadFile / getTempFileURL / deleteFile完整 API - 云存储数据权限 — 公有读 / 私有读切换
- 云存储安全规则 — 自定义规则的字段和语法
- add-auth-wechat-miniprogram — 前置:登录接入
- add-database-wechat-miniprogram — 把 fileID 写到数据库
下一步
- 多租户文件隔离规则:secure-database-multi-tenant-rules
- 上传后跑图片审核 / 抠图等处理:控制台「数据万象」,本文不展开
- 给上传成功后发个订阅消息提醒:add-subscribe-message-cloud-function