跳到主要內容

如何使用 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 資料夾,您將在其中定義資料庫結構描述。
  • 專案根目錄中的一個 .env 檔案,用於儲存環境變數。
    注意

    您將不會使用 .env 檔案,因為它們與 Cloudflare Workers 不相容。您稍後將刪除此檔案。

3. 建立 Prisma Postgres 執行個體並啟用即時功能

為了儲存應用程式的資料,您將使用 Prisma Data Platform 建立 Prisma Postgres 資料庫執行個體。

請按照以下步驟建立您的 Prisma Postgres 資料庫

  1. 登入並開啟主控台。
  2. 在您選擇的工作區中,按一下 New project(新增專案)按鈕。
  3. Name(名稱)欄位中輸入專案名稱,例如 hello-ppg
  4. Prisma Postgres 區段中,按一下 Get started(開始使用)按鈕。
  5. Region(區域)下拉式選單中,選取最靠近您目前位置的區域,例如 US East (N. Virginia)(美國東部(維吉尼亞北部))。
  6. 按一下 Create project(建立專案)按鈕。

此時,您將被重新導向至 Database(資料庫)頁面,您需要在該頁面等待幾秒鐘,直到資料庫的狀態從 PROVISIONING 變更為 CONNECTED

一旦出現綠色的 CONNECTED 標籤,您的資料庫即可使用!

您還需要在主控台中啟用 Prisma Postgres 的即時功能

  1. 在側邊導覽列中選取 Pulse 標籤。
  2. 找到並按一下 Enable Pulse(啟用 Pulse)按鈕。
  3. Add Pulse to your application(將 Pulse 新增至您的應用程式)區段中,按一下 Generate API key(產生 API 金鑰)按鈕。
  4. 安全地儲存 PULSE_API_KEY 環境變數,因為本指南需要用到它。

然後,在 Set up database access(設定資料庫存取權限)區段中找到您的資料庫憑證,複製 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 結構描述

  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. 套用資料庫結構描述變更

為了使用結構描述變更更新您的資料庫,您將建立並執行移轉。

  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/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 支援處理即時更新。為了進一步增強它