cloudflare + github 实现留言板

在 小白服务器踩坑(1)中,我们成功部署网站上线,现在为网站实现留言板功能

实现原理

采用 Github Issues + CloudFlare Worker 实现,在 Worder 添加函数实现留言内容接收,并提交至 Github Issues,Github Issues 来存放留言数据,也方便网站显示留言记录

这样做的好处不需要数据库,也不需要部署后台服务,Github Issues 审核,分类,标签也方便管理留言

创建 Worker

Worker 只是一个函数,前端调用接口实际是执行这个函数,进入 cloudflare,选择计算机和AI/Workders和Pages创建应用程序,选择从 Hello World!开始

可以先部署程序,部署成功后修改程序。在程序详情页面,右上角点击编辑代码,添加提交留言的代码

php 复制代码
const corsHeaders = {
  "Access-Control-Allow-Origin": "*", // 本地 + 生产都能用
  "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

export default {
  async fetch(request, env) {
    // =========================
    // 1️⃣ 处理 CORS 预检请求
    // =========================
    if (request.method === "OPTIONS") {
      return new Response(null, {
        status: 204,
        headers: corsHeaders,
      });
    }

    // =========================
    // 2️⃣ GET:健康检查
    // =========================
    if (request.method === "GET") {
      return new Response(
        JSON.stringify({
          status: "ok",
          message: "Comment API is running",
        }),
        {
          headers: {
            "Content-Type": "application/json",
            ...corsHeaders,
          },
        }
      );
    }

    // =========================
    // 3️⃣ 只允许 POST
    // =========================
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", {
        status: 405,
        headers: corsHeaders,
      });
    }

    // =========================
    // 4️⃣ 正常处理留言
    // =========================
    let data;
    try {
      data = await request.json();
    } catch {
      return new Response("Invalid JSON", {
        status: 400,
        headers: corsHeaders,
      });
    }

    const { content } = data;

    if (!content) {
      return new Response("Content is required", {
        status: 400,
        headers: corsHeaders,
      });
    }

    // -------------------------
    // 创建 GitHub Issue
    // -------------------------
    const issueBody = content;

    const res = await fetch(
      `https://api.github.com/repos/${env.GITHUB_REPO}/issues`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${env.GITHUB_TOKEN}`,
          "Content-Type": "application/json",
          "User-Agent": "CF-Worker",
        },
        body: JSON.stringify({
          title: "用户留言",
          body: issueBody,
          labels: ["pending"],
        }),
      }
    );

    if (!res.ok) {
      const err = await res.text();
      return new Response(err, {
        status: 500,
        headers: corsHeaders,
      });
    }

    // -------------------------
    // 发送通知(Telegram 或邮件)
    // -------------------------
    try {
      // Telegram 机器人通知
      if (env.TELEGRAM_BOT_TOKEN && env.TELEGRAM_CHAT_ID) {
        const telegramMessage = `📝 新留言\n\n${content}`;
        await fetch(
          `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              chat_id: env.TELEGRAM_CHAT_ID,
              text: telegramMessage,
            }),
          }
        );
      }

      // 邮件通知(使用 SendGrid)
      if (env.SENDGRID_API_KEY && env.NOTIFICATION_EMAIL) {
        await fetch("https://api.sendgrid.com/v3/mail/send", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${env.SENDGRID_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            personalizations: [
              {
                to: [{ email: env.NOTIFICATION_EMAIL }],
                subject: "新留言通知",
              },
            ],
            from: { email: env.FROM_EMAIL || env.NOTIFICATION_EMAIL },
            content: [
              {
                type: "text/plain",
                value: content,
              },
            ],
          }),
        });
      }
    } catch (notifyError) {
      // 通知失败不影响主流程,只记录错误
      console.error("通知发送失败:", notifyError);
    }

    return new Response(
      JSON.stringify({ success: true }),
      {
        headers: {
          "Content-Type": "application/json",
          ...corsHeaders,
        },
      }
    );
  },
};

机器人和邮箱通知可选,因为要用我们自己的 Github 账号提交 Issues,还需要在 Worker 里配置变量

GITHUB_REPO是保存 Issues 的项目名,GITHUB_TOKEN需要在 github 生成 token

Github Token

登录 github,打开Settings/Developer settings,选择Personal access tokens/Token(classic)

点击Generate new token/Generate new token(classic),填写 token 备注,选择有效时长,不需要勾选其他权限,创建 token,复制 token 到上一步设置的GITHUB_TOKEN变量

前端提交

前端核心就是调用 cloudflare 创建的 worker api 地址

php 复制代码
  const response = await fetch("https://comment.staryou.workers.dev", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      content: content,
    }),
  });

前端调用 api 成功,worker 获取前端提交 content 内容,将 issues 状态设为 pending(github 自行设定),我们可以修改 issues 状态,方便前端筛选留言

前端调用留言 api,这样就实现了留言过滤和分页显示

perl 复制代码
https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/issues?labels=approved&state=open&per_page=${ISSUES_PER_PAGE}&page=${page}

其他

worker 中仓库名配置错误,前端控制台报错

生成 github token 最好选择 classic,避免权限问题

相关推荐
是你的小橘呀2 小时前
单页应用路由怎么搞?React Router 从原理到实战全解析!
前端·javascript
xuedaobian2 小时前
2025年我是怎么用AI写代码的
前端·程序员·ai编程
风止何安啊2 小时前
Set/Map+Weak三剑客的骚操作:JS 界的 “去重王者” ,“万能钥匙”和“隐形清洁工”
前端·javascript·面试
saberxyL2 小时前
前端登录加密与Token管理实践
前端
3秒一个大2 小时前
React 中 Context 的作用与用法:从主题切换案例说起
前端·react.js
2501_944446002 小时前
Flutter&OpenHarmony文本输入组件开发
前端·javascript·flutter
AI前端老薛2 小时前
你了解react合成事件吗
前端·react.js·前端框架
贺今宵2 小时前
2025.electron-vue3-sqlite3使用
前端·javascript·electron
王同学_1163 小时前
爬虫辅助技术(css选择器、xpath、正则基础语法)
前端·css·爬虫