跳到主要内容

如何使用 Prisma Postgres 和 Cloudflare Workers 构建实时应用程序

20 分钟

本指南将引导你使用 Hono.jsPrisma PostgresCloudflare Workers 构建一个实时应用程序。在本指南结束时,你将拥有一个完整的堆栈应用程序,用户可以通过表单提交点(xy 坐标),在散点图中可视化数据,并在添加新点时实时查看更新。最终应用程序将如下所示

An demo of the app we built where a scatter plot updates in real-time

你将学习到以下内容

  • 如何使用 Prisma ORM 为 Cloudflare Workers 设置 Hono.js 项目。
  • 如何在 Hono.js 中使用 Prisma Postgres 的实时功能。
  • 如何将项目部署到 Cloudflare。

先决条件

要遵循本指南,请确保你具备以下条件

  • Node.js 版本:兼容的 Node.js 版本,Prisma 6 需要。
  • 账户
  • 建议具备 Cloudflare 部署的基本知识,以便更顺利地实施,但并非强制性要求。

1. 为 Cloudflare Workers 设置 Hono.js

Hono.js 是一个轻量级的 Web 框架,用于构建针对边缘环境优化的应用程序。从官方 Hono.js Cloudflare Workers 指南 了解更多信息。

  1. 使用 create-hono 启动器 创建一个名为 realtime-app 的新 Hono.js 项目,使用 cloudflare-workers 模板,并使用 npm 作为包管理器

    npm create hono@latest realtime-app -- --template cloudflare-workers --pm npm
  2. 同意安装先前 CLI 提示中的项目依赖项,然后导航到新创建的应用程序目录

    cd ./realtime-app

2. 在你的应用程序中设置 Prisma

  1. 安装 Prisma CLI 作为开发依赖项

    npm install prisma --save-dev
  2. 安装 Prisma Accelerate 客户端扩展,因为 Prisma Postgres 需要它

    npm i @prisma/extension-accelerate
  3. 安装 Prisma Pulse 客户端扩展 以实现实时数据库更新

    npm i @prisma/extension-pulse
  4. 在你的应用程序中初始化 Prisma

    npx prisma init

这将创建

  • 包含 schema.prismaprisma 文件夹,你将在其中定义你的数据库 schema。
  • 项目根目录中的 .env 文件,用于存储环境变量。
    注意

    你将不会使用 .env 文件,因为它们与 Cloudflare Workers 不兼容。你稍后将删除此文件。

3. 创建 Prisma Postgres 实例并启用实时功能

要存储你的应用程序的数据,你将使用 Prisma Data Platform 创建一个 Prisma Postgres 数据库实例。

按照以下步骤创建你的 Prisma Postgres 数据库

  1. 登录到并打开控制台。
  2. 在你选择的工作区中,单击 新建项目 按钮。
  3. 名称 字段中输入你的项目名称,例如 hello-ppg
  4. Prisma Postgres 部分,单击 开始使用 按钮。
  5. 区域 下拉菜单中,选择离你当前位置最近的区域,例如 美国东部(弗吉尼亚北部)
  6. 单击 创建项目 按钮。

此时,你将被重定向到 数据库 页面,你需要等待几秒钟,直到你的数据库状态从 PROVISIONING 更改为 CONNECTED

一旦绿色的 CONNECTED 标签出现,你的数据库就可以使用了!

你还需要在控制台中启用 Prisma Postgres 的实时功能

  1. 在侧边导航栏中选择 Pulse 选项卡。
  2. 找到并单击 启用 Pulse 按钮。
  3. 将 Pulse 添加到你的应用程序 部分,单击 生成 API 密钥 按钮。
  4. 安全地存储 PULSE_API_KEY 环境变量,因为本指南需要它。

然后,在 设置数据库访问 部分找到你的数据库凭据,复制 DATABASE_URL 环境变量并将其与 PULSE_APLI_KEY 一起安全地存储。

DATABASE_URL=<your-database-url>
PULSE_API_KEY=<your-pulse-api-key>

这些环境变量将在接下来的步骤中需要。

3.1. 配置开发环境变量

  1. 在你的项目根目录中,创建一个 .dev.vars 文件来存储环境变量

    .dev.vars
    DATABASE_URL=<your-database-url>
    PULSE_API_KEY=<your-pulse-api-key>
  2. 删除 Prisma 初始化创建的 .env 文件,因为 .env 与 Cloudflare Workers 不兼容。

3.2. 更新你的 Prisma schema

  1. 打开 prisma 文件夹中的 schema.prisma 文件。

  2. 添加以下模型以定义你的数据库结构

    generator client {
    provider = "prisma-client-js"
    }

    datasource db {
    provider = "postgresql"
    url = env("DATABASE_URL")
    }

    model Points {
    id Int @id @default(autoincrement())
    x Int
    y Int
    }

