blog

使用 Cloudflare Workers 制作简单的表单接收并存储到 D1 数据库

1 个月前UTC+8

搭建网站通常会需要一个收集用户反馈的表单,在这里公开一个 ZeoSeven 所使用的表单接收并保存的 Worker ,可以复制粘贴即用,当然,弄清楚它的原理会更好些 ...

以 Cloudflare Workers 为计算平台,首先,需要在 Cloudflare 仪表板 dash.cloudflare.com 中创建一个输出 Hello World 的默认 Worker 。创建完成后在你的 Worker 配置页右上角通常会出现一个 “编辑代码” 的蓝色按钮,单击以开始编写,但想要开始编写可能要等待很长时间的浏览器端 VSCode 加载 ...

准备资源

1. 一个域名,因为默认的 *.workers.dev 在中国大陆无法连接

2. 创建好的新 Cloudflare Workers

3. 创建好的 D1 数据库,并绑定到 Worker ,绑定的变量名称使用 DB

4. D1 数据库新建一个 Forms 表,表内新建 1 个 data 列,并使用 text 类型

开始

需要使用 默认输出、异步处理和 fetch API 作为一个 “大框框” 来运行其中的 JavaScript 代码。

就像这样:

export default { 
  async fetch(request, env) { 
      // ... 
  } 
};

在 async fetch 中获取请求的路径并使用 if 条件,以确保只有在特定条件下才能触发 Worker 并在不满足条件时返回 403 错误,当然也可以是其它错误 ... 提高安全性的同时还可以使用 if...else 链在 1 个 Worker 中执行不同的任务。

let path = new URL(request.url).pathname; 
if (request.method === 'POST' && path === '/') { 
  // ... 
} else { 
  return new Response('403 Forbidden' ,{ 
      headers: { 
          'Content-Type': 'text/plain' 
      }, 
      status: 403 
  }); 
}

可根据你的前端表单请求方法 method 来设置 if 块条件中的 request.method 值。

if 块内的核心代码:一个获取 formData 后转换为 Object 并输出到自定义模板,再定义一个 NoneFormData 标识来处理空表单的情况,当然,记录一下表单提交时的北京时间通常被认为是更好的。

try { 
  // 等待 formData 可用后获取 
  const FormData = await request.formData(); 
  // 将表单数据转换为 Object 
  const getFormData = Object.fromEntries(FormData.entries()); 
  // 定义一个空字符串作为最终输出 
  let FormDataOutput = ''; 
  // 定义一个空表单内容的标识 
  let NoneFormData = true; 
  // 循环获取表单的 key 和 value 
  for (const [key, value] of Object.entries(getFormData)) { 
      // 条件判断:键的值需有值才保存,不需要可删除 if 块 
      if (value.trim() !== '') { 
          // 既然有值了,则将空表单内容标识设置为 false 
          NoneFormData = false; 
          // 输出格式化后的内容 
          FormDataOutput += `${key}:${value}`; 
      } 
  }
  // 如果没有表单数据,则忽略,有实际表单,则执行存储 
  if (!NoneFormData) { 
      // 创建一个新的 Date 时间并使用计算北京时间后输出到 FormDataOutput 
      FormDataOutput = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60 * 1000 + 8 * 60 * 60 * 1000).toISOString().replace('Z', '').replace('T', ' ') + FormDataOutput; 
      // 从 env.DB 变量获取 D1 数据库存储到 Forms 表中的 data 列并等待存储完成, SQL 语句中的表名和列名可根据实际 D1 数据库表列名自定义 
      await env.DB.prepare(`INSERT INTO "Forms" (data) VALUES (?)`).bind(FormDataOutput).run(); 
      // 成功提示,可以返回内容或重定向 
      return new Response('200 OK' ,{ 
          headers: { 
              'Content-Type': 'text/plain' 
          }, 
          status: 200 
      }); 
  } 
} catch (err) { 
  // 保存失败提示,通常返回错误信息 
  return new Response(`<pre>${err.toString()}</pre>` ,{ 
      headers: { 
          'Content-Type': 'text/html' 
      }, 
      status: 500 
  }); 
}

如果你不想直接返回内容而是重定向,可以使用

// 永久重定向 
return Response.redirect('https://example.org/success', 301); 

// 临时重定向 
return Response.redirect('https://example.org/success', 302);

这样的整体实现了 1 个 Worker 多个功能,只要满足其 if...else 链的条件,比如说在接收表单的逻辑之后,判断 GET 路径是否为 /robots.txt 则返回 robots 内容:

if ( ... ) { 
  ... 
} else if (request.method === 'GET' && path === '/robots.txt') { 
  return new Response('User-agent: *\nDisallow: /', { 
      headers: { 
          'Content-Type': 'text/plain' 
      }, 
      status: 200 
  }); 
}

最后,在 Worker 的 “设置” 选项页面,添加 1 个 “域和路由” 即可使用域或路径来触发 Worker ,如果是路由,则域名需要在 Cloudflare DNS 中为 已代理状态 ,比较推荐使用自定义域,使 Worker 以独立的方式更佳的工作,其实本篇中的实现对路径 path 进行了判断,如果要按照本篇直接使用,则应使用自定义域并且前端表单的请求方法 method 需要是 POST 。

扩展:前端表单示例,可以和此 Worker 配合使用,其中 https://example.org 应替换为你的 Worker 路由或自定义域。

<form action='https://example.org' method='POST'> 
  <label>姓名:<input type='text' name='name' /></label> 
  <label>邮箱:<input type='email' name='email' /></label> 
  <label>电话:<input type='tel' name='tel' /></label> 
  <label>留言:<textarea name='message'></textarea></label> 
  <button type='submit'>提交</button> 
</form>