Skip to content

如何使用 CloudFlare D1 来搭建评论服务?

Updated: at 20:07编辑

本文内容最后更新于 2023-06-17 20:07。本文仅为纪念之用,内容仅供参考,不保证其时效性和准确性。

在本教程中,我们将学习如何使用 D1 来搭建一个无服务器评论服务。为此,我们将构建一个新的 D1 数据库,并构建一个允许创建和检索评论的 JSON API。这可能是国内第一篇详细介绍 D1 和具体写法的博文了 (bushi)


设置你的项目

在此示例中,我们将使用 Hono,一个 Express.js 风格的框架,用于构建我们的 API。要在此项目中使用 Hono,请使用 npm 安装它:

npm install hono

接下来,在 src/index.ts 中,初始化一个新的 Hono 应用程序:

import { Hono } from "hono";

const app = new Hono();

app.get("/posts/:slug/comments", async (c) => {
  // Do something and return an HTTP response
  // Optionally, do something with `c.req.param("slug")`
});

app.post("/posts/:slug/comments", async (c) => {
  // Do something and return an HTTP response
  // Optionally, do something with `c.req.param("slug")`
});

export default app;

创建数据库

我们现在将创建一个 D1 数据库。在 Wrangler 2 中,支持 d1 子命令,它允许我们直接从命令行创建和查询 D1 数据库。使用以下命令创建一个新数据库:

wrangler d1 create d1-example

通过在我们的文件 Wrangler 的配置文件中创建绑定,在我们的 Worker 代码中引用我们创建的数据库。wrangler.toml 绑定允许我们在代码中使用一个简单的变量名来访问 Cloudflare 资源,例如 D1 数据库、KV 命名空间和 R2 存储桶。在wrangler.toml中,设置绑定 DB 并将其连接到database_namedatabase_id

[[ d1_databases ]]
binding = "DB" # available in your Worker on `env.DB`
database_name = "d1-example"
database_id = "4e1c28a9-90e4-41da-8b4b-6cf36e5abb29"

通过在文件中配置绑定 wrangler.toml,我们可以从命令行和 Workers 函数内部与数据库进行交互。

与 D1 互动

通过使用以下命令发出直接 SQL 命令与 D1 交互 wrangler d1 execute:

wrangler d1 execute d1-example --command "SELECT name FROM sqlite_schema WHERE type ='table'"

我们还可以传递一个 SQL 文件 - 非常适合在单个命令中进行初始数据格式化。创建 schemas/schema.sql,这将为我们的项目创建一个新 comments 表:

DROP TABLE IF EXISTS comments;
CREATE TABLE IF NOT EXISTS comments (
  id integer PRIMARY KEY AUTOINCREMENT,
  author text NOT NULL,
  body text NOT NULL,
  pathname text NOT NULL
);
CREATE INDEX idx_comments_pathname ON comments (pathname);

-- Optionally, use the below query to create data

INSERT INTO COMMENTS (author, body, pathname) VALUES ("甜力怕", "Great post!", "/hello-world.thml");

创建文件后,通过将标志传递给 D1 数据库来执行模式文件—file:

wrangler d1 execute d1-example --file schemas/schema.sql

执行 SQL

在前面的步骤中,我们创建了一个 SQL 数据库并用初始数据填充了它。现在,我们将向我们的 Workers 函数添加一个路由,以从该数据库检索数据。根据 wrangler.toml 前面步骤中的配置,现在可以通过 DB 绑定访问 D1 数据库。在我们的代码中,使用绑定来准备 SQL 语句并执行它们,例如,检索注释:

app.get("/posts/:slug/comments", async (c) => {
  const { slug } = c.req.param();
  const { results } = await c.env.DB.prepare(
    `
    select * from comments where pathname = ?
  `
  )
    .bind(slug)
    .all();
  return c.json(results);
});

上面的代码使用 D1 绑定上的函数来准备和执行 SQL 语句。

在此函数中,我们接受一个 URL 查询参数id并设置一个新的 SQL 语句,我们可以在其中选择所有与我们的查询参数id具有匹配值的评论。然后,我们可以将其作为简单的 JSON 响应返回。

插入数据

通过完成上一步,您已经建立了对数据的只读访问权限。接下来,您将在src/index.ts中定义另一个端点函数,该函数允许通过将数据插入数据库来创建新评论:

app.post("/posts/:slug/comments", async (c) => {
  const { slug } = c.req.param();
  const { author, body } = await c.req.json();

  if (!author) return c.text("Missing author value for new comment");
  if (!body) return c.text("Missing body value for new comment");

  const { success } = await c.env.DB.prepare(
    `
    insert into comments (author, body, pathname) values (?, ?, ?)
  `
  )
    .bind(author, body, slug)
    .run();

  if (success) {
    c.status(201);
    return c.text("Created");
  } else {
    c.status(500);
    return c.text("Something went wrong");
  }
});

部署

在您的应用程序准备好部署后,使用 Wrangler 构建您的项目并将其发布到 Cloudflare 网络。

