4.2 云函数补充:表单与留言后端

创业页做到这里,功能上还差一块。页面上有个留言表单:投资人看完项目介绍,如果想留个联系方式或者写两句反馈,点提交就行。但静态站没有后端,表单提交后数据往哪存?总不能让用户每次都复制邮箱发邮件。这一篇讲我们怎么用云函数给静态站补后端,一个能存留言、能收通知的接口,成本几乎为零。

场景:一个表单,三种死法

需求很简单:用户填昵称和留言,点提交,数据存起来,我们收到通知。但没有后端服务器,这里有三种死法。第一种是表单提交动作指向一个不存在的地址,点完直接 404。第二种是接第三方表单服务,免费额度很快用完,而且数据攥在别人手里。第三种是咬牙买台云服务器,就为一个一周收不了三条留言的表单养一台机器,运维成本全摊进来了——还要给它配安全组、打补丁、做监控,光想想就头疼。

我们是 Node 团队,思路自然落在云函数上:写一个 TypeScript 函数,部署到云函数 SCF,配一个 HTTP 触发器对外暴露接口,前端 fetch 过来就行。按调用次数计费,每月免费额度对一个创业页来说绰绰有余。先放一张选型对照表,我们就是照着这个决策的:

方案 月成本 运维量 数据归属 适用场景
自建 Express 服务器 几十元起 高,要保活打补丁 自己 有复杂后端的项目
第三方表单服务 0 到几十元 低 第三方平台 只想快点上线
云函数 SCF 免费额度内 低,只管代码 自己的 COS 轻量接口、事件处理

选完方案,接下来写代码。

方案:一个函数处理留言

云函数的写法对 Node 开发者来说几乎没有学习成本,就是一个普通函数:SCF 运行时传入事件对象,我们处理完返回结果即可。下面是留言接口的完整实现,存储直接用第三篇已经开好的 COS 桶:

// src/index.ts:留言接口
import COS from "cos-nodejs-sdk-v5";

const cos = new COS({
  SecretId: process.env.TC_SECRET_ID!,
  SecretKey: process.env.TC_SECRET_KEY!,
});

const BUCKET = "guestbook-1300000000";
const REGION = "ap-guangzhou";

interface Message {
  name: string;
  content: string;
  createdAt: string;
}

export const main = async (event: any) => {
  // 解析前端 POST 过来的 JSON
  const body = JSON.parse(event.body || "{}");
  const { name, content } = body;

  if (!name || !content) {
    return { statusCode: 400, body: JSON.stringify({ msg: "参数缺失" }) };
  }

  const msg: Message = {
    name: String(name).slice(0, 32),
    content: String(content).slice(0, 500),
    createdAt: new Date().toISOString(),
  };

  // 追加写入 COS 上的留言文件
  const date = msg.createdAt.slice(0, 10);
  const key = `messages/${date}.json`;
  const data = await cos
    .getObject({ Bucket: BUCKET, Region: REGION, Key: key })
    .promise()
    .catch(() => null);
  const list: Message[] = data ? JSON.parse(data.Body.toString()) : [];
  list.push(msg);

  await cos
    .putObject({
      Bucket: BUCKET,
      Region: REGION,
      Key: key,
      Body: JSON.stringify(list),
    })
    .promise();

  return { statusCode: 200, body: JSON.stringify({ msg: "ok" }) };
};

存储选 COS 是因为它按量计费,这个量级下基本不花钱。也可以接数据库,但对每天几条留言来说,一个 JSON 文件足够,还省去建表的麻烦。代码里做了两件防御性的事:字段长度截断,防止有人塞超长内容;按天分文件,避免单个文件无限膨胀。

部署在 SCF 控制台完成:新建函数、选 Node 运行时、把打包好的代码上传,再配一个 HTTP 触发器,拿到一个 API 地址。把这个地址填到前端表单的提交逻辑里,闭环就通了。前端那侧是标准的 fetch 调用,没有任何特殊要求。

上传代码有两个细节。第一,依赖要一起打包:云函数运行时只提供基础环境,你 import 的每个包都要打进上传的压缩包里,不然线上找不到模块。我们用打包工具把代码和 node_modules 一起压成 zip,再通过控制台上传,也可以绑定为持续集成自动上传。第二,运行时版本要对齐:本地用 Node 18 开发就选 Node 18 的运行环境,高版本写好的语法在低环境上直接报错,这类问题排查起来很费时间。

除了存数据,我们还给函数加了一步通知:留言写入成功后,顺便调用企业微信的机器人接口推一条消息到群里。加一个 webhook 请求就行,十行代码,但团队里任何人都能第一时间看到用户反馈,回复速度从按天变成按小时。对创业页来说,每一条留言都可能是潜在投资人的第一句话,晚回一天可能就聊不起来了。

整个留言功能的入口最终就是网站托管活动页开通的资源串起来的:托管放静态站、COS 存留言、云函数跑接口,新用户首年价格很友好,这套留言后端搭下来几乎没花钱。

踩坑与排查

第一个坑是跨域。静态站在自己的域名下,函数接口在另一个域名,浏览器直接拦截。解决方式是在函数返回里带上跨域响应头,并且同时处理 OPTIONS 预检请求,否则浏览器第一次探测就被挡住。这是最容易漏的一步,页面上的表现就是点击提交毫无反应,控制台一片红。把跨域头写进返回模板,别每个分支重复写。

第二个坑是冷启动。云函数一段时间没人访问会被回收,下次请求要先拉起环境,多几百毫秒延迟。留言提交不是高频操作,影响有限,但可以在函数配置里开预置并发消除冷启动,代价是开始按时间计费。我们的选择是不开,留言晚半秒到达没人介意,省钱更要紧。

第三个坑是密钥管理。一开始图省事把 SecretId 和 SecretKey 硬编码进代码,代码仓一传全组人都看见了,组长查代码时当场被揪出来。正确做法是把密钥写进函数的环境变量,代码里只读环境变量。线上密钥要定期轮换,这个习惯越早养成越好。函数配置与管理的官方说明在云函数文档(cloud.tencent.com/document/product/598)里很详细。

第四个坑是防刷。接口裸奔在公网上,总会遇到脚本乱填。我们加了两道最低限度的防护:单 IP 每分钟限五次提交,靠函数里的简单计数兜底;关键字段做敏感词过滤。更稳的做法是把限流放到 API 网关上,配置方便,统计也清楚。如果你的站将来要做登录注册这类更完整的功能,云开发一体化方案里的鉴权思路可以直接借鉴,腾讯云实践合集里也有安全加固的章节值得照做。

第五个坑是日志。函数出问题时控制台看不到任何输出,因为日志要去 SCF 的日志面板里翻。强烈建议把每次请求的关键步骤打出来:参数校验结果、写入耗时、异常堆栈。线上有个神秘问题,留言偶发提交失败,就是靠日志里的一行超时记录定位到 COS 写入的偶发抖动,加了个重试就解决了。排障效率的差距,往往就是几行 console.log 的差距。

小结

一个留言板,从无到有大概两小时:写函数、配触发器、改前端表单。回头再看开头那个吓人的服务器选项,我们用零运维的方式把问题解决了,每月成本停在免费额度内。这正是云函数对静态站最大的价值——你只写业务逻辑,剩下的事平台兜住。下一篇解决一个更头疼的问题:内容多了之后,怎么在静态站里做搜索。

延伸阅读