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 通的函数,下一节就可以往里填真正的业务逻辑了——先从最常用的列表分页开始,里头的门道比想象中多不少,那些在内容发布与存储里要用的分页规则,都会在下一节定型。

延伸阅读