Введение
Обычный Cloudflare Worker может обрабатывать множество запросов, но нельзя предполагать, что следующий запрос попадёт в тот же работающий экземпляр JavaScript. Такая архитектура без состояния отлично подходит для независимых операций. Однако она становится неудобной, когда нескольким запросам нужно работать с одним изменяемым значением — например, с количеством людей в очереди службы поддержки.
Durable Object предоставляет приложению один адресуемый объект координации. В этой лабораторной работе каждое имя счётчика выбирает отдельный объект. Запросы к support снова и снова попадают в один логический счётчик, а запросы к billing — в другой счётчик с отдельным состоянием. Cloudflare может перемещать или перезапускать базовую среду выполнения, но стабильный идентификатор объекта и состояние в SQLite остаются частью контракта приложения.
Вы свяжете четыре понятия:
- Класс определяет, какие операции может выполнять один объект-счётчик.
- Пространство имён — это коллекция объектов, созданных на основе этого класса.
- Binding предоставляет входному Worker доступ к пространству имён.
getByName()превращает одно и то же проверенное имя в одну и ту же ссылку на объект, а метод RPC вызывает код этого объекта.
Вы создадите приложение, локально подтвердите маршрутизацию по имени, развернёте его в собственной учебной учётной записи Cloudflare, сопоставите данные из терминала с Dashboard, а после завершения удалите пространство имён класса и Worker.
Перед началом этого курса пройдите лабораторную работу Connect LabEx to Your Cloudflare Account. В ней объясняется работа с терминалом виртуальной машины LabEx, авторизация устройства Wrangler, подтверждение учётной записи и настройка account ID. Предполагается, что вы уже знаете, как небольшой JavaScript Worker обрабатывает HTTP-запрос. Предварительные знания о Durable Objects не требуются.
Согласно текущей официальной документации, Durable Objects с хранилищем SQLite доступны в Workers Free. В этой лабораторной работе создаётся одно временное пространство имён класса, несколько небольших объектов и выполняется только ограниченное количество запросов. План Workers Paid не требуется. В процессе настройки устанавливаются Node.js 22.22.0 и локальный Wrangler 4.132.0 в /home/labex/project/named-counters; настройка не выполняет вход, не создаёт облачные ресурсы, не развёртывает код и не завершает реализацию учащегося.
Авторизуйте виртуальную машину и задайте имя приложения
На этом шаге вы подключите новую виртуальную машину LabEx к учебной учётной записи Cloudflare и создадите уникальную конфигурацию приложения. Вход в Dashboard через браузер не авторизует автоматически команды внутри новой виртуальной машины.
Перейдите в подготовленный проект и проверьте закреплённую версию Wrangler:
cd /home/labex/project/named-counters
npx wrangler --version
Ожидаемый результат — 4.132.0. Запустите процесс авторизации устройства Wrangler:
npx wrangler login --device --browser=false
Wrangler выведет URL и короткий код устройства. Откройте URL в браузере, введите код, убедитесь, что выбрана ваша выделенная учебная учётная запись, и проверьте запрашиваемые разрешения перед авторизацией. Разрешение фонового доступа может отображаться потому, что Wrangler должен продолжать работу после вашего возвращения в терминал. Никогда не передавайте пароль или токен через терминал.
После сообщения об успешной авторизации в браузере вернитесь в терминал и дождитесь завершения работы Wrangler. Запросите структурированную информацию об учётной записи:
npx wrangler whoami --json
Убедитесь, что указано loggedIn: true, затем определите нужную учётную запись — даже если отображается только одна. Имя учётной записи служит проверкой для человека, а ID — стабильным значением конфигурации, которое не нужно выводить в терминал.
Сохраните структурированный результат, выведите только несекретное имя учётной записи и выберите соответствующий ID для LabEx Learning:
WHOAMI="$(npx wrangler whoami --json)"
printf '%s\n' "$WHOAMI" | jq '{loggedIn, authType, accounts: [.accounts[] | {name}]}'
ACCOUNT_ID="$(printf '%s\n' "$WHOAMI" | jq -r '.accounts[] | select(.name == "LabEx Learning") | .id')"
test -n "$ACCOUNT_ID"
Конструкция $(...) записывает вывод команды в переменную оболочки. Сначала jq выводит только имя учётной записи для проверки, затем в отдельной операции выбирает связанный с ним ID. Команда test -n завершается успешно только в том случае, если выбранное значение не пустое. Если у вашей выделенной учебной учётной записи отображается другое имя, после его проверки замените LabEx Learning в выражении выбора.
Сгенерируйте уникальное имя Worker. Команда openssl rand -hex 6 создаёт 12 случайных шестнадцатеричных символов, а $(...) вставляет их в переменную оболочки:
RUN="labex-c10-o01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"
Создайте wrangler.jsonc. Файл конфигурации сообщает Wrangler, какой код нужно развернуть и какие возможности Cloudflare следует подключить к среде выполнения. Маркер JSON без кавычек позволяет подставить значения $RUN и $ACCOUNT_ID, а обратная косая черта сохраняет ключ $schema в исходном виде.
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/index.js",
"compatibility_date": "2026-09-18",
"workers_dev": true,
"preview_urls": false,
"observability": {
"enabled": true,
"head_sampling_rate": 1
},
"durable_objects": {
"bindings": [
{ "name": "COUNTERS", "class_name": "Counter" }
]
},
"exports": {
"Counter": { "type": "durable-object", "storage": "sqlite" }
}
}
JSON
Этот файл описывает приложение, но пока ничего не создаёт в Cloudflare. Параметр observability сохраняет журналы запросов и приложения для последующей проверки в Dashboard. Поля Durable Objects станут значимыми на следующем шаге.
Свяжите пространство имён, binding и класс
На этом шаге вы разберёте конфигурацию Durable Objects как схему прохождения запроса к одному объекту с состоянием, а затем сгенерируете типы среды выполнения, открывающие binding в вашем коде.
Класс Durable Object — это JavaScript-шаблон одного объекта. Позже вы напишете класс Counter, который определит такие операции, как увеличение и чтение значения.
Пространство имён — это коллекция всех объектов, созданных на основе этого класса. Одно пространство имён может содержать support, billing и множество других именованных счётчиков. Это не означает, что счётчики используют одно общее значение: каждый стабильный идентификатор объекта владеет отдельным хранилищем.
Binding — это имя, с помощью которого входной Worker обращается к пространству имён. В этой конфигурации имя COUNTERS связано с классом Counter. Поэтому в коде вы будете использовать env.COUNTERS.
Запись exports объявляет текущее состояние жизненного цикла класса. Она сообщает Cloudflare, что при первом развёртывании нужно создать Counter с серверной частью хранения SQLite. SQLite рекомендуется для новых классов и доступен в Workers Free. В небольшой таблице этой лабораторной работы внутри каждого объекта хранится только одно целое число.
Сгенерируйте описание типов из конфигурации:
npx wrangler types
Найдите COUNTERS в созданном файле:
grep -n 'COUNTERS' worker-configuration.d.ts
Строка будет похожа на эту:
COUNTERS: DurableObjectNamespace<import("./src/index").Counter>;
Точный окружающий сгенерированный текст может измениться, но важны три факта: binding называется COUNTERS, имеет тип DurableObjectNamespace, а также указывает на экспортированный класс Counter. При изменении binding повторно генерируйте типы, чтобы конфигурация и код не расходились незаметно.
Создайте именованный счётчик
На этом шаге вы реализуете класс Counter и входной Worker, который направляет проверенное имя из URL к одному объекту.
Каждый Durable Object имеет собственное хранилище. Конструктор создаёт таблицу из одной строки с именем counter_state и добавляет начальное значение только в том случае, если строки ещё нет. Метод blockConcurrencyWhile() задерживает обработку запросов к объекту, пока эта короткая инициализация не завершится. Он подходит для настройки схемы, но не должен оборачивать каждый запрос или внешнюю сетевую операцию.
Открытые методы increment() и getCount() являются методами RPC. RPC — сокращение от remote procedure call, то есть удалённого вызова процедуры. Он позволяет Worker вызывать метод в заглушке Durable Object так, как если бы это был асинхронный объект JavaScript. Cloudflare передаёт вызов выбранному объекту.
Создайте точку входа Worker:
cat > src/index.js <<'JS'
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS counter_state (
key INTEGER PRIMARY KEY CHECK (key = 1),
value INTEGER NOT NULL
)
`);
this.ctx.storage.sql.exec(
"INSERT OR IGNORE INTO counter_state (key, value) VALUES (1, 0)"
);
});
}
increment() {
return this.ctx.storage.sql
.exec("UPDATE counter_state SET value = value + 1 WHERE key = 1 RETURNING value")
.one().value;
}
getCount() {
return this.ctx.storage.sql
.exec("SELECT value FROM counter_state WHERE key = 1")
.one().value;
}
}
function json(data, status = 200) {
return Response.json(data, { status });
}
function counterName(pathname) {
const match = pathname.match(/^\/counters\/([^/]+)$/);
if (!match) return { error: "not_found", status: 404 };
let name;
try {
name = decodeURIComponent(match[1]);
} catch {
return { error: "invalid_counter_name", status: 400 };
}
if (!/^[a-z][a-z0-9-]{0,31}$/.test(name)) {
return { error: "invalid_counter_name", status: 400 };
}
return { name };
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "GET" && url.pathname === "/health") {
return json({ status: "ok" });
}
const parsed = counterName(url.pathname);
if (parsed.error) return json({ error: parsed.error }, parsed.status);
if (request.method !== "GET" && request.method !== "POST") {
return json({ error: "method_not_allowed" }, 405);
}
const name = parsed.name;
const stub = env.COUNTERS.getByName(name);
const count = request.method === "POST"
? await stub.increment()
: await stub.getCount();
console.log(JSON.stringify({
event: request.method === "POST" ? "counter_incremented" : "counter_read",
name,
count
}));
return json({ name, count });
}
};
JS
Строка маршрутизации getByName(name) задаёт границу идентичности. Один и тот же проверенный текст детерминированно выбирает один и тот же логический объект, а другой текст выбирает другой объект. Заглушка — это только ссылка. Объект создаётся отложенно, когда вызов RPC действительно доходит до него.
Запустите предоставленные детерминированные тесты. Они используют небольшую локальную фикстуру пространства имён и поэтому не отправляют запросы в облако:
NODE_NO_WARNINGS=1 node --experimental-loader ./test/cloudflare-loader.mjs --test test/worker.test.mjs
Небольшой загрузчик предоставляет только локальную замену базового класса cloudflare:workers, чтобы Node мог импортировать модуль; фикстура пространства имён по-прежнему управляет каждым тестируемым вызовом, и API Cloudflare не используется. Ожидайте три успешно пройденных теста. Затем попросите Wrangler собрать Worker без развёртывания:
npx wrangler deploy --dry-run
Тесты подтверждают контракт HTTP-маршрутизации, а пробный запуск подтверждает, что Wrangler может собрать настоящий класс Durable Object. Ни одно из этих действий не создаёт удалённое пространство имён.
Локально подтвердите стабильность имён
На этом шаге вы запустите приложение в локальной среде выполнения Workers и используете два имени, чтобы увидеть правило маршрутизации до создания облачного ресурса.
Запустите Wrangler в фоновом режиме на порту 8787. Символ > сохраняет журналы в файл, 2>&1 объединяет ошибки с обычным выводом, а & возвращает приглашение командной строки. $! содержит идентификатор процесса только что запущенной команды.
npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
Дождитесь ответа маршрута проверки состояния. Цикл выполняет попытку раз в секунду и останавливается сразу после ответа Worker:
for attempt in $(seq 1 30); do
if curl --silent --fail http://127.0.0.1:8787/health; then
break
fi
sleep 1
done
Ожидайте {"status":"ok"}. Дважды увеличьте счётчик support:
curl --silent --request POST http://127.0.0.1:8787/counters/support | jq
curl --silent --request POST http://127.0.0.1:8787/counters/support | jq
В ответах значение support изменится с 1 на 2:
{
"name": "support",
"count": 2
}
Теперь один раз увеличьте billing:
curl --silent --request POST http://127.0.0.1:8787/counters/billing | jq
Его значение равно 1, а не 3. Пространство имён является коллекцией, а каждое имя выбирает изолированный объект внутри этой коллекции.
Прочитайте оба объекта, не изменяя их:
curl --silent http://127.0.0.1:8787/counters/support | jq
curl --silent http://127.0.0.1:8787/counters/billing | jq
Значения останутся равными 2 и 1. Наконец, подтвердите, что некорректный ввод отклоняется ещё до того, как getByName() сможет выбрать объект:
curl --silent --request POST --write-out '\nHTTP %{http_code}\n' \
http://127.0.0.1:8787/counters/Not_Allowed
Ожидайте {"error":"invalid_counter_name"} и HTTP 400. Символ подчёркивания и прописные буквы не соответствуют описанному правилу именования.
Разверните приложение и изучите пространство имён
На этом шаге вы остановите локальную среду выполнения, развернёте то же приложение в Cloudflare и сопоставите поведение API с пространством имён, binding, метриками и журналами, доступными в Dashboard.
Остановите только процесс разработки, ID которого вы сохранили:
kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
Разверните Worker и объявленный класс Counter с хранилищем SQLite:
npx wrangler deploy
Wrangler выведет общедоступный URL workers.dev и результат согласования класса. Сохраните точный URL, заменив пример:
WORKER_URL="https://YOUR_WORKER_URL"
Маршруту на периферии может потребоваться немного времени для готовности. Проверяйте только маршрут состояния, который не обращается к Durable Object:
for attempt in $(seq 1 30); do
if curl --silent --fail "$WORKER_URL/health"; then
break
fi
sleep 2
done
Создайте два запроса для support и один для billing:
curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/billing" | jq
Прочитайте значения:
curl --silent "$WORKER_URL/counters/support" | jq
curl --silent "$WORKER_URL/counters/billing" | jq
Удалённое приложение должно показать тот же контракт идентичности, что и локальная среда выполнения: значение support равно 2, а billing — 1.
Откройте Workers & Pages в Cloudflare Dashboard. Ваш Worker с уникальным именем появится в списке приложений. Имя Worker, временные метки и общие показатели использования учётной записи на следующем снимке экрана относятся к проверочному запуску; найдите имя labex-c10-o01-..., сгенерированное в вашем терминале.

Откройте Cloudflare Dashboard и перейдите в Workers & Pages → Overview → ваш Worker labex-c10-o01-... → Settings → Bindings. Найдите binding Durable Object с именем COUNTERS и его класс Counter. Worker знает имя binding, а Cloudflare связывает его с пространством имён, объявленным экспортом класса.
На схеме binding должно быть показано, что Worker подключён к Durable Object через COUNTERS. Имена Worker и пространства имён на этом снимке экрана относятся к конкретному запуску и приведены только в качестве примера; важны имя binding и связь между объектами.

Затем откройте Durable Objects в навигации Developer Platform. Выберите пространство имён, принадлежащее вашему временному Worker. Убедитесь, что оно использует хранилище SQLite, а класс называется Counter. Пространство имён — это коллекция уровня класса; имена support и billing обозначают объекты внутри неё.
В обзоре пространства имён отображается Storage: SQL. Его имя и ID относятся к временному проверочному запуску, поэтому у вас значения будут другими.

Откройте представление пространства имён Metrics. Недавние запросы могут появиться не сразу, поэтому временно пустой график не даёт однозначного вывода. Не создавайте большой цикл запросов только для того, чтобы принудительно построить график.
На примере снимка экрана пространства имён по-прежнему отображается ноль недавних вызовов, хотя запросы среды выполнения завершились успешно. Это показывает, почему отложенные метрики Dashboard являются дополнительным контекстом, а не основным функциональным подтверждением.
Вернитесь к Worker и откройте Observability → Logs. Найдите недавнее событие counter_incremented или counter_read. В структурированном журнале содержатся синтетическое имя счётчика и его значение, но нет идентификатора учётной записи или учётных данных. Сопоставьте событие с одним из ограниченных запросов выше.
Разверните подходящее событие. В проверочном запуске имя, созданное верификатором, достигло значения 2, а диаграмма событий показала успешные запросы и нулевое количество ошибок. У вас синтетическое имя и значения будут другими.

Значения Dashboard, такие как имена Worker, ID объектов, временные метки и количество запросов, зависят от конкретного запуска. Проверки через CLI, API и среду выполнения остаются основным подтверждением; представления Dashboard показывают, где можно увидеть те же связи.
Удалите пространство имён и выйдите из системы
На этом шаге вы намеренно выведете из эксплуатации класс Counter, удалите его пространство имён и сохранённые данные, удалите Worker, а затем отзовёте сеанс Wrangler этой виртуальной машины.
Одного удаления скрипта Worker недостаточно, чтобы однозначно заявить о необходимости удалить сохранённые данные Durable Object. В жизненном цикле exports используется удалённая tombstone-запись — краткосрочная конфигурационная запись, сообщающая Cloudflare о необходимости безвозвратно удалить пространство имён одного класса. Для этой операции нет корзины, поэтому убедитесь, что класс и имя Worker относятся к этой лабораторной работе.
Создайте минимальную точку входа для очистки без экспорта Counter:
cat > src/cleanup.js <<'JS'
export default {
fetch() {
return Response.json({ status: "cleanup" }, { status: 410 });
}
};
JS
Создайте конфигурацию очистки. Она сохраняет то же имя Worker и учётную запись, удаляет binding и помечает только Counter как удалённый:
ACCOUNT_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).account_id)')"
cat > wrangler.cleanup.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/cleanup.js",
"compatibility_date": "2026-09-18",
"workers_dev": true,
"preview_urls": false,
"exports": {
"Counter": { "type": "durable-object", "state": "deleted" }
}
}
JSON
Разверните tombstone-запись:
npx wrangler deploy --config wrangler.cleanup.jsonc
Внимательно прочитайте результат согласования Wrangler. В нём должно быть указано, что Counter удалён. Это безвозвратно удаляет пространство имён класса и небольшие значения, сохранённые объектами support, billing и независимым верификатором.
Теперь удалите оставшийся Worker без состояния:
npx wrangler delete --config wrangler.cleanup.jsonc
Подтвердите, что выбран именно нужный application labex-c10-o01-.... В Dashboard убедитесь, что этот Worker отсутствует, а пространство имён, которым он владел, больше не отображается. Исторические метрики или журналы могут временно сохраняться и не являются активными ресурсами.
Перед удалением авторизации выполните проверку удаления с аутентификацией:
python3 .labex/verify.py deleted
Только после вывода PASS: deleted выйдите из системы:
npx wrangler logout
npx wrangler whoami --json
В итоговом выводе должно явно отображаться loggedIn: false. Сетевая ошибка не является доказательством выхода из системы.
Итоги
Вы создали и использовали своё первое приложение на Durable Objects. Вы узнали, что класс определяет поведение одного объекта, пространство имён объединяет объекты этого класса, binding открывает пространство имён для Worker, а getByName() детерминированно выбирает один логический объект. Методы RPC изменяли состояние в SQLite и читали его, повторяющиеся имена использовали общий счётчик, разные имена оставались изолированными, а некорректные имена отклонялись до выбора объекта.
Вы также связали поведение среды выполнения с Cloudflare Dashboard, а затем использовали декларативную tombstone-запись класса, чтобы удалить пространство имён и его данные перед удалением Worker и выходом из системы. В следующей лабораторной работе эта модель идентичности будет расширена: SQLite будет использоваться как журнал активности, а также будет показано, чем постоянное хранилище отличается от временного состояния в памяти.