首先运行 wrangler whoami 以确认您已登录到您的 Cloudflare 帐户。如果您未登录,Wrangler 将提示您登录,创建一个 API 密钥,您可以使用该密钥从本地计算机自动发出经过身份验证的请求。

登录后,确认您的wrangler.toml文件的配置与下图类似。您可以将name字段更改为您选择的项目名称:

name = "d1-example"
main = "src/index.ts"
compatibility_date = "2023-01-15"

[[ d1_databases ]]
binding = "DB" # available in your Worker on env.DB
database_name = "<YOUR_DATABASE_NAME>"
database_id = "<YOUR_DATABASE_UUID>"

现在,运行wrangler publish将您的项目发布到 Cloudflare。成功发布后,通过发出GET请求以检索相关帖子的评论来测试 API。由于您还没有任何帖子,此响应将为空,但无论如何它仍会向 D1 数据库发出请求,您可以使用它来确认应用程序已正确部署:

# Note: Your workers.dev deployment URL may be different
$ curl https://d1-example.helloworld.workers.dev/posts/hello-world/comments
[
  {
    "id": 1,
    "author": "甜力怕",
    "body": "Hello from the comments section!",
    "pathname": "/hello-world.html"
  }
]

使用前端进行测试

此应用程序只是一个 API 后端,最好与用于创建和查看评论的前端 UI 一起使用。要使用预构建的前端 UI 测试此后端,请参阅文末。值得注意的是,loadCommentssubmitComment 函数向该站点的部署版本发出请求,这意味着您可以使用前端并将 URL 替换为本教程中代码库的部署版本,以使用您自己的数据。

请注意,从前端与此 API 交互需要在后端 API 中启用特定的跨源资源共享(或CORS)标头。幸运的是,Hono 有一种快速的方法可以为您的应用程序启用此功能。导入cors模块并将其作为中间件添加到src/index.ts中的 API:

import { Hono } from "hono";
import { cors } from "hono/cors";
const app = new Hono();
app.use("/*", cors());

现在,当您向/*发出请求时,Hono 将自动生成 CORS 标头并将其添加到来自您的 API 的响应中,从而允许前端 UI 与其交互而不会出错。

文末福利 - 前端代码

<template>
	<div class="post">
		<h1 v-text="post.title" />
		<p v-text="post.content" />

		<h3>Comments (<span v-text="post.comments ? post.comments.length : 0" />)</h3>

		<form v-on:submit="submitComment">
			<textarea
				required
				placeholder="写下你的留言"
				v-model="comment.body"
				cols="40"
				rows="4"
			/>
			<input required type="text" placeholder="您的名称" v-model="comment.author" />
			<input type="submit" />
		</form>

		<span v-if="!post.comments && loadingComments">加载评论中...</span>

		<div v-if="post.comments">
			<div v-for="comment in post.comments">
				<p v-text="sanitize(comment.body)"></p>
				<p>
					<em>- {{ sanitize(comment.author) }}</em>
				</p>
			</div>
		</div>
	</div>
</template>

<script type="module">
	const posts = {
		'hello-world': {
			title: 'Hello World!',
			content: 'Testing, one two',
			slug: '/hello-world.html',
		},
	};
	export default {
		data() {
			return {
				comment: {
					author: '',
					body: '',
				},
				post: null,
				loadingComments: false,
			};
		},
		mounted() {
			const param = this.$route.params.post;
			if (posts[param]) {
				this.post = posts[param];
				this.loadComments();
			} else {
				throw new Error("无法找到博文");
			}
		},
		methods: {
			async loadComments() {
				this.loadingComments = true;
				const resp = await fetch(
					`https://d1-example.helloworld.workers.dev/posts/${this.post.slug}/comments`
				);
				const comments = await resp.json();
				this.post.comments = comments;
				this.loadingComments = false;
			},
			async submitComment(evt) {
				evt.preventDefault();
				const newComment = {
					body: this.sanitize(this.comment.body),
					author: this.sanitize(this.comment.author),
				};
				const resp = await fetch(
					`https://d1-example.helloworld.workers.dev/posts/${this.post.slug}/comments`,
					{
						method: 'POST',
						body: JSON.stringify(newComment),
					}
				);
				if (resp.status == 201) this.post.comments.push(newComment);
				this.comment.author = '';
				this.comment.body = '';
			},
			sanitize(str) {
        /**
         * 1. g 全局匹配,找到所有匹配,而不是在第一个匹配后停止
         * 2. i 匹配全部大小写
         * 3. m 多行,将开始和结束字符(^和$)视为在多行上工作,而不只是只匹配整个输入字符串的最开
         * 始和最末尾处。
         * 4. s 与 m 相反,单行匹配
         */
				str = str.replace(/[^a-z0-9 \.,_-]/gim, '');
				return str.trim();
			},
		},
	};
</script>

上一篇
新博客正式上线啦~
下一篇
How to Make an HTTP Request in JavaScript

评论加载中.. 如无法加载请刷新页面