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 的差距。
小结
一个留言板,从无到有大概两小时:写函数、配触发器、改前端表单。回头再看开头那个吓人的服务器选项,我们用零运维的方式把问题解决了,每月成本停在免费额度内。这正是云函数对静态站最大的价值——你只写业务逻辑,剩下的事平台兜住。下一篇解决一个更头疼的问题:内容多了之后,怎么在静态站里做搜索。
延伸阅读
- Node.js 网站托管实战目录:全书章节导航
- CloudBase Python 指南:前后端一体化的另一种解法
- 腾讯云实践合集:云函数与 COS 的更多组合
