3.1 云函数创建:环境里第一行 Node 代码
数据库和集合在第二篇都备好了,这一节开始写真正的后端逻辑。我们项目里所有接口都不是直连数据库的,而是统一走云函数——这样做的原因、第一次创建云函数的完整过程、以及本地怎么调试,都在这一节里。如果你跟着环境搭建那一节走过一遍,这里应该十分钟就能跑通。
场景:为什么不直接前端调数据库
CloudBase 的 SDK 是允许前端直接读写数据库的,而且权限配好之后完全可用。团队里第一次为这个事拍过桌子:前端说直接调多省事,后端说我还要不要做校验。最后我们还是把所有写操作和关键读操作都收进云函数,理由有这么几条:
- 数据库的权限只能做粗粒度控制,写起来很不灵活
- 列表需要按多字段混合排序、做聚合统计,前端拼接太脆弱
- 业务校验放前端等于没校验,改个参数就能绕过去
- 云函数里能拿到完整的调用上下文,做鉴权最方便
于是项目里就形成了一条约定:只读的公开列表可以前端直连,涉及写入和计数的全部走云函数。这个约定在数据表设计之后确定,一直沿用到最后没改过。
约定归约定,第一次创建云函数的时候我还是被一堆小问题卡了半小时。下面把完整过程记录下来,你照着走可以少踩几个。
创建云函数的完整过程
控制台和 CLI 两种方式都可以,项目里我们都用 CLI,因为可以跟着代码一起进版本库。先看 CLI 的初始化:
# 在项目根目录新建函数,nodejs 模板
tcb fn init article --runtime nodejs
# 生成的目录结构
# article/index.js 函数入口
# article/package.json 依赖声明
入口文件 index.js 是整个函数的起点,第一次跑通的版本是这样:
// article/index.js
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
exports.main = async (event, context) => {
const { action } = event
console.log('收到调用', JSON.stringify(event))
if (action === 'ping') {
return { code: 0, msg: 'pong', time: Date.now() }
}
return { code: -1, msg: '未知 action' }
}
这里有几个新手容易忽略的细节,我们一个个说。
cloud.init 里的 DYNAMIC_CURRENT_ENV 表示用当前部署所在的环境,不要手写环境 ID。我最早手写成了测试环境的 ID,结果函数部署到正式环境后数据全写错库,排查了半个下午。这是云函数里最经典的第一个坑。
入口函数是 async 的,返回值会直接被序列化给调用方。event 是调用参数,context 里有函数运行的环境信息。结构化打印入参这个习惯,在监控与成本那一节会看到它救场的时候。
部署和调用就两行命令:
# 部署函数到云端
tcb fn deploy article
# 调用测试一下
tcb fn invoke article --params '{"action":"ping"}'
如果用控制台方式,入口在云开发控制台的云函数页面,新建函数 → 填函数名 → 选运行环境 nodejs → 在线编辑代码保存即可。两种方式产物一样,区别只是代码进不进版本库。
运行环境与依赖配置
云函数的 Node 版本和本地的差异,是第二个容易踩的地方。常用的运行时配置对照如下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 运行环境 | nodejs 16 及以上 | 旧版本部分依赖已不支持 |
| 执行超时 | 列表类 5 秒,图片类 30 秒 | 默认 3 秒常不够用 |
| 内存 | 列表类 256MB,图片处理 1024MB | 影响计费也影响冷启动 |
| 部署方式 | CLI 部署进版本库 | 控制台适合临时热修 |
| 依赖安装 | package.json 声明自动安装 | 不要提交 node_modules |
package.json 里声明依赖,部署时会自动安装:
{
"name": "article",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
关于版本号有个真实教训:有次我顺手写成 latest,某次部署赶上 SDK 大版本升级,接口行为变了,线上函数直接报错。此后所有依赖一律锁死版本号,绝不含糊。锁版本号这个习惯,后来在部署上线的灰度流程里也帮了大忙。
一个函数还是多个函数
最初我把每个接口都建成独立函数:list、detail、create、like、comment、digest,六七个函数摊在控制台里。部署几次之后发现管理成本很高,每个函数都要单独配超时、单独看日志、单独 Versions。后来改成按业务域聚合,一个 article 函数用 action 分发,结构变成这样:
exports.main = async (event, context) => {
const { action } = event
const handlers = {
list: listArticles,
detail: getDetail,
create: createArticle,
like: toggleLike
}
const fn = handlers[action]
if (!fn) {
return { code: -1, msg: `未知 action ${action}` }
}
return await fn(event, context)
}
两种方式的对比如下,我们最后选了聚合:
| 对比项 | 一函数一接口 | 按域聚合分发 |
|---|---|---|
| 冷启动 | 每个接口都可能冷 | 常用接口互相当预热 |
| 部署管理 | 函数多配置繁琐 | 一处配置全量生效 |
| 日志检索 | 分散在多个函数 | 同一函数内按 action 过滤 |
| 代码组织 | 天然隔离 | 需要目录纪律约束 |
| 超时设置 | 可单独精细调控 | 只能取统一上限 |
聚合的代价是函数体变大、超时只能取最粗的上限,但对我们这个量级的项目完全够用。如果你的应用里某个接口特别重,比如图片处理动辄十几秒,那它就值得独立成一个函数单独配超时,这个混合策略我们在每日摘要那一节也用到了。
本地调试与踩坑
本地调试云函数有两种方式。一种是 CLI 的本地调用:
# 本地运行函数,不部署到云端
tcb fn invoke article --params-file ./test/article.json
另一种是写本地脚本直连,适合调试复杂逻辑:
// dev/local-test.js
const cloud = require('wx-server-sdk')
cloud.init({ env: 'your-env-id', resourceAppid: 'your-appid' })
const article = require('../article/index.js')
article.main({ action: 'list', page: 1 }).then(r => {
console.log('本地返回', JSON.stringify(r, null, 2))
})
本地调试最大的坑是环境不一致。触发器、定时器这些在本地根本不会触发,只能在云端验证。还有一个更隐蔽的:本地跑通不代表云端行,因为云端的文件系统是只读的,本地能写临时文件云端会直接报错。我们的处理是临时目录统一用 /tmp,这个路径在云函数里是唯一可写的。
日志怎么看也是一关。函数报错在前端只显示一串通用错误码,真正的堆栈要去控制台的日志页翻,或者用 CLI:
# 拉取最近 20 条日志
tcb fn log article --limit 20
一开始我总盯着前端报错排查,方向完全错了。后来看腾讯云实践里的同款经历才知道,云函数时代第一反应就应该是去翻日志,前端那点信息基本没用。
小结
这一节从零建了第一个云函数,过了部署、调用、依赖、聚合、调试几道关。核心就三句话:环境用 DYNAMIC_CURRENT_ENV 动态获取,依赖锁死版本号,日志永远是第一排查现场。有了这个能 ping 通的函数,下一节就可以往里填真正的业务逻辑了——先从最常用的列表分页开始,里头的门道比想象中多不少,那些在内容发布与存储里要用的分页规则,都会在下一节定型。
延伸阅读
- Python 云开发实战目录:本书章节索引与云函数后续章节
- Node 云函数实战:另一条 Serverless 路线的函数实践
- 腾讯云实践:云函数常见问题复盘合集
