做小程序这几年,被问得最多的一类问题是:“后端我一点都不会,能不能自己把数据存起来?”早些年答案很勉强,得租个服务器、配环境、写接口、搞域名备案,一套流程下来,热情基本耗光。后来微信小程序云开发出来,情况确实变了:数据库、存储、云函数三件套开箱即用,前端写完直接能读写数据,不用碰服务器,不用配运维。这篇就把微信小程序云开发从零到上线的完整流程捋一遍,包括环境怎么初始化、数据库集合怎么设计、云函数怎么写怎么部署、权限和配额怎么卡、以及那些文档里不写但一定会踩的坑。适合两类人看:一类是完全没后端经验、想独立把一个小程序做出来的个人开发者;另一类是有后端底子、但想快速验证一个想法、不想在基础设施上耗时间的人。所有步骤都是我自己反复跑过的路径,参数和代码可以直接抄。
1. 先想清楚:云开发到底替你干了哪些活
1.1 三大能力与它们各自的边界
很多人对云开发的理解停在“不用买服务器”,这话对但太浅。它本质上提供的是一套托管在云端的 BaaS(Backend as a Service)能力,你调用的是 SDK,不是 HTTP 接口,所以省掉的不只是服务器,还有接口设计、鉴权、跨域、部署这一整条链路。
具体是三块:
- 云数据库:文档型数据库,数据以 JSON 文档形式存在集合(collection)里,写法和 MongoDB 高度相似,支持 where 条件查询、聚合、事务。它跟 MySQL 那种关系型最大的区别是,字段可以随便加,不用先建表改表,特别适合需求还在变、字段还没定死的早期项目。
- 云存储:对象存储,用来放图片、音频、视频、导出文件。上传一次拿到一个
fileID,这个 ID 可以直接塞进<image>组件的 src 里渲染,底层走 CDN,不用自己拼域名。 - 云函数:跑在云端的 Node.js 函数,一段代码一个函数,按调用次数和执行时长计费。它的价值在于:凡是不能信任客户端的逻辑,都必须放这儿。比如扣库存、算价格、校验会员状态、调用需要密钥的第三方接口。
这三块的关系我一般这么跟人解释:云数据库是仓库,云存储是快递柜,云函数是那个能进仓库、能开门、但你只能在门口递条子让他帮忙办事的店员。客户端只能隔着柜台喊话,店员(云函数)有钥匙。
注意:云数据库的客户端 SDK 是可以直接操作数据的,这跟传统认知里“数据库不该暴露给前端”完全相反。所以权限规则不是可选项,是必答题,后面第 6 节会专门讲。
1.2 什么样的项目适合上云开发,什么样的别碰
我自己接过不少咨询,有人上来就想用它做一个日活几万的电商主站,这种我一定劝退。先给个判断标准。
适合的场景:
- 个人作品、课程作业、内部工具、报名登记、活动打卡、小型社区、内容展示类小程序
- MVP 验证阶段,需求一天三变,需要极快迭代
- 团队里没有专职后端,或者后端排期排到两个月后
- 数据关系简单,没有复杂的多表关联和强事务要求
不太适合的场景:
- 需要复杂 SQL 联表、多表事务、存储过程的老系统迁移
- 对冷启动延迟极度敏感的场景(云函数冷启动可能几百毫秒到一两秒)
- 数据量特别大、查询模式复杂到需要精细调优的场景
- 需要和其他非云开发系统做深度数据库层集成的项目
我个人的经验是:验证期用云开发,跑通模式和留存之后再考虑迁移。很多项目死在“先把架构做扎实”上,其实它根本活不到需要扎实架构的那天。
1.3 费用模型:先把钱的事说清楚
云开发有免费额度,超出后按量计费。我见过有人做出来一上线,第二天被流量冲了一笔账单才反应过来,所以这部分必须提前知道。大致量级是这样的(具体数值以控制台实时公布为准,这里只帮我建立感知):
| 资源项 | 免费额度大致量级 | 超出的主要触发场景 |
|---|---|---|
| 数据库读操作 | 每天数万次 | 首页一次性拉全量列表、没做分页 |
| 数据库写操作 | 每天数万次 | 频繁上报埋点、心跳写库 |
| 存储容量 | 数 GB | 用户上传原图不压缩 |
| 存储下载流量 | 每月数 GB | 图片没走压缩参数、大图直出 |
| 云函数调用次数 | 每月数十万次 | 轮询式调用、每次点击都打一次 |
| 云函数资源使用量(GBs) | 每月数万 GBs | 单函数内存开太大、超时时间设太长 |
关键认知:最容易被刷爆的是数据库读次数和存储流量。一个 200 条数据的列表页,如果每个用户每次进页面都全量拉一遍,一天一千个用户就是二十万次读,直接超标。所以列表必须分页,图片必须压缩,这两个动作在做第一版的时候就该有。
2. 上手前的环境准备与项目初始化
2.1 账号、工具版本与基础库选择
开工前需要准备的东西其实就三样:一个能登录开发者工具的小程序账号(个人主体也能用云开发,功能上够用)、一台装得动开发者工具的电脑、稳定的网络。
工具侧有几个细节容易卡人:
- 开发者工具必须是比较新的稳定版,老版本里云开发面板可能显示不全,或者上传云函数时卡在“安装依赖”。
- 项目创建时,模板可以选“不使用云服务”的空模板,也可以直接选云开发模板。我一般选空模板自己接,因为官方模板里带的示例代码会干扰判断——出问题的时候分不清是自己的还是模板的。
- 基础库版本要留意。云开发的部分 API 对基础库有最低要求,比如云函数本地调试、部分聚合能力。我通常把调试基础库设成当前较新的稳定版本,同时在
app.json里把"cloud": true打开。
app.json里加这一行是必须的,很多人漏掉之后发现wx.cloud是 undefined:
{ "pages": ["pages/index/index"], "window": { "navigationBarTitleText": "示例" }, "cloud": true }另外,工程根目录的project.config.json里需要一个cloudfunctionRoot字段,指明云函数放在哪个文件夹。默认是cloudfunctions/,如果你改了目录名,这里必须跟着改,否则右键菜单里根本不会出现“上传并部署”这个选项,会让人怀疑人生。
{ "miniprogramRoot": "miniprogram/", "cloudfunctionRoot": "cloudfunctions/", "setting": { "urlCheck": false } }心得:
miniprogramRoot和cloudfunctionRoot是两个平行目录,不要把小程序的 pages 直接放根目录又设 cloudfunctionRoot 到子目录,容易在编译时把云函数代码一起打进去,包体积莫名其妙变大。
2.2 初始化云环境与目录规划
第一次在工具里点“云开发”按钮,会让你开通环境。一个账号可以有多个环境,我强烈建议至少开两个:一个叫 dev,一个叫 prod。理由很实在:调试的时候清库、改权限、删集合这些操作,你绝对不想碰生产数据。环境之间数据完全隔离,切换靠一个环境 ID 字符串。
环境 ID 是一串形如xxx-1g2h3i4j5k6l7m的字符,在云开发控制台首页能看到。初始化代码放在app.js的onLaunch里,全局只做一次:
// app.js App({ globalData: { envId: 'your-env-id-here' }, onLaunch() { if (!wx.cloud) { console.error('当前基础库版本过低,不支持云开发'); return; } wx.cloud.init({ env: this.globalData.envId, traceUser: true }); } });几个参数值得解释一下。env传环境 ID 字符串,不传的话会用默认环境,多环境下容易出诡异问题,所以我一律显式写死。traceUser: true的作用是把用户访问记录到云开发控制台的用户管理里,排查问题时能看到是谁在什么时候访问的,成本几乎为零,建议开着。
wx.cloud.init是同步的,不需要 await,但必须在任何wx.cloud.database()、wx.cloud.callFunction()之前执行。理论上onLaunch一定早于页面onLoad,所以放这里最稳。我遇到过有人把它写在页面onLoad里,然后在同一个onLoad里立刻查库,偶尔报环境未初始化,就是这个原因。
目录结构我的习惯是这样:
project/ ├── miniprogram/ │ ├── pages/ │ ├── utils/ │ │ └── db.js # 数据库操作的统一封装 │ └── app.js ├── cloudfunctions/ │ ├── login/ │ │ ├── index.js │ │ ├── package.json │ │ └── config.json │ └── createOrder/ └── project.config.jsonutils/db.js这个封装层是我踩坑之后加的。一开始所有页面直接wx.cloud.database().collection('xx').get(),后来要统一加错误处理、统一加分页、统一加 loading,改起来要动几十个文件。抽一层出来,后面会很省事。
3. 云数据库:从建集合到分页查询的完整落地
3.1 集合设计与权限规则
集合不用提前“建表”,但必须手动创建。这一点坑过不少人:代码里直接db.collection('orders').add(...),如果orders这个集合在控制台里不存在,会报一个类似“collection not exists”的错。所以第一步是打开云开发控制台 → 数据库 → 新建集合,把名字敲进去。
命名上我建议全小写加下划线,跟云函数名区分开(云函数我习惯驼峰,比如createOrder)。命名本身没有强制,但混在一起看日志的时候容易懵。
字段设计有几个实践原则:
- 每条记录自带
_id,系统生成,字符串类型,不要自己覆盖 - 每条记录自带
_openid,在客户端写入时自动填当前用户,这是后面做“我的数据”过滤的基础 - 时间字段统一用
db.serverDate()写入,不要用new Date(),否则用户手机时间不准就会写出未来或过去的数据 - 状态类字段用数字或短字符串,别存中文长句,查询和索引都更省
然后是权限,这是新手最容易忽略、后果最严重的一环。控制台里每个集合可以选四种权限模式:
| 权限模式 | 谁能读 | 谁能写 | 典型用途 |
|---|---|---|---|
| 所有用户可读,仅创建者可写 | 所有人 | 仅数据创建者 | 帖子、评论、公开列表 |
| 仅创建者可读写 | 仅创建者 | 仅创建者 | 个人草稿、私密资料 |
| 所有用户可读,仅管理端可写 | 所有人 | 仅云函数/控制台 | 商品、配置、公告 |
| 仅管理端可读写 | 仅云函数/控制台 | 仅云函数/控制台 | 订单、余额、敏感日志 |
默认是第一种。这意味着如果你建了个users集合存手机号,忘了改权限,那么客户端理论上能读到别人的记录。虽然实际查询还需要构造条件,但这个风险不该留。我的固定做法是:除公开内容表外,一律先设成“仅管理端可读写”,然后需要读的地方通过云函数中转。
提示:权限改完立即生效,但客户端有缓存,改完最好重新编译一次再测。
3.2 增删改查的实操写法
先看最基础的写入。注意add里的data是一个对象,返回值里带_id:
// utils/db.js const db = wx.cloud.database(); export function addRecord(collection, data) { return db.collection(collection).add({ data: { ...data, createTime: db.serverDate(), updateTime: db.serverDate() } }); }这里的db.serverDate()返回的是一个服务端时间占位符,真正落库时才取服务器时间,这样多端写入也不会出现时间错乱。
查询的坑最多。小程序端单次get()最多返回 20 条,云函数端最多 100 条。这个限制是硬的,超了不会报错,只会静默截断。我见过有人列表页只显示了 20 条,以为是数据没写进去,查了半天。所以分页不是优化项,是必需品。
// 分页查询示例:第 page 页,每页 10 条 const PAGE_SIZE = 10; export async function getList(collection, page = 0) { const res = await db.collection(collection) .where({ status: 1 }) .orderBy('createTime', 'desc') .skip(page * PAGE_SIZE) .limit(PAGE_SIZE) .get(); return res.data; }where里的条件写法跟 Mongo 基本一致。常用的操作符要通过db.command拿:
const _ = db.command; // 大于、区间、包含于 db.collection('orders').where({ amount: _.gt(100), status: _.in([1, 2]), createTime: _.gte(new Date('2024-01-01')) });更新分两种,update是局部更新,set是整体覆盖。这里的差别是致命的:用set传一个只含一个字段的对象,会把其他字段全删掉。我吃过这个亏,一个用户资料页用set更新昵称,结果头像和签名全没了。除非你确实要整体重置,一律用update。
// 局部更新:计数自增,同时刷新更新时间 export function increaseCount(collection, id) { return db.collection(collection).doc(id).update({ data: { viewCount: _.inc(1), updateTime: db.serverDate() } }); }_.inc(1)是原子自增,多个用户同时点也不会丢计数,比“先读出来加一再写回去”安全得多。后者在并发下必然出错,只是小流量时你看不出来。
删除单条用doc(id).remove(),批量删除在客户端做不了,必须走云函数,因为客户端remove只作用于单条文档,且受权限约束。
3.3 聚合、索引与深分页的性能坑
当需求从“列出全部”变成“按分类统计数量”或者“取每个用户最新一条”时,普通查询就不够了,需要聚合。云数据库的聚合写法接近 Mongo 的 pipeline:
// 统计各分类下的文章数量 const res = await db.collection('articles').aggregate() .match({ status: 1 }) .group({ _id: '$category', count: db.command.aggregate.sum(1) }) .sort({ count: -1 }) .end();注意聚合的match里不能用普通查询的db.command,要用db.command.aggregate,这两个是两套东西,混用会报错。这一点文档写得比较简略,我第一次写的时候卡了挺久。
索引这块,控制台里可以对字段建索引。什么时候该建?我给的判断是:凡是你在where里长期作为主条件的字段,以及orderBy用到的字段,都建上。比如订单表按_openid + status查,就建一个联合索引。数据量在几百条以内感觉不出差别,上千条之后不加索引的查询会明显变慢,甚至超时。
深分页是另一个隐形炸弹。skip(1000).limit(10)这种写法,数据库实际上要扫描并丢弃前 1000 条,页码越大越慢。翻到几十页之后直接卡住是常态。正确做法是游标分页,用上一页最后一条记录的排序字段作为下一页的起点:
// 游标分页:传上一页最后一条的 createTime export async function getListByCursor(collection, lastTime) { const _ = db.command; const query = lastTime ? db.collection(collection).where({ createTime: _.lt(lastTime) }) : db.collection(collection); const res = await query.orderBy('createTime', 'desc').limit(20).get(); return { list: res.data, nextCursor: res.data.length ? res.data[res.data.length - 1].createTime : null }; }这种写法不管翻到第几页,查询代价都一样。代价是不能跳页,只能“加载更多”。对移动端列表来说,这本来也是更自然的交互,所以我不觉得是损失。
4. 云函数:把敏感逻辑挪到服务端
4.1 云函数的结构、依赖与部署流程
一个云函数就是一个文件夹,里面至少三个文件:
cloudfunctions/login/ ├── index.js # 入口,导出 main ├── package.json # 依赖声明 └── config.json # 权限、触发器配置(可选)index.js的最小骨架:
// 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(); return { openid: OPENID, appid: APPID }; };cloud.DYNAMIC_CURRENT_ENV这个写法很关键。它表示“用当前云函数所在的环境”,这样同一份代码部署到 dev 和 prod 都不用改。如果写死环境 ID,切环境的时候必然出一次事故。
cloud.getWXContext()是云函数里最有价值的一个 API。它拿到的 OPENID 是由平台注入的、不可伪造的,请求方无论怎么改参数都伪造不了。所有“判断这个操作是不是本人发起的”逻辑,都必须基于它,而不是基于前端传上来的 openid。前端传的 openid 只能当普通字符串看待。
package.json里声明依赖,云函数默认已经能用wx-server-sdk,如果用别的包(比如生成 PDF、做加密),就在这里加,然后右键“上传并部署:云端安装依赖”,云端会自己 npm install。这个选项比“上传所有文件”省事,不用本地装 node_modules,包体积也小。
部署完怎么验证?控制台 → 云函数 → 选中函数 → 云端测试,手动传一段 JSON 当event跑一次,看返回值和日志。这一步别省,我见过太多人本地写完直接调,报错之后完全不知道是逻辑问题还是部署没成功。
4.2 调用姿势与返回值约定
客户端调用:
const res = await wx.cloud.callFunction({ name: 'createOrder', data: { productId: 'p_1001', count: 2 } }); console.log(res.result);返回值在res.result里,不是res.data,这个细节每次新人都会问一遍。云函数的返回值必须是可 JSON 序列化的,返回undefined、函数、循环引用都会出问题。
超时时间默认 3 秒,最大可以调到 60 秒(视具体配置)。默认 3 秒是个很容易踩的坑:云函数里如果调了第三方接口、或者做了一批数据库写入,3 秒很容易不够,报超时但日志里看不出明显异常。我的做法是,凡是涉及外部请求或者批量写,一律把超时调到 10 到 20 秒,宁可失败慢一点,也别莫名超时。
内存默认 256MB,一般够用。只有做图片处理、大文件解析时才需要往上调,因为内存直接和计费挂钩。
返回值我建议统一成一种结构,前端好处理:
// 统一返回格式 return { code: 0, msg: 'ok', data: {...} }; // 出错时 return { code: 4001, msg: '余额不足', data: null };不要用throw来传递业务错误。云函数抛出异常时,客户端拿到的是一个包装过的错误对象,错误信息可能被截断,而且日志里会标红,跟真正的系统故障混在一起,排查时很难分辨。业务错误走返回值,系统异常才 throw,这是我定下的规矩。
4.3 定时触发器与云调用
定时触发器在config.json里配:
{ "triggers": [ { "name": "dailyClean", "type": "timer", "config": "0 0 3 * * * *" } ] }这个 cron 是七段式:秒 分 时 日 月 周 年。上面这条表示每天凌晨 3 点整执行。时区是北京时间,不用自己换算,这点比较省心。但要注意,定时触发器需要重新上传部署才会生效,改完config.json不重新部署等于没改。
定时任务的典型用途:清理过期数据、每天汇总统计、给临期订单发提醒。写的时候要特别注意幂等——如果这次执行超时了,下次可能重复处理同一批数据。我的做法是给每条待处理记录加一个processed标记,处理前先原子更新这个标记,更新成功才继续,避免重复。
云调用是另一块能力,指在云函数里直接调平台的开放接口,比如发送订阅消息、生成小程序码。它不需要你手动维护 access_token,省掉了最烦的那部分。配置方式:
{ "permissions": { "openapi": ["subscribeMessage.send", "wxacode.getUnlimited"] } }const result = await cloud.openapi.subscribeMessage.send({ touser: OPENID, templateId: 'your-template-id', page: 'pages/order/detail?id=123', data: { thing1: { value: '订单提醒' }, time2: { value: '2024-06-01 10:00' } } });permissions.openapi里没声明的接口调用会直接报无权限,这个报错信息还算清楚,但很多人不知道要去改 config.json,会一直怀疑是参数问题。
5. 云存储与文件上传全流程
5.1 上传、展示与临时链接的取舍
上传的入口 API 是wx.cloud.uploadFile,需要两个参数:本地文件路径和云端路径。
async function uploadImage(tempFilePath) { const ext = tempFilePath.split('.').pop(); const cloudPath = `avatars/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; const res = await wx.cloud.uploadFile({ cloudPath, filePath: tempFilePath }); return res.fileID; }cloudPath是云端存储路径,必须以文件名结尾。它的设计有几个讲究:
- 不要用用户上传的原始文件名,中文、空格、特殊字符都可能出问题,而且会泄露信息
- 用时间戳加随机串保证唯一,避免同名覆盖
- 按业务分目录(
avatars/、orders/、exports/),后面做清理和统计方便
返回的fileID形如cloud://env-id.xxxx/avatars/1234-abcd.jpg。这个 ID 可以直接用在<image src>里,框架会自动换成可访问的地址。但它不能直接用在wx.request或者<video>的部分场景里,那些地方需要一个真正的 HTTPS 链接,得换:
const { fileList } = await wx.cloud.getTempFileURL({ fileList: ['cloud://env-id.xxxx/avatars/1234-abcd.jpg'] }); console.log(fileList[0].tempFileURL);临时链接有有效期(大概两小时),所以不要把它存进数据库。正确做法是数据库里只存fileID,展示的时候临时换。我见过有人把 tempFileURL 存下来,第二天全站图片裂开。
5.2 图片压缩与安全校验
存储流量是最容易烧钱的地方,而图片是流量的绝对大头。三个动作能省下大部分开销。
第一,上传前压缩。用wx.compressImage或者wx.chooseMedia的sizeType: ['compressed']。一张手机拍的图动辄 3 到 5 MB,压到 200 KB 质量在手机上肉眼看不出差别。这一下就是十几倍的差距。
第二,展示时带处理参数。fileID后面可以拼图片处理参数,直接在 CDN 层裁剪压缩:
// 原图 + 处理参数,输出 200x200 的缩略图 const thumb = `${fileID}?imageView2/1/w/200/h/200`;列表页用缩略图,详情页才加载原图,这个做法我每个项目都上,效果立竿见影。
第三,上传做校验。客户端选完文件先校验大小和类型,超过 2 MB 直接拦掉,后缀不在白名单里也拦。这不是为了安全(客户端校验都能绕过),主要是省流量、省存储,顺便提升体验。真正的安全校验放云函数里做:检查文件头、检查实际大小、限制单用户上传频率。
注意:云存储的权限也可以在控制台里设。默认规则下文件是公开可读的,如果你的存储里有身份证照片、合同扫描件这类内容,必须改成仅管理端可读,读的时候通过云函数换临时链接。
6. 权限、安全与配额:上线前必须过的关
6.1 身份鉴权与数据归属
这套体系里唯一可信的身份来源,就是云函数里cloud.getWXContext().OPENID。所有涉及“这条数据是不是我的”的判断,都要落在服务端。
举个典型错误写法的例子。假设做一个“修改昵称”的功能,客户端这样调:
// 错误示范:把 openid 当参数传上去 wx.cloud.callFunction({ name: 'updateProfile', data: { openid: 'xxx', nickname: '新昵称' } });云函数如果直接用了这个openid去更新,那么任何人构造一下请求就能改别人的资料。正确写法是云函数里忽略传入的 openid,自己取:
exports.main = async (event) => { const { OPENID } = cloud.getWXContext(); return db.collection('users') .where({ _openid: OPENID }) .update({ data: { nickname: event.nickname } }); };同理,客户端做数据库查询时,虽然系统会自动给新记录填_openid,但查询时不加_openid条件是读不到别人数据的(在“仅创建者可读写”模式下)。如果你的集合设成了“所有用户可读”,那就真的能读到全部,这时候必须自己在where里加_openid: '{openid}',注意这个写法是云数据库里的特殊语法:
db.collection('drafts').where({ _openid: '{openid}' // 由系统替换成当前用户 }).get();这个'{openid}'占位符只在客户端 SDK 里有效,云函数里用不了,云函数得用实际取到的 OPENID 值。
6.2 配额监控与成本控制
上线前我会做一次“配额体检”,把几个高风险点过一遍:
| 风险点 | 表现 | 处理办法 |
|---|---|---|
| 列表未分页 | 数据库读次数飙升 | 强制 limit,加加载更多 |
| 图片未压缩 | 存储容量和流量双高 | 压缩 + CDN 参数 |
| 轮询请求 | 云函数调用次数暴涨 | 改成长连接或降低频率 |
| 云函数内存过大 | GBs 消耗翻倍 | 按实际需要调,一般 256MB 够 |
| 云函数超时设太长 | 失败请求也计费 | 按实际耗时设,留 2 倍余量 |
| 日志无限增长 | 存储悄悄变大 | 定期清理,只记关键信息 |
控制台里有资源使用统计,能看每天的趋势。养成习惯:上线第一周每天都看一眼曲线,找到峰值时段和异常调用来源。有一次我发现某天凌晨的云函数调用量是白天的三倍,查下来是定时任务里写了个死循环重试,改掉之后费用立刻回落。
还有一个容易被忽略的是环境隔离带来的费用归属。dev 环境如果也开着各种定时任务,它会一直跑、一直计费。我的做法是 dev 环境不配定时触发器,需要测的时候手动触发一次。
7. 常见问题与排查技巧实录
7.1 高频报错速查表
下面这些是我在自己和别人的项目里反复见到的,按出现频率排:
| 报错/现象 | 大概率原因 | 解决方向 |
|---|---|---|
wx.cloud is undefined | app.json缺"cloud": true,或基础库太低 | 补配置,升级调试基础库 |
| 环境未初始化 | wx.cloud.init没执行或晚于调用 | 挪到onLaunch最前面 |
| collection not exists | 集合没在控制台创建 | 手动新建同名集合 |
| 查询只返回 20 条 | 客户端单次上限就是 20 | 加分页,或用云函数取(上限 100) |
| database permission denied | 集合权限模式挡住了 | 调整权限或用云函数中转 |
| 云函数调用超时 | 默认 3 秒不够 | 调高超时时间,检查外部请求 |
| 云函数报 module not found | 依赖没装 | 用“云端安装依赖”方式重新部署 |
| 云函数改了没生效 | 忘了重新部署 | 右键上传并部署 |
| 定时任务不触发 | 改了 config.json 没重新部署 | 重新部署,检查 cron 段数 |
| 图片裂开 | 用了过期的临时链接 | 数据库只存 fileID,用时现换 |
_openid为空 | 数据是云函数写入的 | 云函数里手动写入 OPENID 字段 |
| 上传大文件失败 | 超出大小限制 | 压缩或分片 |
最后一条值得展开说一下。_openid只在客户端 SDK 写入时自动填充。如果一条记录是云函数写入的(比如订单、日志),_openid是空的,字段名叫_openid但值为 undefined。这时候如果前端按_openid查自己的订单,一条都查不到。正确做法是云函数写入时手动加一个openid字段,值取getWXContext().OPENID。命名上可以故意跟系统字段错开,避免混淆。
7.2 调试手段与发布前自检清单
云函数调试有个提效很大的功能:本地调试。在工具里右键云函数选“开启本地调试”,之后调用会走本地进程,能直接打断点、看变量、改代码即时生效,不用每次都上传。前提是本地装了依赖(在云函数目录执行npm install)。这个开关我用得非常多,尤其是排查“返回值跟预期不一样”这类问题时,日志看不出来的东西,断点一打就清楚了。
另一个手段是日志。云函数里的console.log会进控制台的日志面板,但要注意:日志里别打印敏感信息,包括完整手机号、身份证号、openid 全文。openid 可以用来定位用户,但它本身也算标识信息,我一般只打前 6 位加后 4 位。
发布前我的自检清单是这几条:
- 所有集合的权限模式都确认过,没有该收紧却留着默认的
- 所有列表接口都有 limit,没有裸
get() - 所有图片上传都有大小和类型校验
- 云函数里没有硬编码的环境 ID,用的都是动态环境
- 敏感写操作全部走云函数,客户端不能直接改
- 定时任务的幂等逻辑验证过一遍
- dev 和 prod 的环境 ID 在代码里是配置项,不是写死的字符串
- 控制台的资源统计看过一眼,没有异常峰值
这套清单我每次都过,虽然看起来啰嗦,但每一条背后都是一次真实的翻车。
有一点体会想单独说:云开发最舒服的地方不是省了服务器钱,而是它把“从想法到能用”的距离压到了一天以内。我做过一个内部登记工具,从建项目到同事能扫码使用,前后四个小时,中间没有任何一步需要等别人配合。这种节奏对个人开发者来说价值很大,因为它让你愿意去试那些“可能没什么用”的想法,而大部分好用的东西,最初都是从这种随手一试里长出来的。
如果后面要继续往下走,我建议先把几个方向补上:一是把数据库操作全部收拢到一个封装层,别散在页面里;二是给云函数加一个统一的错误码表,前后端对齐;三是把环境的切换做成配置,别靠改代码。这三件事做完,项目从“能跑”到“敢维护”之间的那道坎,基本就过去了。