云开发要解决的问题

传统小程序项目的后端链路是这样的:买服务器、配域名、备案、写接口、维护鉴权、盯数据库。对一个两三个人的小团队,甚至一个独立开发者来说,最耗时的往往不是业务逻辑,而是这些与业务无关的基础设施。小程序云开发(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,否则存储里会留下永远没人引用的孤儿文件。