跳到主要内容

在微信小程序中接入 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-sdk2.27.3
@cloudbase/adapter-wx_mp1.3.1
微信开发者工具1.06.x

另外需要:

  • 已完成 add-auth-wechat-miniprogram,auth.hasLoginState() 返回 true
  • 控制台「云存储」已开通,默认会有一个空 bucket
  • app.jsonrequiredPrivateInfos 加上 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。这里的 appadd-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-permissionsecure-database-multi-tenant-rules

运行验证

  1. 微信开发者工具编译,登录态就绪后进入上传页
  2. pickMediauploadAll,Console 应输出一组 cloud:// 开头的 fileID
  3. 控制台 → 云存储 → 浏览,在 users/{openid}/ 目录下能看到刚上传的文件
  4. 控制台 → 数据库 → orders 集合,对应的订单 images 字段是 fileID 数组
  5. 把订单详情页打开,图片应该能正常渲染。Console 输出 tempFileURL 全部是 https:// 开头

常见错误

错误码 / 现象原因修复
STORAGE_REQUEST_FAIL / 上传超时网络弱 / 文件大sizeType: ['compressed']chooseMedia 返回压缩版;视频建议加大超时
INVALID_FILE_NAMEcloudPath 以 / 开头,或者出现 //,或者太长检查命名,见第二步规范
上传成功但控制台看不到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

相关文档

下一步