尝试用 ChatGPT 完整的实现一个 Serverless 后端工程

本文转自 Xiaowen.Z 的博客,原文:https://xiaowenz.com/blog/2023/04/coding-with-chatgpt-pilot/,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang

写在前面

因为从过年开始,断断续续的开始写一个自己用的记账工具,当时自己的想法是尽量小巧,尽量用自己不熟悉的技术栈来玩。那时候还没有横空出世的 ChatGPT,我老老实实的在哄睡张小望的时候看 B 站的视频把 Vue3 撸了一遍,然后花了两三天搭了一个前端出来。

后来发现,Cloudflare 除了已经有了 KV 存储外,去年发布了 Alpha 测试阶段的D1[1] —— 基于 SQLite 的边缘数据库,于是考虑用 Cloudflware Worker 和 D1 来做一个完整的 Serverless 的后端。

可我没写过 Cloudflare Worker,也没用过 SQLite,也不会 Javascript。

万事不决问 AI,于是有了如下对话:

You:

我打算使用 Cloudflare Worker + Cloudflare D1 的 SQLite 数据库创建一个后端服务。数据库中会有几张数据库表,Worker 提供对这些表的增删改查。请帮忙设计一个基本的代码框架结构,方便后续扩展新的表和新的服务。

ChatGPT:

在这个项目中,我们将使用 Cloudflare Workers 和 Cloudflare Durable Objects (D1) 的 SQLite 数据库来创建一个后端服务。为了保持可扩展性,我们将创建一个基本的代码框架结构,以便后续添加新表和新服务。

以下是一个基本的代码框架结构:

  1. 文件结构:

- src/
  - database/
    - index.js
    - migration.js
  - handlers/
    - index.js
    - table1.js
    - table2.js
  - utils/
    - index.js
  - worker.js
  1. 代码实现:

/src/database/index.js

import { open } from 'sqlite';
import { Database } from 'sqlite3';

export async function getDb() {
  return open({
    filename: ':memory:',
    driver: Database,
  });
}

/src/database/migration.js

export async function runMigrations(db) {
  await db.migrate({ force: 'last' });
}

/src/handlers/index.js

import { handleTable1 } from './table1';
import { handleTable2 } from './table2';

export function getHandler(path) {
  switch (path) {
    case '/table1':
      return handleTable1;
    case '/table2':
      return handleTable2;
    default:
      return null;
  }
}

/src/handlers/table1.js

export async function handleTable1(request, db) {
  // 根据request.method实现对应的增删改查功能
  // 例如: GET, POST, PUT, DELETE
}

/src/handlers/table2.js

export async function handleTable2(request, db) {
  // 根据request.method实现对应的增删改查功能
  // 例如: GET, POST, PUT, DELETE
}

/src/utils/index.js

export function parsePath(request) {
  const url = new URL(request.url);
  return url.pathname;
}

/src/worker.js

import { getDb } from './database';
import { runMigrations } from './database/migration';
import { getHandler } from './handlers';
import { parsePath } from './utils';

addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const db = await getDb();
  await runMigrations(db);

  const path = parsePath(request);
  const handler = getHandler(path);

  if (!handler) {
    return new Response('Not found', { status: 404 });
  }

  return handler(request, db);
}

这个框架为你提供了一个基本的结构,可以根据需要扩展。你可以在 handlers 文件夹下为新表创建新的处理器,并在 handlers/index.js 中将它们添加到路由中。同时,你可以在 database/migration.js 中为新表添加迁移脚本。

You:

假设第一张表名为 account,字段如下:

这个地方手误了,直接敲了回车……

ChatGPT:

假设 account 表有以下字段:

  1. id (整型,主键,自增)

  2. username (文本,唯一,不可为空)

  3. email (文本,唯一,不可为空)

  4. password (文本,不可为空)

  5. created_at (文本,不可为空)

  6. updated_at (文本,可为空)

首先,在 /src/database/migrations 文件夹中创建一个名为 01_create_account_table.sql 的迁移文件:

-- /src/database/migrations/01_create_account_table.sql
CREATE TABLE IF NOT EXISTS account (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT NOT NULL UNIQUE,
  email TEXT NOT NULL UNIQUE,
  password TEXT NOT NULL,
  created_at TEXT NOT NULL,
  updated_at TEXT
);

接下来,修改 /src/database/migration.js 文件,添加迁移文件路径:

export async function runMigrations(db) {
  await db.migrate({ force: 'last', migrationsPath: './src/database/migrations' });
}

