最早接触小程序云开发那会儿,我踩的第一个坑不是写代码,而是根本没搞明白“云开发到底替我省掉了什么”。很多人刚上手会以为它只是把数据库搬到了网上,结果写了一堆前端直连数据库的代码,等用户量一上来,查询慢、权限乱、数据被人改,问题全冒出来了。后来我把这套流程完整地重做了几遍,从环境初始化、集合设计、云函数拆分,到真机联调、上线审核、配额控制,才算把这条链路理顺。这篇内容讲的就是这一整套流程——小程序云开发从零到上线的完整实现路径,包括每个环节为什么这么做、参数怎么定、哪里最容易翻车。适合三类人:完全没碰过云开发、想快速做出一个能跑的小程序的新手;用过一点但数据权限和云函数调得一塌糊涂的进阶者;以及想判断“这项目该不该用云开发”的技术决策者。我尽量不说空话,能抄的配置直接抄,能避的坑提前标出来。
1. 整体方案设计与选型思路拆解
1.1 自建后端和云开发,到底差在哪几个环节
先把这件事说透,不然后面所有设计都是无根之水。传统做法是:买服务器、装环境、配数据库、写接口、搞鉴权、弄域名备案和证书、部署、监控。云开发把这些环节压缩成了四件事——云数据库、云函数、云存储、云调用。它本质上是一种服务端能力托管,你不再关心机器在哪、系统怎么装、进程有没有挂,只关心“数据怎么放、逻辑写在哪、权限给谁”。
我把两者的实际差异列在下面这张表里,这张表我建议你存下来,跟人讨论选型时直接拿出来用:
| 对比维度 | 传统自建后端 | 小程序云开发 |
|---|---|---|
| 服务器运维 | 需要自己选购、装系统、配安全组 | 无需关心,平台托管 |
| 接口开发 | 自己定义 RESTful 或用框架 | 云函数即接口,一个函数一个能力 |
| 鉴权体系 | 要自己做 token、session、刷新机制 | 平台直接下发用户身份,云函数内可取到唯一标识 |
| 域名与证书 | 需要备案、配 HTTPS、加白名单 | 小程序内调用云能力不需要配业务域名 |
| 上线速度 | 一周到一个月不等 | 熟悉的话半天能出可运行版本 |
| 成本模型 | 固定月费,闲时也花钱 | 按量计费,低峰期几乎为零 |
| 扩展天花板 | 高,几乎不受限 | 有并发、存储、时长的明确上限 |
| 迁移成本 | 低,代码在手 | 较高,业务逻辑与平台能力耦合 |
看懂这张表你就会明白,云开发的真正价值不是“便宜”,而是把不确定性极高的基础设施工作,换成了确定性极高的配置工作。对于个人项目、MVP 验证、小型工具类小程序,这个交换极其划算;对于有明确高并发预期、需要复杂事务和外部系统深度集成的项目,就得慎重。
1.2 四大能力边界:什么该放哪,一定要提前划清
我见过太多项目把本该在云函数里做的校验写在了页面里,结果前端一改请求就能绕过。所以动手之前,先给四大能力划清职责边界:
- 云数据库:只负责存数据和提供查询能力。它不是一个“带权限的业务层”,别指望靠它做复杂业务校验。
- 云函数:所有不可信输入的校验、涉及多表一致性的写操作、需要调用外部接口的逻辑、需要密钥的操作,全部放这里。
- 云存储:只放文件本体(图片、音视频、文档),业务元数据(谁传的、属于哪个业务对象、审核状态)一律进数据库。
- 云调用:需要平台侧能力(如内容安全检测、订阅消息下发)时使用,走云函数内的免鉴权调用。
这个划分有一个非常实用的判断标准:凡是“用户能不能做这件事”的判断,一律不能在客户端完成。客户端能做的只有“把用户想做的事表达出来”,能不能做由云函数说了算。这一条如果一开始就吃透,后面能省掉 80% 的安全返工。
1.3 什么样的项目适合上云开发,什么样的别硬上
不是所有项目都适合。我的经验判断是这样的:
适合的典型场景——内容展示加轻互动(比如某个活动的报名与展示页)、内部工具类小程序(打卡、登记、审批流转)、带图片上传的轻社区、数据采集与统计看板、快速验证想法的 Demo。这类项目的特点是:读多写少、并发峰值不极端、业务逻辑不复杂、上线时间紧。
不太适合的场景——需要秒杀级并发、需要与已有后端系统做大量双向同步、需要复杂分布式事务、有强合规审计要求的数据留存、需要长连接推送。这些不是云开发做不到,而是会做得很难受,后期的迁就成本会把前期省下的时间全吃回去。
还有一个常被忽略的点:云开发的计费是按量走的,读操作也算。如果你的页面设计成每次进入都全量拉一遍列表,用户多一点账单就会让你肉疼。这一点在设计阶段就要考虑缓存和分页,后面第 7 节会细说。
2. 开工前的准备与项目初始化实操
2.1 账号、环境与工具链的正确安装姿势
准备工作看着简单,实际上这一步出问题的人最多。完整顺序是这样:
- 注册小程序账号,拿到 AppID。没有 AppID 用测试号也能玩,但云开发的部分能力会受限,建议一开始就用正式 AppID。
- 下载官方的开发者工具,安装后登录。稳定版就够,不必追最新版。
- 新建项目时,模板一定要选“云开发”相关的那个,不要选普通模板再手动加,手动加容易漏掉配置项,尤其是云函数根目录的声明。
- 项目建好后,点工具栏的“云开发”按钮,开通环境。第一次开通会让你选套餐,选基础的那档即可,后面可以改。
这里有个新手最容易卡住的地方:环境 ID 不是 AppID。环境 ID 是开通云开发后系统分配的一串字符,形如prod-1a2b3c4d5e6f7g,它是你后续所有代码里要填的东西。我见过有人在wx.cloud.init里填了 AppID,然后一直报初始化失败,找了两小时。
还有一个实操细节:一个 AppID 默认可以创建多个环境,环境之间数据完全隔离。新手建议就建两个:一个叫dev,一个叫prod。别图省事只建一个,否则测试数据和生产数据混在一起,清理起来非常痛苦。
2.2 目录结构与基础配置的规范搭建
项目初始化的代码只有几行,但这几行放在哪、怎么写,直接决定了后面维护舒不舒服。我的标准做法是在app.js的onLaunch里做一次性初始化:
// app.js App({ globalData: { envId: 'dev-1a2b3c4d5e6f7g' }, onLaunch() { if (!wx.cloud) { console.error('当前基础库版本过低,请升级到支持云开发的最低版本'); return; } wx.cloud.init({ env: this.globalData.envId, traceUser: true }); } });traceUser: true这个参数很多人不写,它的作用是在控制台的用户访问记录里留下可追踪的身份信息,排查线上问题时非常有用,建议都打开。而env参数如果不填,会默认走当前环境的第一个,多环境项目里这是灾难的开始,一定要显式指定。
目录结构上,我强烈建议从一开始就按功能分,而不是按类型分。下面这套结构我用了好几个项目,改动很小:
project/ ├── miniprogram/ │ ├── app.js │ ├── app.json │ ├── pages/ │ │ ├── index/ │ │ └── detail/ │ ├── components/ │ ├── utils/ │ │ ├── cloud.js # 统一封装云函数调用 │ │ └── format.js │ └── images/ ├── cloudfunctions/ │ ├── login/ │ │ ├── index.js │ │ ├── package.json │ │ └── config.json │ ├── postCreate/ │ └── postList/ └── project.config.jsonproject.config.json里必须有一行"cloudfunctionRoot": "cloudfunctions/",没有这行,工具里右键就没有“上传并部署”的选项。这是模板自动生成的,但如果你是从别人的代码迁移过来,经常漏掉。
关于云函数目录,有一个规范我建议强制执行:一个云函数只做一件事。postCreate只管创建,postList只管列表查询,不要搞一个post函数靠type参数分支。理由很实际:云函数是按调用次数和运行时长计费的,一个臃肿的函数每次调用都要加载全部依赖,冷启动更慢,日志也更难查。拆细之后单函数代码量小,冷启动快,日志清晰,排查问题一目了然。
2.3 开发环境与生产环境的隔离策略
多环境这件事,我建议在写第一行业务代码之前就规划好,而不是等上线前才想起来分离。具体做法是三层:
- 代码层:环境 ID 不写死在业务代码里,统一从一个配置模块读取,通过构建或手动切换。
- 云函数层:每个云函数里也要区分环境。云函数内部获取当前环境用
cloud.getWXContext().ENV,但更稳妥的做法是在部署时通过环境变量或者直接在代码里写当前函数所属环境的 ID。 - 数据层:
dev环境的集合结构和prod保持一致,但数据可以是脱敏的样例数据。千万不要把生产数据直接复制到开发环境,一旦开发环境的权限配置错了,等于把真实用户数据暴露了。
一个很实用的小技巧:在开发环境的页面上加一条颜色很明显的顶栏(比如深橙色),写上“当前为开发环境”。真机调试的时候,人很容易搞混自己连着哪个环境,有了这个标识,一眼就能确认。这个成本几乎为零,但救过我至少三次。
3. 云数据库:集合设计与查询优化的实操细节
3.1 集合与字段设计的原则
云数据库是文档型数据库,不是关系型,这一点是设计思路的总纲。新手最容易犯的错是拿关系型的思维去设计,比如为了“规范”把用户名单独放一个集合,靠 ID 关联,结果每次列表页都要多做一次查询,一次列表 20 条就是 20 次额外查询,成本和时间全浪费了。
我的原则是:读的时候要一起出数据的,写的时候就放在一起。比如一个帖子列表,列表页要展示标题、封面、作者昵称、作者头像,那这些字段就冗余存在帖子文档里。作者改了昵称怎么办?在修改昵称的云函数里同步更新该作者的历史帖子。这是典型的用写入成本换读取成本,在“读多写少”的场景下极其划算。
字段设计上,有几个约定我建议固定下来:
- 每个文档都带
_openid(平台自动写入创建者标识)和createTime、updateTime(自己写,用服务端时间)。 - 状态字段用英文常量而不是中文,比如
status: 'published',避免编码和显示混用。 - 数值型字段明确单位,比如
sizeInBytes、priceInFen,不要出现price这种含义不明的字段。 - 列表排序字段要有索引,通常是
createTime降序。
关于服务端时间,这里有个细节值得单独说:排序和展示用的时间,一定要用服务端的db.serverDate(),不要用客户端new Date()。用户手机时间是可以随便改的,客户端时间一旦被改,你的列表顺序就会乱,甚至出现“未来的帖子”排在第一位。
3.2 权限设置与安全规则,最容易翻车的地方
云数据库的权限有四种预设:
| 权限模式 | 含义 | 适用场景 |
|---|---|---|
| 所有用户可读,仅创建者可写 | 任何人可查全部,只有创建者能改自己的 | 公开内容,如文章、商品 |
| 仅创建者可读写 | 每个人只能看到和改自己的 | 私密数据,如个人资料、草稿 |
| 仅管理端可读写 | 客户端完全不可访问,只有云函数能操作 | 敏感数据,如订单、余额 |
| 所有用户可读写 | 任何人可读可写 | 几乎不该用,风险极高 |
我看到的最常见的错误是:为了省事,把用户表、订单表设成“所有用户可读”。这意味着任何一个懂点技术的用户,都能在自己手机上把整个集合的数据拉下来。这不是危言耸听,这是每天都在发生的事。
我的建议非常明确:除了明确要公开的内容集合,其他一律设成“仅管理端可读写”,所有访问都走云函数。这样多一层云函数,看起来麻烦,实际上是把校验逻辑收敛到了一个地方,而且日志、限流、参数校验全都有地方放。
如果确实需要客户端直连查询(比如公开列表页,为了省一次云函数调用),那就在这个集合的权限里用自定义安全规则,把条件写死。安全规则的写法如下:
{ "read": "doc.status == 'published'", "write": "doc._openid == auth.openid" }这段规则的意思是:只能读已发布的文档,只能写自己创建的文档。注意这里有个坑——安全规则里的doc指的是被操作的文档本身,查询时的条件必须能被规则推导出来,否则会返回空结果。比如你查询时没带status: 'published'条件,规则就推导不出,结果直接是空数组。很多人以为是数据没写进去,其实是规则挡掉了。
3.3 分页、聚合与实时监听的实用写法
先说分页。云数据库有一个硬限制:小程序端单次查询默认最多返回 20 条,云函数端默认 100 条。所以列表页必须分页,写法如下:
// 云函数 postList/index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); exports.main = async (event) => { const { page = 0, pageSize = 10 } = event; const size = Math.min(pageSize, 20); const res = await db.collection('posts') .where({ status: 'published' }) .orderBy('createTime', 'desc') .skip(page * size) .limit(size) .field({ title: true, cover: true, authorName: true, createTime: true }) .get(); return { list: res.data, hasMore: res.data.length === size }; };这里有两个细节值得强调。一是field指定返回字段,只取列表需要的字段,能显著减少传输量和内存占用,尤其是文档里有长文本或大数组时。二是hasMore的判断用“返回条数是否等于页大小”,虽然理论上会有边界问题(正好最后一页满),但实际用起来够用,真要精确就额外做一次 count 查询。
再说聚合。遇到“统计每个分类下的帖子数”这类需求,不要在外面拉全量数据自己算,用聚合:
const res = await db.collection('posts') .aggregate() .match({ status: 'published' }) .group({ _id: '$categoryId', count: { $sum: 1 } }) .end();aggregate在小程序端的返回条数限制是 20 条,云函数端是 1000 条(按阶段有差异)。所以聚合统计类操作一定要放云函数里做。
最后说实时监听。db.collection().watch()能监听数据集变化,做聊天、协同编辑时很好用。但它有个很大的成本陷阱:每个监听连接都算一次连接和一个读配额,页面不销毁连接就不会断。所以一定要在onUnload里手动close(),并且不要在一个页面里对多个集合同时开监听。我见过有人在列表页对整张表开 watch,用户一多,配额直接见底。
4. 云函数:把业务逻辑搬到服务端
4.1 云函数的基本结构与调用链路
一个最简的云函数结构是这样:
// 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 } = cloud.getWXContext(); return { openid: OPENID, appid: APPID }; };cloud.DYNAMIC_CURRENT_ENV这个写法很关键,它表示“使用当前云函数所在的环境”,这样同一份代码部署到dev和prod都不需要改。如果写死环境 ID,部署到另一个环境就会连错库,这是线上事故的经典来源。
调用侧这样写:
// utils/cloud.js function call(name, data = {}) { return wx.cloud.callFunction({ name, data }) .then(res => res.result) .catch(err => { console.error(`[cloud:${name}]`, err); throw err; }); } module.exports = { call };统一封装的好处是:日志格式统一、错误处理统一、以后要加统一的 loading 或重试也在一个地方改。不要在每个页面里裸写wx.cloud.callFunction,项目一过十个页面就没法维护了。
关于返回结构,我建议给所有云函数定一个统一约定:
{ code: 0, data: ..., msg: '' }code为 0 表示成功,非 0 表示业务错误。这样前端处理逻辑就统一了,不用每个函数猜返回什么形状。注意区分“业务错误”和“调用失败”——云函数抛异常会让callFunction进 catch,那属于调用失败;业务上的校验不通过应该正常返回,只是code非 0。这个区分很重要,因为抛异常在控制台会记为错误,指标会不好看,也会干扰你的告警判断。
4.2 身份识别与鉴权,别信客户端传来的任何身份
这是整个云开发里安全的核心。用户的唯一标识只能从cloud.getWXContext()里取,绝对不能从event里取。因为event是客户端传上来的,客户端想传什么就传什么。
// 正确写法 const { OPENID } = cloud.getWXContext(); // 错误写法,等于把大门钥匙交给用户 const { openid } = event;我见过太多代码图省事,前端把 openid 传上来,云函数直接拿来用。这意味着任何人只要知道别人的 openid,就能以别人的身份做任何事。这种漏洞在审核阶段不一定被发现,但一旦出问题就是大问题。
在此基础上,还需要做“资源归属校验”。比如删除一条帖子,不能只校验用户登录了,还要校验这条帖子的_openid等于当前用户:
exports.main = async (event) => { const { OPENID } = cloud.getWXContext(); const { postId } = event; const doc = await db.collection('posts').doc(postId).get().catch(() => null); if (!doc || !doc.data) { return { code: 404, msg: '内容不存在' }; } if (doc.data._openid !== OPENID) { return { code: 403, msg: '无权操作' }; } await db.collection('posts').doc(postId).remove(); return { code: 0, data: true }; };注意.doc(id).get()在文档不存在时会抛异常,所以要 catch。这是文档型数据库的一个小脾气,处理不好会让函数直接报错,前端收到的是一个没有意义的错误码。
4.3 定时触发器与云函数间调用
定时任务用config.json配置触发器,写法如下:
{ "triggers": [ { "name": "dailyClean", "type": "timer", "config": "0 0 3 * * * *" } ] }这个 cron 表达式是七位的:秒、分、时、日、月、周、年。上面这条表示每天 3 点整执行。这里有个新手常踩的坑:很多人按标准五位的 cron 写,结果触发器不生效也不报错。一定要写满七位。
cron 表达式的时间基准是 UTC,不是北京时间。想让它在北京时间凌晨 3 点跑,就要写成 19 点(UTC+8 的换算,具体以控制台展示为准)。部署触发器之后,一定要去控制台看一眼“触发器”页面,确认它显示的下次执行时间是不是你要的。我因为这个问题白等过一个晚上——任务没跑,以为是代码问题,其实是时间差了八小时。
云函数之间可以互相调用,用cloud.callFunction:
exports.main = async () => { const res = await cloud.callFunction({ name: 'sendNotice', data: { userId: 'xxx' } }); return res.result; };但要注意两点:一是云函数间调用是同步的,被调函数超时会把调用方也拖超时;二是嵌套调用会让日志链路变长,排查时容易找不到源头。所以我的建议是:最多两层,超过就用消息队列或者数据库表做异步解耦。简单做法是写一张tasks表,定时器扫描待处理任务,一条一条消费,失败了还能重试。
4.4 冷启动与性能优化,实测下来的几条经验
云函数的默认超时时间是 3 秒,最大可以调到 60 秒。这个 3 秒非常容易踩坑,尤其是要调用外部接口的时候。我第一次写调用外部接口的云函数,接口本身要 2 秒,加上冷启动 1.5 秒,直接超时。所以凡是涉及外部调用的函数,超时时间一律调到 10 秒以上。
冷启动是云函数绕不开的话题。实测下来的几条经验:
- 依赖越少越快。
package.json里只装真正需要的包,别把一整个工具库塞进去。 - 初始化逻辑放函数体外。数据库连接、SDK 初始化这些放在
exports.main外面,这样在同一个函数实例内可以复用。 - 减少函数体积。不要把整个
node_modules传上去,上线时选“云端安装依赖”,只传package.json。 - 业务上容忍冷启动。对延迟敏感的入口(比如首页列表),可以在小程序启动时预热一次,也就是
onLaunch里悄悄调一下列表函数,等用户点进去时实例已经热了。
还有一点容易被忽略:云函数并发是有上限的。基础套餐的并发限制不高,如果某个函数会被高频调用,就会出现排队。解决办法不是调大超时,而是减少调用次数——把多次小调用合并成一次批量调用,比如批量查询、批量更新,这是最有效的性能优化手段。
5. 云存储:文件上传下载的完整链路
5.1 上传、下载与文件标识的正确用法
云存储的用法很直接,上传后返回一个fileID,形如cloud://dev-xxx.xxxx/uploads/2024/05/a.jpg。这个fileID就是文件的永久标识,把它存到数据库里,展示时用它来换临时链接。
// 上传 const res = await wx.cloud.uploadFile({ cloudPath: `uploads/${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`, filePath: tempFilePath }); // res.fileID 存进数据库cloudPath的命名有个讲究:不要用用户上传的原始文件名。原始文件名可能重复、可能带特殊字符、可能带中文,一旦有重名就会互相覆盖。我用的规则是“时间戳 + 随机串 + 原扩展名”,保证全局唯一。
展示图片时,<image src="cloud://...">是支持的,平台会自动处理。但如果要在外部使用(比如分享、导出),就需要换成临时链接:
const { fileList } = await wx.cloud.getTempFileURL({ fileList: ['cloud://dev-xxx.xxxx/uploads/a.jpg'] }); // fileList[0].tempFileURL 有有效期临时链接是有有效期的,所以不要把它存进数据库。这是一个常见错误:有人图省事,上传完换一次链接存下来,第二天全部图片都变成裂图。永远存fileID,用的时候现换。
5.2 图片处理、限流与存储成本控制
上传前的压缩是必须做的。现在手机拍的照片动辄五六兆,直接传上去既慢又费存储。用wx.compressImage在客户端先压一遍,质量设 60 到 80,对绝大多数展示场景完全够用,体积能降到原来的十分之一左右。
上传的第二个必做项是大小和类型的校验。客户端校验是为了体验(提前提示),云函数或存储规则校验是为了安全。因为客户端校验可以被绕过,所以真正的把关必须在服务端:
if (!['jpg', 'jpeg', 'png', 'webp'].includes(ext)) { return { code: 400, msg: '仅支持常见图片格式' }; } if (fileSize > 5 * 1024 * 1024) { return { code: 400, msg: '文件不能超过 5MB' }; }存储成本这一块很容易失控。几个实操建议:
- 建立“孤儿文件”清理机制。用户上传了但最终没提交的内容,文件已经存在存储里了,要定期扫描清理。
- 列表页用缩略图,不要直接加载原图。可以在上传时同时生成一份小尺寸版本,列表用小的,详情页用大的。
- 删除业务数据时,记得同步删除关联文件。这一步很容易漏,数据库删了,存储里的文件还在,日积月累就是一笔开销。
6. 联调、真机调试与常见问题排查实录
6.1 高频错误速查表
下面这些是我和自己团队真实遇到过、并且反复有人问的问题,整理成表方便对照:
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 云函数调用报找不到函数 | 函数没上传部署,或名称拼错 | 右键函数目录,上传并部署全部文件 |
| 查询一直返回空数组 | 权限规则挡掉了,或 where 条件不匹配 | 改用管理端权限,先在云函数里试查 |
| 初始化失败 | wx.cloud.init的 env 填错,或没开通环境 | 核对环境 ID,确认控制台环境存在 |
| 云函数超时 | 默认 3 秒不够,外部调用慢 | 调大到 10 到 20 秒,精简依赖 |
| 上传文件报错 | 路径含非法字符,或超大小限制 | 用时间戳随机名,压缩后再传 |
| 真机正常、体验版异常 | 体验版连的是生产环境,数据不同 | 明确版本对应的环境,核对环境 ID |
| 定时任务不执行 | cron 位数不对,或时区理解错 | 用七位表达式,核对控制台展示时间 |
| 图片裂图 | 存了临时链接而不是 fileID | 数据库统一存 fileID,用时再换 |
6.2 模拟器和真机不一致的坑,我踩过的几个
第一个坑是网络环境。模拟器走的是电脑网络,真机走的是手机网络,某些外部接口在真机上会因为没有白名单或者超时而失败。所以任何涉及外部请求的功能,一定要在真机上验一遍,别信模拟器。
第二个坑是基础库版本。模拟器可以用最新基础库,但真实用户的版本分布很杂。云开发的很多能力有最低基础库要求,用户版本低就会直接调用失败。所以wx.cloud的存在性判断一定要写:
if (!wx.cloud) { wx.showModal({ title: '提示', content: '当前版本过低,请升级后再使用' }); return; }第三个坑是权限授权状态。用户可能之前拒绝过某个授权,再次调用不会弹窗而是直接失败。这种情况要引导用户去设置页手动开启,wx.openSetting是唯一的路。光靠提示“授权失败”用户是懵的,必须给出可操作的入口。
第四个坑是开发者工具缓存。有时候代码改了但行为没变,多半是缓存没清。编译时选“清缓存并重新编译”,或者干脆重启工具,能省掉大量自我怀疑的时间。这个听起来很傻,但我至少浪费过好几个小时在这个上面。
6.3 日志排查的正确姿势
云函数的日志在控制台的云开发面板里看,分为调用日志和运行日志。排查问题的顺序建议是:
先看调用是否到达——如果调用日志里根本没有记录,问题在客户端,检查wx.cloud.init和函数名。再看函数的入参是什么——把关键参数打进日志里,确认前端传的和后端收到的一致。最后看异常堆栈——注意时区,日志时间可能是 UTC。
有个实用的习惯:每个云函数入口打一行带请求标识的日志。比如生成一个随机串,在入口和关键节点都带上,这样并发高的时候能按请求串把一次完整调用串起来。没有这个,几十条日志混在一起根本没法看。
还有一点:不要在日志里打印完整用户数据。日志的留存和访问权限比数据库松,打印手机号、地址这类信息是不合适的。要打就打 ID 和状态,需要详情时再按 ID 去查。
7. 上线发布、配额与成本控制的实战经验
7.1 提审前的自检清单
上线前我一定会过一遍这份清单,每次都能查出点东西:
- 环境切到生产,
envId确认是prod那个。 - 所有云函数重新上传部署,别漏了某个改过但没部署的。
- 数据库权限逐集合核对,敏感的确认是“仅管理端可读写”。
- 索引建好,列表排序字段确认有索引。
- 定时触发器确认已在生产环境部署。
- 所有调试用的
console.log清掉或者降级。 - 用户协议、隐私相关说明按平台要求补齐。
- 空数据、网络异常、超时三种情况的前端兜底都验过。
第 8 条特别容易被忽略。很多人只测了“一切正常”的路径,结果审核时网络一抖,页面白屏,直接被驳回。兜底状态一定要做,而且要有明确的空状态文案和重试按钮。
7.2 配额、计费与省钱的具体做法
云开发的计费项主要包括:数据库读写次数、存储容量与流量、云函数调用次数与资源消耗。控制成本的思路不是“少用”,而是“别浪费”。
几条实测有效的做法:
- 列表页强制分页,每页 10 条,不要一次拉完。
- 详情页做本地缓存,短期内重复进入不重复请求。
- 图片做尺寸分级,列表用小图,别用原图。
- 定时任务合并,不要拆成十几个一分钟一次的小任务。
- 统计类数据用聚合在服务端算一次,别把原始数据全拉到前端算。
- 定期清理孤儿文件和过期数据,存储是按容量计费的。
还有一个容易被忽略的点:日志也会产生存储开销。默认保留时间比较长的话,高频调用的函数会攒下大量日志。定期看一眼日志占用,该清理就清理。
我自己的一个教训:早期有个列表页没做分页,每次进入都拉全量,前期用户少完全没感觉,等日活上千之后,数据库读次数涨得飞快,账单出来才发现问题。所以分页这件事,一开始就要做,不要等出问题再补。
7.3 后续还能怎么扩展
跑通这套流程之后,能扩展的方向其实不少。一是把云函数拆得更细,配合任务表做异步化,扛住更高的写入量。二是把常用查询结果缓存到本地存储,配合版本号做失效,进一步减少读次数。三是把内容审核、消息通知这类能力接到云调用上,减少自己维护接口的成本。四是当业务真的涨到云开发扛不住时,可以做平滑迁移——因为你的业务逻辑都在云函数里,只要把数据访问层抽出来,换成外部数据库,上层代码改动很小。这也是我一直强调“业务逻辑必须放云函数”的原因之一:它不只是为了安全,也是为了将来能走。
我在实际项目里体会最深的一点是,云开发真正难的不是写代码,而是把边界划清楚——什么放前端、什么放云函数、什么放数据库规则。这三者的边界一旦清晰,整个项目的复杂度会肉眼可见地降下来。反过来,如果一开始图快,把校验写在页面里、把权限设成“所有人可读”,后面返工的成本会是前期省下的十倍。