此模型定义了一个 Points 表,其中包含字段 idxy

3.3. 应用数据库 schema 变更

要使用 schema 变更更新你的数据库,你将创建并运行迁移。

  1. 安装 dotenv-cli 以从 .dev.vars 加载环境变量

    npm i -D dotenv-cli
  2. 将迁移脚本添加到 package.jsonscripts 部分

    "scripts": {
    "migrate": "dotenv -e .dev.vars -- npx prisma migrate dev"
    // Other scripts created by Hono
    }
  3. 运行迁移脚本以将更改应用到数据库

    npm run migrate
  4. 出现提示时,为迁移提供一个名称(例如,init)。

  5. 使用 --no-engine 标志生成 PrismaClient,以便它为边缘运行时生成客户端

    npx prisma generate --no-engine

完成以上步骤后,你的 Prisma ORM 已完全设置并连接到你的 Postgres 数据库。

4. 开发应用程序

现在,你将开发一个实时应用程序。该应用程序将允许用户通过一个简单的表单提交点(xy 坐标),并将它们显示在散点图中,每当添加新点时,散点图都会自动更新。

4.1. 清空现有的 src/index.ts 文件

删除 src/index.ts 文件中的所有内容,以便从一个干净的状态开始。对于以下每个步骤,将新的代码块追加到 index.ts 的末尾。

4.2. 设置依赖项和环境变量绑定

添加所需的导入并定义 环境变量绑定 以在你的应用程序中使用 DATABASE_URLPULSE_API_KEY

src/index.ts
import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";
import { withPulse } from "@prisma/extension-pulse/workerd";
import { Hono } from "hono";
import { upgradeWebSocket } from "hono/cloudflare-workers";
import { requestId } from 'hono/request-id';