然后,在 /src/handlers 文件夹中创建一个名为 account.js 的文件,实现增删改查功能:

// /src/handlers/account.js
export async function handleAccount(request, db) {
  const { method } = request;

  switch (method) {
    case 'GET':
      return getAccounts(db);
    case 'POST':
      return createAccount(request, db);
    case 'PUT':
      return updateAccount(request, db);
    case 'DELETE':
      return deleteAccount(request, db);
    default:
      return new Response('Method not allowed', { status: 405 });
  }
}

async function getAccounts(db) {
  // 查询所有账户
}

async function createAccount(request, db) {
  // 创建新账户
}

async function updateAccount(request, db) {
  // 更新账户
}

async function deleteAccount(request, db) {
  // 删除账户
}

最后,在 /src/handlers/index.js 文件中导入并添加新的路由处理程序:

import { handleTable1 } from './table1';
import { handleTable2 } from './table2';
import { handleAccount } from './account';

export function getHandler(path) {
  switch (path) {
    case '/table1':
      return handleTable1;
    case '/table2':
      return handleTable2;
    case '/account':
      return handleAccount;
    default:
      return null;
  }
}

现在你已经为 account 表创建了对应的增删改查处理程序。你可以按照相同的方法为其他表添加处理程序。

You:

请继续实现 account.js 中的 createAccount 和 getAccounts 方法。

ChatGPT:

account.js 文件中,我们将实现 createAccountgetAccounts 方法。为了简化示例,我们不会对密码进行哈希处理,但在实际项目中,你应该使用诸如 bcrypt 之类的库对密码进行哈希处理。

这是实现 createAccountgetAccounts 方法的代码:

// /src/handlers/account.js
import { json, text } from 'co-body';

async function getAccounts(db) {
  const accounts = await db.all('SELECT * FROM account');
  return new Response(JSON.stringify(accounts), {
    headers: { 'Content-Type': 'application/json' },
  });
}

async function createAccount(request, db) {
  const requestBody = await json(request);
  const { username, email, password } = requestBody;

  if (!username || !email || !password) {
    return new Response('Missing required fields', { status: 400 });
  }

  const created_at = new Date().toISOString();

  try {
    await db.run(
      'INSERT INTO account (username, email, password, created_at) VALUES (?, ?, ?, ?)',
      [username, email, password, created_at]
    );
  } catch (error) {
    return new Response('Error creating account', { status: 500 });
  }

  return new Response('Account created', { status: 201 });
}

在这里,getAccounts 函数使用 SELECT * FROM account 查询从数据库中获取所有账户,并将结果作为 JSON 对象返回。

createAccount 函数首先解析请求正文以获取 usernameemailpassword。如果有任何缺失字段,它将返回一个状态为 400 的响应。然后,它将当前日期和时间作为 created_at 字段的值。接下来,我们尝试将新用户插入到数据库中。如果插入成功,则返回状态为 201 的响应。如果插入失败,则返回状态为 500 的响应。

现在你已经实现了 createAccountgetAccounts 方法,可以继续实现 updateAccountdeleteAccount 方法。请注意,在实际项目中,你可能需要实现更严格的验证和错误处理机制。

You:

请问如何在 worker.js 中输出日志?

穿插极其低端的问题……

ChatGPT:

在 Cloudflare Workers 中,可以使用 console.log()console.info()console.warn()console.error() 方法将日志输出到控制台。这些日志可以在 Cloudflare Workers 的 Dashboard 中查看。

worker.js 中,你可以在需要的地方添加日志输出,例如:

// /src/worker.js
import { getDb } from './database';
import { runMigrations } from './database/migration';
import { getHandler } from './handlers';
import { parsePath } from './utils';

addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  console.log('Request received:', request);

  const db = await getDb();
  console.log('Database opened');
  
  await runMigrations(db);
  console.log('Migrations run');

  const path = parsePath(request);
  console.log('Request path:', path);
  
  const handler = getHandler(path);

  if (!handler) {
    console.warn('Handler not found for path:', path);
    return new Response('Not found', { status: 404 });
  }

  console.log('Handling request with handler for path:', path);
  return handler(request, db);
}

这里,我们添加了一些 console.log() 调用来输出接收到的请求、打开的数据库、运行的迁移以及处理请求的路径。如果找不到请求路径的处理程序,我们还使用 console.warn() 输出一个警告。

