云开发要解决的问题
传统小程序项目的后端链路是这样的:买服务器、配域名、备案、写接口、维护鉴权、盯数据库。对一个两三个人的小团队,甚至一个独立开发者来说,最耗时的往往不是业务逻辑,而是这些与业务无关的基础设施。小程序云开发(CloudBase)的思路是把这一整层托管掉:你依然写 Node.js 代码、依然操作数据库,但运行环境和存储都由微信侧提供,且调用方天然带着小程序身份,不需要自己实现登录态。
云开发不是“没有后端”,而是把后端拆成了三块可以单独调用的能力:云函数跑逻辑、云数据库存数据、云存储放文件——而且这三块只认小程序身份,鉴权在框架层就完成了。
代价同样存在:你的业务逻辑被绑定在微信生态里,跨端复用会变难;云函数的冷启动与超时限制需要单独设计;费用按调用量、存储量和流量计费,用量上来以后要重新算账。因此它最适合“先快速验证,再考虑架构”的场景。
初始化:一个环境 ID 的约定
使用云开发的前提是在小程序后台开通环境,每个环境有独立的 ID,数据库、云函数、存储都按环境隔离。建议至少开两套:一套开发、一套生产,避免调试时污染真实数据。初始化只在 app.js 里做一次。
// app.js
App({
onLaunch() {
if (!wx.cloud) {
console.error("请使用 2.2.3 及以上基础库以使用云能力");
return;
}
wx.cloud.init({
env: "jjy-demo-1a2b3c", // 环境 ID,在云开发控制台可以看到
traceUser: true // 记录访问来源用户,便于控制台排查
});
}
});
初始化之后,页面里就可以直接调用三个入口:wx.cloud.callFunction、wx.cloud.database、wx.cloud.uploadFile。注意 env 参数也可以不显式传,此时使用默认环境——但项目一旦有多环境,显式传值能省掉大量“为什么本地数据和生产数据不一样”的排查时间。
云函数:把后端逻辑写成一个函数
云函数就是运行在 Node.js 环境里的一个导出函数。它最典型的价值是拿得到调用者的身份:通过 cloud.getWXContext() 可以直接读到 OPENID、APPID、UNIONID,而这一切不需要你实现登录、签发 token 或校验签名。
// cloudfunctions/login/index.js
const cloud = require("wx-server-sdk");
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
exports.main = async (event, context) => {
const { OPENID, APPID, UNIONID } = cloud.getWXContext();
const db = cloud.database();
const users = db.collection("users");
const existed = await users.where({ _openid: OPENID }).count();
if (existed.total === 0) {
await users.add({ data: { nickName: event.nickName || "", createdAt: db.serverDate() } });
}
return { openid: OPENID, appid: APPID, unionid: UNIONID || "" };
};
依赖写在云函数目录自己的 package.json 里,wx-server-sdk 是必需的;上传部署时工具会把 node_modules 一并打包。还有一个需要习惯的约束:云函数的超时时间是可以配置的(默认很短,最长通常为 60 秒),因此它不适合跑长任务。批量处理、定时任务、长轮询都应该走“触发 + 轮询结果”的模式,而不是让云函数一直等待。
云数据库:从小程序端直接读写
云数据库是文档型数据库,数据以集合为单位组织,每条记录是 JSON 文档。最贴心的一点是每条记录会自动带上 _openid,配合权限设置就能实现“用户只能看自己的数据”。
// pages/todo/todo.js
const db = wx.cloud.database();
const todos = db.collection("todos");
// 新增
await todos.add({ data: { title: "写一篇小程序笔记", done: false, due: db.serverDate() } });
// 条件查询 + 排序 + 分页
const res = await todos
.where({ done: false, title: db.RegExp({ regexp: "笔记", options: "i" }) })
.orderBy("due", "desc")
.skip(0).limit(20)
.get();
// 聚合:按完成状态分组统计
const stat = await todos
.aggregate()
.group({ _id: "$done", count: $.sum(1) })
.end();
小程序端的查询能力是有意受限的:单个集合最多返回 20 条、一次 skip 的上限也有约束,真正的批量与复杂统计要放到云函数里做(云函数端的上限宽松得多)。这个“客户端保守、服务端放开”的设计,本质是防止有人在小程序里把整张表拉走。
云存储与上传链路
云存储用来放图片、音频、视频这类二进制文件。上传后会得到一个 fileID,它既是访问凭据也是删除凭据,最适合把它存进数据库记录里,页面渲染时把这个 fileID 交给 image 组件的 src 属性即可显示。
// 一次完整的“选图 → 上传 → 入库”
wx.chooseMedia({
count: 1, mediaType: ["image"], sizeType: ["compressed"],
success: async (r) => {
const filePath = r.tempFiles[0].tempFilePath;
const ext = filePath.split(".").pop();
const up = await wx.cloud.uploadFile({
cloudPath: `notes/${Date.now()}-${Math.floor(Math.random() * 1e6)}.${ext}`,
filePath
});
await db.collection("todos").doc(id).update({ data: { cover: up.fileID } });
}
});
三个实践提醒:命名要随机化,避免用户连续上传同名文件互相覆盖;上传前压缩,云存储的流量是要计费的;删除数据库记录时记得一并删除 fileID,否则存储里会留下永远没人引用的孤儿文件。
权限与安全:谁能读、谁能写
云开发把权限做成了四档开关,选错一档就是一个安全漏洞。判断标准很简单:这份数据是不是“用户自己的”。
| 权限模式 | 谁可读 | 谁可写 | 典型用途 |
|---|---|---|---|
| 仅创建者可读写 | 本人 | 本人 | 个人待办、草稿、私密配置 |
| 所有用户可读,仅创建者可写 | 所有人 | 本人 | 笔记、评论、用户上传内容 |
| 所有用户可读写 | 所有人 | 所有人 | 只适合无敏感性的公共字典、榜单 |
| 仅管理端可读写 | 云函数 | 云函数 | 订单、积分、审核记录等强一致数据 |
- 敏感写操作放进云函数积分扣减、库存扣减、订单创建这类操作,客户端不可信,必须由云函数校验后执行。
- 不要相信 event 里的 openid云函数中一律用 cloud.getWXContext().OPENID,它在服务端注入,客户端伪造不了。
- 定期看调用日志云开发控制台的调用日志能直接看出异常的调用量、失败的入参,是排查刷接口最快的地方。
还有一个容易被误解的点:把权限设成“仅管理端可读写”之后,小程序端读数据也必须绕道云函数。这确实多了一次调用,但换来的是所有读写都经过你的服务端校验,值不值得取决于数据的重要程度。
落地实践:一套无服务器的迭代流程
把云开发用顺,关键在于把“改代码”和“改环境”分开,并形成固定节奏。
- 环境分离:开发环境 env-dev 允许随意清库,生产环境只通过发布流程更新云函数;两个环境用同一份代码,env 参数从配置里读。
- 云函数本地调试:开发者工具支持本地调试云函数,配合断点可以避免“改一次上传一次”的漫长反馈。真机联调时再上传到测试环境。
- 数据结构先定再写:集合名、字段名、_openid 的使用方式一旦上线就很难改,先用一条示例记录把结构固定下来。
- 观测先行:给云函数加统一的日志前缀(如 [login]),配合控制台按关键字过滤,能在出问题的第一时间定位到具体函数。
// 用一份配置驱动多环境,避免把环境 ID 写死在业务代码里
// config/env.js
const ENVS = {
dev: { id: "jjy-demo-dev", version: "trial" },
release: { id: "jjy-demo-prod", version: "release" }
};
const stage = wx.getAccountInfoSync().miniProgram.envVersion === "release" ? "release" : "dev";
module.exports = { env: ENVS[stage] };
// app.js
const { env } = require("./config/env");
wx.cloud.init({ env: env.id, traceUser: true });
当项目从“验证想法”走到“有真实用户”时,需要重新评估的是成本与可迁移性:把云函数里的业务逻辑保持成纯 Node.js 函数、把数据库访问收敛到一层仓储代码,将来要迁到自建服务时,改动量会小得多。