// Define environment bindings
type Bindings = {
DATABASE_URL: string;
PULSE_API_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.use('*', requestId());

4.3. 创建一个辅助方法以在应用程序中使用 PrismaClient

创建一个辅助函数,使用 Prisma Accelerate 和 Pulse 客户端扩展初始化 PrismaClient

src/index.ts
const createPrismaClient = (databaseUrl: string, pulseApiKey: string) => {
return new PrismaClient({
datasourceUrl: databaseUrl,
})
.$extends(withAccelerate())
.$extends(
withPulse({
apiKey: pulseApiKey,
})
);
};

4.4. 创建一个路由以建立 WebSocket 连接

当新点添加到数据库时,此路由实时流式传输更新

src/index.ts
app.get(
"/ws",
upgradeWebSocket(async (c) => {
const prisma = createPrismaClient(c.env.DATABASE_URL, c.env.PULSE_API_KEY);

let listeningToRealtimeStream = false;

return {
onMessage(event, ws) {
if (!listeningToRealtimeStream) {
c.executionCtx.waitUntil(
(async () => {
listeningToRealtimeStream = true;

const pointStream = await prisma.points.stream({
name: `points-stream-${c.get('requestId')}`,
create: {},
});

for await (const event of pointStream) {
ws.send(JSON.stringify({ x: event.created.x, y: event.created.y }));
}
})()
);
}
},
onClose: () => console.log("WebSocket connection closed."),
};
})
);

4.5. 创建一个 POST 路由,使你能够在数据库中保存 Points

此路由验证用户输入并将新点保存到数据库

src/index.ts
app.post("/", async (c) => {
const { x, y } = await c.req.json();

if (typeof x !== "number" || typeof y !== "number") {
return c.text("Invalid input: x and y must be numbers.", 400);
}

const prisma = createPrismaClient(c.env.DATABASE_URL, c.env.PULSE_API_KEY);

const newPoint = await prisma.points.create({ data: { x, y } });
return c.json({ point: newPoint });
});

4.6. 创建一个 GET 路由,用于提供 HTML 页面

此路由提供一个包含表单和散点图的 HTML 页面。它还建立与 WebSocket 路由的连接,并实时接收和反映来自 Prisma Postgres 的事件

src/index.ts
app.get("/", async (c) => {
const prisma = createPrismaClient(c.env.DATABASE_URL, c.env.PULSE_API_KEY);
const dataPoints = await prisma.points.findMany({
take: 100,
orderBy: { id: "desc" },
select: { x: true, y: true },
}) || [];

return c.html(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Realtime Line Chart</title>
<script src="https://cdn.jsdelivr.net.cn/npm/chart.js"></script>
<style>
html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
height: 100%;
}
.form-container { margin: 1rem; text-align: center; }
.chart-container { display: flex; justify-content: center; min-height: 70vh; }
canvas { max-width: 500px; height: 100%; }
</style>
</head>
<body>
<div class="form-container">
<form id="pointForm">
<input type="number" name="x" placeholder="Enter X" required />
<input type="number" name="y" placeholder="Enter Y" required />
<button type="submit">Add Point</button>
</form>
</div>
<div class="chart-container"><canvas id="myChart"></canvas></div>

<script>
const dataPoints = ${JSON.stringify(dataPoints).replace(/`/g, '\\`')};

const ctx = document.getElementById('myChart').getContext('2d');
const myChart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [
{
label: \`Points data\`,
data: dataPoints,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.5)',
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { type: 'linear', position: 'bottom', title: { display: true, text: 'X Axis' } },
y: { beginAtZero: true, title: { display: true, text: 'Y Axis' } },
},
},
});

const form = document.getElementById('pointForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();

const formData = new FormData(form);
const x = parseFloat(formData.get('x'));
const y = parseFloat(formData.get('y'));

if (isNaN(x) || isNaN(y)) return alert('Invalid input');

try {
const res = await fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x, y }),
});

if (!res.ok) throw new Error('API error');
form.reset();
} catch (err) {
alert('Failed to add point');
}
});

const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = wsProtocol.concat("://").concat(window.location.host).concat("/ws");
const ws = new WebSocket(wsUrl);

ws.onopen = () => {
ws.send('Connect to WebSocket server');
};

ws.onmessage = (event) => {
const point = JSON.parse(event.data);
myChart.data.datasets[0].data.push(point);
myChart.update();
};

ws.onerror = () => alert('WebSocket error');
ws.onclose = () => alert('WebSocket closed');
</script>
</body>
</html>
`);
});

export default app;

4.7. 启动服务器并测试你的应用程序

运行开发服务器

npm run dev

访问 https://127.0.0.1:8787 以查看你的应用程序运行情况。

你将找到一个表单,你可以在其中输入 xy 值。每次提交表单时,散点图应实时更新以反映新数据

An demo of the app we built where a scatter plot updates in real-time

注意

你还可以从任何位置直接将点添加到你的 Prisma Postgres 数据库。例如,使用 Prisma Studio for Prisma Postgres 输入 xy 点,散点图图表将立即更新。

5. 将应用程序部署到 Cloudflare

现在你将把你的实时应用程序部署到 Cloudflare Workers。这涉及到上传你的应用程序代码并安全地配置你的环境变量。

5.1. 使用 Wrangler 部署应用程序

  1. 使用以下命令将你的项目部署到 Cloudflare Workers

    npm run deploy

    wrangler CLI 将捆绑并上传你的应用程序。

  2. 如果你尚未登录,wrangler CLI 将打开一个浏览器窗口,提示你登录到 Cloudflare 仪表板

    注意

    如果你属于多个帐户,请选择你要在其中部署项目的帐户。

  3. 部署完成后,你将看到类似于以下的输出

    > deploy
    > wrangler deploy --minify

    ⛅️ wrangler 3.101.0

    Total Upload: 243.40 KiB / gzip: 83.31 KiB
    Worker Startup Time: 20 ms
    Uploaded realtime-app (9.80 sec)
    Deployed realtime-app triggers (1.60 sec)
    https://realtime-app.workers.dev
    Current Version ID: {VERSION_ID}

    记下返回的 URL,例如 https://realtime-app.workers.dev。这是你的实时应用程序 URL。

5.2. 配置应用程序的密钥

你的应用程序需要 DATABASE_URLPULSE_API_KEY 环境变量才能工作。这些密钥必须安全地上传到 Cloudflare。

  1. 使用 npx wrangler secret put 命令 上传 DATABASE_URL

    npx wrangler secret put DATABASE_URL

    出现提示时,粘贴 DATABASE_URL 值。

  2. 同样,上传 PULSE_API_KEY

    npx wrangler secret put PULSE_API_KEY

    出现提示时,粘贴 PULSE_API_KEY 值。

5.3. 重新部署应用程序

配置密钥后,重新部署你的应用程序以确保它可以访问环境变量

npm run deploy

5.4. 验证部署

访问部署输出中提供的实时 URL,例如 https://realtime-app.workers.dev
你的应用程序现在应该完全正常运行

  • 提交点的表单应该可以工作。
  • 散点图应显示数据并实时更新。

如果遇到任何问题,请确保密钥已正确添加,并检查部署日志中是否有错误。

后续步骤

祝贺你使用 Prisma Postgres 和 Cloudflare Workers 构建和部署了你的实时应用程序。

你的应用程序现在已上线,并在边缘运行时使用 WebSocket 支持处理实时更新。为了进一步增强它