要查看这些日志,请在 Cloudflare Workers Dashboard 中选择对应的 Worker,并单击 "Logs" 选项卡。在这里,你可以实时查看日志输出。请注意,为了查看日志,你需要将 Worker 部署到开发环境。

You:

调试的时候遇到了这个错误。

No matching export in “src/worker.js” for import “default”

ChatGPT:

这个错误表明在尝试从 src/worker.js 文件中导入一个名为 default 的导出,但在该文件中找不到匹配的导出。Cloudflare Workers 的入口点应该是 addEventListener('fetch', (event) => {}),而不是一个默认导出。

在你之前提供的 worker.js 文件中,没有使用默认导出,因此你不需要从这个文件中导入默认导出。请确保在其他文件中没有尝试从 src/worker.js 导入默认导出。例如,确保你没有在其他文件中编写类似以下代码:

import worker from './worker.js';

如果你确实找到了类似的导入语句,请将其删除,因为在 worker.js 文件中已经有了 addEventListener,你不需要在其他地方导入它。

You:

account.js 中如何引用 util.js 里面的方法?

再问一个低端问题。

ChatGPT:

要在 account.js 中引用 util.js 里的方法,首先需要在 util.js 文件中导出你想要引用的方法,然后在 account.js 中使用 import 语句导入它们。以下是一个示例:

  1. /src/utils/index.js 中添加并导出一个名为 yourFunction 的方法:

// /src/utils/index.js

export function yourFunction() {
  // 你的函数实现
}
  1. /src/handlers/account.js 中导入并使用 yourFunction 方法:

// /src/handlers/account.js
import { yourFunction } from '../utils';

export async function handleAccount(request, db) {
  // 使用 yourFunction
  yourFunction();

  // 其他代码...
}

在这个例子中,我们在 utils/index.js 中定义了一个名为 yourFunction 的函数,并将其导出。然后,在 handlers/account.js 中,我们使用 import 语句导入了 yourFunction,并在 handleAccount 函数中调用了它。你可以按照这种方式导入和使用任何在 utils/index.js 中定义和导出的函数。

You:

请继续实现 account.js 中的 deleteAccount 方法。

ChatGPT:

接下来,我们将实现 account.js 中的 deleteAccount 方法。这个方法将根据提供的 id 删除相应的账户。

// /src/handlers/account.js
import { json, text } from 'co-body';

// ... 其他方法 ...

async function deleteAccount(request, db) {
  const requestBody = await json(request);
  const { id } = requestBody;

  if (!id) {
    return new Response('Missing required field: id', { status: 400 });
  }

  const result = await db.run('DELETE FROM account WHERE id = ?', [id]);

  if (result.changes === 0) {
    return new Response('Account not found', { status: 404 });
  }

  return new Response('Account deleted', { status: 200 });
}

在这里,deleteAccount 函数首先解析请求正文以获取 id。如果 id 缺失,它将返回一个状态为 400 的响应。接下来,我们尝试根据 id 从数据库中删除账户。如果找不到匹配的账户(即没有进行任何更改),则返回状态为 404 的响应。如果删除成功,则返回状态为 200 的响应。

现在你已经实现了 deleteAccount 方法,可以继续实现其他功能。

You:

请确认,Method DELETE 的 HTTP 请求,是否可以包含 requestBody

ChatGPT:

根据 HTTP/1.1 规范,DELETE 方法是可以包含请求正文(request body)的。然而,在实际使用中,大多数情况下,DELETE 方法不会使用请求正文。相反,通常会将所需的参数作为 URL 路径参数或查询参数传递。

虽然 DELETE 方法可以包含请求正文,但许多 Web 服务器、代理和客户端可能不支持它。为了确保更好的兼容性,建议使用查询参数或路径参数来传递所需信息,而不是在 DELETE 请求中包含请求正文。

例如,你可以将之前实现的 deleteAccount 方法修改为使用查询参数传递 id。这是一个调整后的示例:

// /src/handlers/account.js

async function deleteAccount(request, db) {
  const url = new URL(request.url);
  const id = url.searchParams.get('id');

  if (!id) {
    return new Response('Missing required parameter: id', { status: 400 });
  }

  const result = await db.run('DELETE FROM account WHERE id = ?', [id]);

  if (result.changes === 0) {
    return new Response('Account not found', { status: 404 });
  }

  return new Response('Account deleted', { status: 200 });
}

这里,我们使用 URL 对象解析请求 URL,并从查询参数中获取 id。然后继续与之前的实现相同的逻辑。现在,你可以在发送 DELETE 请求时使用类似 /account?id=1 的 URL 来删除具有特定 id 的账户。

