← Cloudflare Durable Objects / durable-objects / tutorials
Создание приложения для бронирования мест с SQLite в Durable Objects
В этом руководстве вы научитесь создавать приложение для бронирования мест с использованием Durable Objects. Оно позволит пользователям бронировать место на рейс. Приложение будет написано на TypeScript и будет использовать новый бэкенд хранилища SQLite в Durable Object чтобы хранить данные.
С помощью Durable Objects можно писать переиспользуемый код, который берет на себя координацию и управление состоянием для нескольких клиентов. При этом запись данных в SQLite в Durable Objects выполняется синхронно и использует локальные диски, поэтому все запросы выполняются с высокой производительностью. Подробнее о хранилище SQLite в Durable Objects можно узнать в запись в блоге о SQLite в Durable Objects ↗.
Приложение будет работать следующим образом:
- Пользователь переходит в приложение, передав номер рейса в качестве параметра запроса.
- Приложение создаст новый Durable Object для номера рейса, если он ещё не существует.
- Если Durable Object уже существует, приложение получит информацию о местах из базы данных SQLite.
- Если Durable Object не существует, приложение создаст новый Durable Object и инициализирует базу данных SQLite информацией о местах. В рамках этого руководства информация о местах задана в приложении напрямую.
- Когда пользователь выбирает место, приложение запрашивает его имя. Затем приложение резервирует место и сохраняет имя в базе данных SQLite.
- Приложение также рассылает всем клиентам любые изменения мест.
Приступим!
Предварительные требования
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
Node.js↗.
менеджер версий Node.js
Используйте менеджер версий Node, например Volta ↗ или
nvm ↗ чтобы избежать проблем с правами доступа и менять
версии Node.js. Wrangler, рассмотренная
далее в этом руководстве, требует версии Node 16.17.0 или более поздней версии.
1. Создать новый проект
Создайте новый проект Worker, чтобы создать и развернуть приложение.
-
Создайте Worker с именем
seat-bookingвыполнив:npm create cloudflare@latest -- seat-bookingДля настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker + Durable Objects. - Для Какой язык вы хотите использовать?, выберите
TypeScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
- Для С чего вы хотите начать?, выберите
-
Перейдите в каталог нового проекта, чтобы начать разработку:
cd seat-booking2. Создайте фронтенд
Фронтенд приложения представляет собой простую HTML-страницу, на которой пользователи могут выбрать место и ввести своё имя. Приложение использует Workers Static Assets для обслуживания фронтенда.
-
Создайте новый каталог с именем
publicв корне проекта. -
Создайте новый файл с именем
index.htmlвpublicкаталог. -
Добавьте следующий HTML-код в
index.htmlфайле:
public/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flight Seat Booking</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.booking-container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.seat-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
margin-top: 20px;
}
.aisle {
grid-column: 4;
}
.seat {
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #ccc;
cursor: pointer;
}
.seat.available {
background-color: #5dbf61ba;
color: white;
}
.seat.unavailable {
background-color: #f4433673;
color: white;
cursor: not-allowed;
}
.airplane {
display: flex;
flex-direction: column;
align-items: center;
background-color: #f0f0f0;
padding: 20px;
border-radius: 20px;
}
</style>
</head>
<body>
<div class="booking-container">
<h2 id="title"></h2>
<div class="airplane">
<div id="seatGrid" class="seat-grid"></div>
</div>
</div>
<script>
const seatGrid = document.getElementById("seatGrid");
const title = document.getElementById("title");
const flightId = window.location.search.split("=")[1];
const hostname = window.location.hostname;
if (flightId === undefined) {
title.textContent = "No Flight ID provided";
seatGrid.innerHTML = "<p>Add `flightId` to the query string</p>";
} else {
handleBooking();
}
function handleBooking() {
let ws;
if (hostname === 'localhost') {
const port = window.location.port;
ws = new WebSocket(`ws://${hostname}:${port}/ws?flightId=${flightId}`);
} else {
ws = new WebSocket(`wss://${hostname}/ws?flightId=${flightId}`);
}
title.textContent = `Book seat for flight ${flightId}`;
ws.onopen = () => {
console.log("Connected to WebSocket server");
};
function createSeatGrid(seats) {
seatGrid.innerHTML = "";
for (let row = 1; row <= 10; row++) {
for (let col = 0; col < 6; col++) {
if (col === 3) {
const aisle = document.createElement("div");
aisle.className = "aisle";
seatGrid.appendChild(aisle);
}
const seatNumber = `${row}${String.fromCharCode(65 + col)}`;
const seat = seats.find((s) => s.seatNumber === seatNumber);
const seatElement = document.createElement("div");
seatElement.className = `seat ${seat && seat.occupant ? "unavailable" : "available"}`;
seatElement.textContent = seatNumber;
seatElement.onclick = () => bookSeat(seatNumber);
seatGrid.appendChild(seatElement);
}
}
}
async function fetchSeats() {
const response = await fetch(`/seats?flightId=${flightId}`);
const seats = await response.json();
createSeatGrid(seats);
}
async function bookSeat(seatNumber) {
const name = prompt("Please enter your name:");
if (!name) {
return; // User canceled the prompt
}
const response = await fetch(`book-seat?flightId=${flightId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ seatNumber, name }),
});
const result = await response.text();
fetchSeats();
}
ws.onmessage = (event) => {
try {
const seats = JSON.parse(event.data);
createSeatGrid(seats);
} catch (error) {
console.error("Error parsing WebSocket message:", error);
}
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
ws.onclose = (event) => {
console.log("WebSocket connection closed:", event);
};
fetchSeats();
}
</script>
</body>
</html>- Фронтенд выполняет HTTP
GETзапрос к/seatsэндпоинт для получения доступных мест на рейс. - Кроме того, он использует WebSocket-соединение для получения обновлений о доступных местах.
- Когда пользователь нажимает на место,
bookSeat()-функция вызывается, предлагая пользователю ввести своё имя, а затем выполняетPOSTзапрос к/book-seatконечная точка.
- Обновите привязки в конфигурационный файл Wrangler чтобы настроить
assetsчтобы обслуживатьpublicкаталог.
{
"assets": {
"directory": "public"
}
}[assets]
directory = "public"- Если вы запустите сервер разработки с помощью следующей команды, фронтенд будет доступен по адресу
http://localhost:8787. Однако это не будет работать, потому что бэкенд ещё не реализован.
npm run dev3. Создание таблицы для каждого рейса
В приложении уже настроена привязка для класса Durable Objects в конфигурационный файл Wrangler. Если вы измените имя класса Durable Objects в src/index.ts, также обязательно обновите binding в конфигурационный файл Wrangler.
- Обновите привязку, чтобы использовать хранилище SQLite в Durable Objects. В конфигурационный файл Wrangler, замените
new_classes=["Flight"]сnew_sqlite_classes=["Flight"],name = "FLIGHT"сname = "FLIGHT", а такжеclass_name = "MyDurableObject"сclass_name = "Flight". ваш конфигурационный файл Wrangler должен выглядеть примерно так:
{
"durable_objects": {
"bindings": [
{
"name": "FLIGHT",
"class_name": "Flight"
}
]
},
// Durable Object migrations.
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"Flight"
]
}
]
}[[durable_objects.bindings]]
name = "FLIGHT"
class_name = "Flight"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Flight" ]Теперь ваше приложение может использовать хранилище SQLite в Durable Objects.
- Добавьте
initializeSeats()функцию вFlightкласс. Эта функция вызывается при инициализации Durable Object. Она проверяет, существует ли таблица, и если нет, создаёт её. Также она добавляет в таблицу информацию о местах.
В этом руководстве функция создаёт одинаковую схему мест для всех рейсов. В реальном проекте эту функцию стоит изменить так, чтобы места добавлялись в зависимости от типа рейса.
Замените Flight класс, используя следующий код:
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
sql = this.ctx.storage.sql;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.initializeSeats();
}
private initializeSeats() {
const cursor = this.sql.exec(`PRAGMA table_list`);
// Check if a table exists.
if ([...cursor].find((t) => t.name === "seats")) {
console.log("Table already exists");
return;
}
this.sql.exec(`
CREATE TABLE IF NOT EXISTS seats (
seatId TEXT PRIMARY KEY,
occupant TEXT
)
`);
// For this demo, we populate the table with 60 seats.
// Since SQLite in DOs is fast, we can do a query per INSERT instead of batching them in a transaction.
for (let row = 1; row <= 10; row++) {
for (let col = 0; col < 6; col++) {
const seatNumber = `${row}${String.fromCharCode(65 + col)}`;
this.sql.exec(`INSERT INTO seats VALUES (?, null)`, seatNumber);
}
}
}
}- Добавьте
fetch-обработчик кFlightкласс. Этот обработчик возвращает текстовый ответ. В Шаг 5 Вы обновитеfetch-обработчик для обработки WebSocket-соединения.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
async fetch(request: Request): Promise<Response> {
return new Response("Hello from Durable Object!", { status: 200 });
}
}- Далее обновите обработчик fetch в Worker, чтобы он создавал уникальный Durable Object для каждого рейса.
export default {
async fetch(request, env, ctx): Promise<Response> {
// Get flight id from the query parameter
const url = new URL(request.url);
const flightId = url.searchParams.get("flightId");
if (!flightId) {
return new Response(
"Flight ID not found. Provide flightId in the query parameter",
{ status: 404 },
);
}
const stub = env.FLIGHT.getByName(flightId);
return stub.fetch(request);
},
} satisfies ExportedHandler<Env>;По идентификатору рейса, полученному из параметра запроса, создается уникальный Durable Object. Если таблица еще не существует, этот Durable Object инициализирует ее.
4. Добавление методов в Durable Object
- Добавьте
getSeats()функцию вFlightкласс. Эта функция возвращает все места из таблицы.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
let results = [];
// Query returns a cursor.
let cursor = this.sql.exec(`SELECT seatId, occupant FROM seats`);
// Cursors are iterable.
for (let row of cursor) {
// Each row is an object with a property for each column.
results.push({ seatNumber: row.seatId, occupant: row.occupant });
}
return results;
}
}- Добавьте
assignSeat()функцию вFlightкласс. Эта функция назначает место пассажиру. Она принимает номер места и имя пассажира в качестве параметров.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
// Check that seat isn't occupied.
let cursor = this.sql.exec(
`SELECT occupant FROM seats WHERE seatId = ?`,
seatId,
);
let result = cursor.toArray()[0]; // Get the first result from the cursor.
if (!result) {
return {message: 'Seat not available', status: 400 };
}
if (result.occupant !== null) {
return {message: 'Seat not available', status: 400 };
}
// If the occupant is already in a different seat, remove them.
this.sql.exec(
`UPDATE seats SET occupant = null WHERE occupant = ?`,
occupant,
);
// Assign the seat. Note: We don't have to worry that a concurrent request may
// have grabbed the seat between the two queries, because the code is synchronous
// (no `await`s) and the database is private to this Durable Object. Nothing else
// could have changed since we checked that the seat was available earlier!
this.sql.exec(
`UPDATE seats SET occupant = ? WHERE seatId = ?`,
occupant,
seatId,
);
// Broadcast the updated seats.
this.broadcastSeats();
return {message: `Seat ${seatId} booked successfully`, status: 200 };
}
}Приведённая выше функция использует broadcastSeats()-функцию, чтобы разослать обновлённые места всем подключённым клиентам. В следующем разделе мы добавим broadcastSeats() функцию.
5. Обработка подключений WebSocket
Все клиенты подключаются к Durable Object по WebSocket. Durable Object рассылает обновлённые места всем подключённым клиентам, что позволяет им обновлять интерфейс в реальном времени.
- Добавьте
handleWebSocket()функцию вFlightкласс. Эта функция обрабатывает WebSocket соединения.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
...
}
private handleWebSocket(request: Request) {
console.log('WebSocket connection requested');
const [client, server] = Object.values(new WebSocketPair());
this.ctx.acceptWebSocket(server);
console.log('WebSocket connection established');
return new Response(null, { status: 101, webSocket: client });
}
}- Добавьте
broadcastSeats()функцию вFlightкласс. Эта функция рассылает обновлённые места всем подключённым клиентам.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
...
}
private handleWebSocket(request: Request) {
...
}
private broadcastSeats() {
this.ctx.getWebSockets().forEach((ws) => ws.send(this.getSeats()));
}
}- Далее обновите
fetch-обработчик вFlightкласс. Этот обработчик принимает все входящие запросы от Worker и управляет WebSocket соединениями с помощьюhandleWebSocket()метод.
import { DurableObject } from "cloudflare:workers";
export class Flight extends DurableObject {
...
private initializeSeats() {
...
}
// Get all seats.
getSeats() {
...
}
// Assign a seat to a passenger.
assignSeat(seatId: string, occupant: string) {
...
}
private handleWebSocket(request: Request) {
...
}
private broadcastSeats() {
...
}
async fetch(request: Request) {
return this.handleWebSocket(request);
}
}- Наконец, обновите
fetch-обработчик Worker'а.
export default {
...
async fetch(request, env, ctx): Promise<Response> {
// Get flight id from the query parameter
...
if (request.method === "GET" && url.pathname === "/seats") {
return new Response(JSON.stringify(await stub.getSeats()), {
headers: { 'Content-Type': 'application/json' },
});
} else if (request.method === "POST" && url.pathname === "/book-seat") {
const { seatNumber, name } = (await request.json()) as {
seatNumber: string;
name: string;
};
const result = await stub.assignSeat(seatNumber, name);
return new Response(JSON.stringify(result));
} else if (request.headers.get("Upgrade") === "websocket") {
return stub.fetch(request);
}
return new Response("Not found", { status: 404 });
},
} satisfies ExportedHandler<Env>; fetch-обработчик в Worker теперь вызывает соответствующую функцию Durable Object для обработки входящего запроса. Если запрос представляет собой GET запрос к /seats, Worker возвращает места из Durable Object. Если запрос является POST запрос к /book-seat, Worker вызывает bookSeat объекта Durable Object, чтобы назначить место пассажиру. Если запрос представляет собой WebSocket-соединение, Durable Object обрабатывает это WebSocket-соединение.
6. Тестирование приложения
Вы можете протестировать приложение локально, выполнив следующую команду:
npm run devТак запускается локальный сервер разработки, на котором работает приложение. Приложение будет доступно по адресу http://localhost:8787.
Перейдите к приложению по адресу http://localhost:8787 в браузере. Поскольку идентификатор рейса не указан, приложение отображает сообщение об ошибке.
Обновите URL, указав идентификатор рейса как http://localhost:8787?flightId=1234. Приложение отображает места на рейс с ID 1234.
7. Развёртывание приложения
Чтобы развернуть приложение, выполните следующую команду:
npm run deploy ⛅️ wrangler 3.78.8
-------------------
🌀 Building list of assets...
🌀 Starting asset upload...
🌀 Found 1 new or modified file to upload. Proceeding with upload...
+ /index.html
Uploaded 1 of 1 assets
✨ Success! Uploaded 1 file (1.93 sec)
Total Upload: 3.45 KiB / gzip: 1.39 KiB
Your worker has access to the following bindings:
- Durable Objects:
- FLIGHT: Flight
Uploaded seat-book (12.12 sec)
Deployed seat-book triggers (5.54 sec)
[DEPLOYED_APP_LINK]
Current Version ID: [BINDING_ID]Перейдите в [DEPLOYED_APP_LINK] чтобы увидеть приложение. И снова не забудьте передать ID рейса в качестве параметра строки запроса.
Сводка
В этом руководстве вы выполнили следующее:
- использовал бэкенд хранения SQLite в Durable Objects для хранения мест на рейс.
- создали класс Durable Object для управления бронированием мест.
- развернули приложение в Cloudflare Workers!
Полный код этого руководства доступен на GitHub ↗.