You:

最后,请帮我实现 account.js 中的 updateAccount 方法。方法中要求根据 id 字段来更新数据,其他字段如果提供了值,就更新,否则不变。

ChatGPT:

接下来,我们将实现 account.js 中的 updateAccount 方法。这个方法将根据提供的 id 更新账户的其他字段(如 usernameemailpassword),如果提供了新值,则更新,否则保持不变。

// /src/handlers/account.js
import { json, text } from 'co-body';

// ... 其他方法 ...

async function updateAccount(request, db) {
  const requestBody = await json(request);
  const { id, username, email, password } = requestBody;

  if (!id) {
    return new Response('Missing required field: id', { status: 400 });
  }

  const existingAccount = await db.get('SELECT * FROM account WHERE id = ?', [id]);
  if (!existingAccount) {
    return new Response('Account not found', { status: 404 });
  }

  const updatedUsername = username || existingAccount.username;
  const updatedEmail = email || existingAccount.email;
  const updatedPassword = password || existingAccount.password;
  const updatedAt = new Date().toISOString();

  try {
    await db.run(
      'UPDATE account SET username = ?, email = ?, password = ?, updated_at = ? WHERE id = ?',
      [updatedUsername, updatedEmail, updatedPassword, updatedAt, id]
    );
  } catch (error) {
    return new Response('Error updating account', { status: 500 });
  }

  return new Response('Account updated', { status: 200 });
}

在这里,updateAccount 函数首先解析请求正文以获取 id 以及其他可选字段。如果 id 缺失,它将返回一个状态为 400 的响应。接下来,我们尝试从数据库中获取现有账户。如果找不到匹配的账户,则返回状态为 404 的响应。

然后,我们根据请求中提供的新值更新账户的字段,如果没有提供新值,则使用现有值。我们还将当前日期和时间作为 updated_at 字段的值。接下来,我们尝试更新数据库中的账户。如果更新成功,则返回状态为 200 的响应。如果更新失败,则返回状态为 500 的响应。

现在你已经实现了 updateAccount 方法,可以继续实现其他功能。

写在最后

受困于 AI 的知识库版本和限制,上述提供的代码实际上并不能直接使用,缺陷如下:

  • 由于即使是现在也没有足够多的 D1 开发的文档和样例,数据库方面 API 和处理方式需要自己修正

  • Cloudflare Worker 目前已经提供了对 ES6 的集成,关键 API 有了调整,样例中的也不太成。

  • 整个工程结构比较初级,如果希望扩展出更合理的实现方式和细节上的抽象,仍然需要自己动手改很多。

受限于 AI 的算力(对应一次性可以接受的上下文),实际上很难在一次交互中完整的把所有设计思路都放进去。因此慢慢拆,慢慢写会是未来一段时间程序员们对副驾驶协作的常态——一个完整的工程和一块具体的代码实现,中间的抽象信息的数量级至少是百倍的。

人工智能副驾驶目前并没有降低软件工程的难度和门槛,但确实大大降低了软件实现(从 0-0.5)的难度……

以上交互基于 ChatGPT4

引用链接

[1]

D1: https://developers.cloudflare.com/d1

0d4f2f10c157733beb8eb947ca976a39.gif

a6bdee2715aca331592cff3df2b078ca.png

你可能还喜欢

点击下方图片即可阅读

Laf Assistant:云开发从未如此爽快!

2023-04-12

d076f76b8b1d7fcc5df4685ae7d2bafe.jpeg

AI 绘画神还原王维的《鸟鸣涧》

2023-04-11

d9d2a609226121e29d2aae8afdfdb80a.jpeg

ChatGPT 与 Midjourney 强强联手,让先秦阿房宫重现辉煌!

2023-04-06

4834edde4636b1df06b7e6e2aa842c4c.jpeg

1bf2fdea20e5cfdc09ca9b7c6f4a8fac.gif

云原生是一种信仰 🤘

关注公众号

后台回复◉sealos◉获取以 kubernetes 为内核的云操作系统发行版,在云桌面中管理 Kubernetes 集群生命周期,运行分布式应用程序!

a4d6e6241a480dd904f2c6b9e60ec685.gif

f5e5f61bbe5615371a25b4dd21a11f5b.gif

点击 "阅读原文" 获取更好的阅读体验!

发现朋友圈变“安静”了吗?

23e1bdc9d182f082f317d00f92d6196e.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值