← Cloudflare Workers / workers / tutorials
Создайте Jamstack-приложение со списком задач
В этом руководстве вы создадите список задач на HTML, CSS и JavaScript. Данные приложения будут храниться в Workers KV.
Прежде чем приступать к этому проекту, у вас уже должен быть некоторый опыт работы с HTML, CSS и JavaScript. Вы узнаете:
- Как разработка с помощью Workers позволяет сосредоточиться на написании кода и выпускать готовые продукты.
- Как добавление Workers KV делает этот туториал отличным введением в разработку полноценных приложений, управляемых данными.
Если вы хотите увидеть готовый код этого проекта, найдите проект на GitHub ↗ и обратитесь к демонстрация в реальном времени ↗ чтобы ознакомиться с тем, что вы будете создавать.
Прежде чем начать
Во всех руководствах предполагается, что вы уже выполнили Руководство по началу работы, который поможет вам настроить аккаунт Cloudflare Workers, C3 ↗, а также Wrangler.
1. Создайте новый проект Workers
Сначала используйте create-cloudflare ↗ CLI-инструмент для создания нового проекта Cloudflare Workers с именем todos. В этом руководстве вы будете использовать стандартный Hello World шаблон для создания проекта Workers.
npm create cloudflare@latest -- todosДля настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker only. - Для Какой язык вы хотите использовать?, выберите
JavaScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
Перейдите в только что созданный каталог:
cd todosВнутри вашего нового todos каталог проекта Worker, index.js представляет собой точку входа в ваше приложение Cloudflare Workers.
Все входящие HTTP-запросы к Worker передаются в fetch() обработчик как запрос объект. После того как Worker получает запрос, ответ, сформированный вашим приложением, возвращается пользователю. В этом руководстве вы узнаете, как работает модель запрос/ответ и как использовать её для создания полнофункциональных приложений.
export default {
async fetch(request, env, ctx) {
return new Response("Hello World!");
},
};В своём стандартном index.js файле вы можете увидеть этот паттерн запрос-ответ в действии. fetch создаёт новый Response с текстом тела 'Hello World!'.
Когда Worker получает request, Worker возвращает клиенту только что созданный ответ. Ваш Worker будет отдавать новые ответы напрямую из глобальная сеть Cloudflare ↗ вместо перенаправления на ваш исходный сервер. Обычный сервер принимает запросы и возвращает ответы. Cloudflare Workers позволяет отвечать, формируя ответы напрямую в глобальной сети Cloudflare.
2. Проверка сведений о проекте
Любой проект, который вы развёртываете в Cloudflare Workers, может использовать современные инструменты JavaScript, такие как ES modules, npm пакеты, а также async/await ↗ функции для создания приложения. Помимо написания Workers, вы можете использовать Workers, чтобы создание полноценных приложений с использованием тех же инструментов и процесса, что и в этом руководстве.
В этом руководстве вы создадите список задач на Workers, который позволяет читать данные из KV хранилище и использования данных для формирования HTML-ответа, отправляемого клиенту.
Работа по созданию этого приложения делится на три задачи:
- Записывайте данные в KV.
- Отображение данных из KV.
- Добавление задач через интерфейс приложения.
В оставшейся части этого руководства вы будете выполнять каждую задачу, постепенно дорабатывая своё приложение, а затем опубликуете его в собственном домене.
3. Запись данных в KV
Для начала разберитесь, как заполнить список задач реальными данными. Для этого используйте Cloudflare Workers KV : хранилище ключ-значение, к которому можно обращаться из вашего Worker для чтения и записи данных.
Чтобы начать работу с KV, создайте пространство имён. Все кешированные данные будут храниться в этом пространстве имён, и после настройки вы сможете обращаться к нему внутри Worker через предопределённую переменную. Используйте Wrangler, чтобы создать новое пространство имён с именем TODOS на kv namespace create команда и получите связанный ID пространства имён, выполнив в терминале следующую команду:
npx wrangler kv namespace create "TODOS" --previewСвязанное пространство имён можно объединить с --preview флаг для взаимодействия с preview-пространством имён вместо production-пространства имён. Пространства имён можно добавить в приложение, определив их в конфигурации Wrangler. Скопируйте ID только что созданного пространства имён и в своём конфигурационный файл Wrangler, определите kv_namespaces ключ для настройки вашего пространства имён:
{
"kv_namespaces": [
{
"binding": "TODOS",
"id": "<YOUR_ID>",
"preview_id": "<YOUR_PREVIEW_ID>"
}
]
}[[kv_namespaces]]
binding = "TODOS"
id = "<YOUR_ID>"
preview_id = "<YOUR_PREVIEW_ID>"Определённое пространство имён, TODOS, теперь будет доступен в вашей кодовой базе. Теперь самое время разобраться с KV API. У KV namespace есть три основных метода для работы с кешем: get, put, а также delete.
Начните сохранять данные, определив начальный набор данных, который вы поместите в кэш с помощью put метод. В следующем примере определяется defaultData объект вместо массива задач todo. Позже вы можете захотеть хранить метаданные и другую информацию внутри этого объекта кеша. Имея такой объект данных, используйте JSON.stringify чтобы добавить строку в кеш:
export default {
async fetch(request, env, ctx) {
const defaultData = {
todos: [
{
id: 1,
name: "Finish the Cloudflare Workers blog post",
completed: false,
},
],
};
await env.TODOS.put("data", JSON.stringify(defaultData));
return new Response("Hello World!");
},
};Workers KV представляет собой согласованное в конечном счёте глобальное хранилище данных. Любая запись в пределах региона сразу отражается в этом же регионе, но не сразу становится доступной в других регионах. Со временем эта запись становится доступной везде, и с этого момента Workers KV гарантирует согласованность данных в каждом регионе.
Поскольку данные уже есть в кэше, а сам кэш предполагается в конечном счёте согласованным, этот код нужно немного доработать: приложение должно проверять кэш и использовать значение, если ключ существует. Если ключа нет, используется defaultData в качестве источника данных пока что (в будущем это нужно будет изменить) и записывать их в кеш для дальнейшего использования. После разбиения кода на несколько функций для простоты результат выглядит так:
export default {
async fetch(request, env, ctx) {
const defaultData = {
todos: [
{
id: 1,
name: "Finish the Cloudflare Workers blog post",
completed: false,
},
],
};
const setCache = (data) => env.TODOS.put("data", data);
const getCache = () => env.TODOS.get("data");
let data;
const cache = await getCache();
if (!cache) {
await setCache(JSON.stringify(defaultData));
data = defaultData;
} else {
data = JSON.parse(cache);
}
return new Response(JSON.stringify(data));
},
};Отображение данных из KV
Поскольку в коде уже есть данные, а именно объект кэшированных данных приложения, эти данные нужно взять и отобразить в пользовательском интерфейсе.
Для этого создайте новый html переменную в своем скрипте Workers и используйте ее для формирования статического HTML шаблона, который можно отдавать клиенту. В fetch, создайте новый Response с Content-Type: text/html заголовок и отправить его клиенту:
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Todos</title>
</head>
<body>
<h1>Todos</h1>
</body>
</html>
`;
async fetch (request, env, ctx) {
// previous code
return new Response(html, {
headers: {
'Content-Type': 'text/html'
}
});
}У вас есть статический HTML-сайт, который уже рендерится, и вы можете начать заполнять его данными. В body добавьте div тег с id todos:
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Todos</title>
</head>
<body>
<h1>Todos</h1>
<div id="todos"></div>
</body>
</html>
`;Добавьте <script> элемент в конце содержимого body, который принимает todos массив. Для каждого todo в массиве, создайте div элемент и добавляет его в todos HTML-элемент:
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Todos</title>
</head>
<body>
<h1>Todos</h1>
<div id="todos"></div>
</body>
<script>
window.todos = []
var todoContainer = document.querySelector("#todos")
window.todos.forEach(todo => {
var el = document.createElement("div")
el.textContent = todo.name
todoContainer.appendChild(el)
})
</script>
</html>
`;Ваша статическая страница может принимать window.todos и отрисовывать HTML на его основе, но на самом деле вы ещё не передали никаких данных из KV. Для этого потребуется внести несколько изменений.
Сначала ваш html переменная изменится на функцию. Эта функция будет принимать todos аргумент, который заполнит window.todos переменную в приведенном выше примере кода:
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
window.todos = ${todos}
var todoContainer = document.querySelector("#todos")
// ...
<script>
</html>
`;В fetch, используйте полученные данные KV для вызова html функция и сгенерировать Response на основе этого:
async fetch (request, env, ctx) {
const body = html(JSON.stringify(data.todos).replace(/</g, '\\u003c'));
return new Response(body, {
headers: { 'Content-Type': 'text/html' },
});
}4. Добавьте задачи через пользовательский интерфейс (UI)
К этому моменту вы уже создали Cloudflare Worker, который берёт данные из Cloudflare KV и на их основе рендерит статическую страницу. Эта страница читает данные и формирует список задач на их основе. Осталось научиться создавать задачи прямо из интерфейса приложения: вы можете добавлять их через KV API, обновляя кеш командой env.TODOS.put(newData).
Чтобы обновить задачу todo, добавьте в скрипт Workers второй обработчик, который будет отслеживать PUT запросов к /. Когда по этому URL получено тело запроса, Worker отправит новые данные todo в ваше хранилище KV.
Добавьте эту новую функциональность в fetch: если метод запроса PUT, тело запроса будет использовано для обновления кеша.
export default {
async fetch(request, env, ctx) {
const setCache = (data) => env.TODOS.put("data", data);
if (request.method === "PUT") {
const body = await request.text();
try {
JSON.parse(body);
await setCache(body);
return new Response(body, { status: 200 });
} catch (err) {
return new Response(err, { status: 500 });
}
}
// previous code
},
};Проверьте, что запрос является PUT и оберните оставшуюся часть кода в try...catch блок. Сначала разберите тело входящего запроса, убедившись, что это JSON, прежде чем обновить кеш новыми данными и вернуть их пользователю. Если что-то пойдёт не так, верните 500 код состояния. Если маршрут вызывается с HTTP-методом, отличным от PUT например, POST или DELETE : возвращает 404 ошибка.
С помощью этого скрипта можно добавить на HTML-страницу динамическую функциональность, которая обращается к этому route. Сначала создайте поле ввода для названия задачи и кнопку для ее отправки.
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<div>
<input type="text" name="name" placeholder="A new todo"></input>
<button id="create">Create</button>
</div>
<!-- existing script -->
</html>
`;На основе этого поля ввода и кнопки добавьте соответствующую функцию JavaScript, которая будет отслеживать нажатия на кнопку: как только кнопка будет нажата, браузер PUT к / и отправьте задачу.
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
// Existing JavaScript code
var createTodo = function() {
var input = document.querySelector("input[name=name]")
if (input.value.length) {
todos = [].concat(todos, {
id: todos.length + 1,
name: input.value,
completed: false,
})
fetch("/", {
method: "PUT",
body: JSON.stringify({ todos: todos }),
})
}
}
document.querySelector("#create").addEventListener("click", createTodo)
</script>
</html>
`;Этот код обновляет кеш. Помните, что кеш KV согласуется в конечном счёте: даже если Worker будет обновлён для чтения из кеша, нет гарантии, что данные в нём окажутся актуальными. Поэтому обновляйте список задач локально: возьмите исходный код отрисовки списка задач и оформите его в виде переиспользуемой функции с именем populateTodos, и вызывая ее при загрузке страницы и по завершении запроса к кэшу:
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
var populateTodos = function() {
var todoContainer = document.querySelector("#todos")
todoContainer.innerHTML = null
window.todos.forEach(todo => {
var el = document.createElement("div")
el.textContent = todo.name
todoContainer.appendChild(el)
})
}
populateTodos()
var createTodo = function() {
var input = document.querySelector("input[name=name]")
if (input.value.length) {
todos = [].concat(todos, {
id: todos.length + 1,
name: input.value,
completed: false,
})
fetch("/", {
method: "PUT",
body: JSON.stringify({ todos: todos }),
})
populateTodos()
input.value = ""
}
}
document.querySelector("#create").addEventListener("click", createTodo)
</script>
`;Когда клиентский код готов, развертывание новой версии функции объединяет все эти части. В результате получается полноценный динамический список задач.
5. Обновите задачи через интерфейс приложения
Для последней части списка задач нужно уметь обновлять задачи, а именно отмечать их как выполненные.
К счастью, значительная часть инфраструктуры для этой работы уже создана. Вы можете обновлять данные списка задач в кэше, что подтверждается вашим createTodo функция. Обновление todo относится скорее к задачам на стороне клиента, чем на стороне Worker.
Для начала populateTodos функцию можно обновить, чтобы она генерировала div для каждой задачи. Кроме того, перенесите название задачи в дочерний элемент div:
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
var populateTodos = function() {
var todoContainer = document.querySelector("#todos")
todoContainer.innerHTML = null
window.todos.forEach(todo => {
var el = document.createElement("div")
var name = document.createElement("span")
name.textContent = todo.name
el.appendChild(name)
todoContainer.appendChild(el)
})
}
</script>
`;Клиентская часть этого кода спроектирована так, чтобы обрабатывать массив todo и отрисовывать список HTML-элементов. В процессе вы делали ряд вещей, применение которых пока оставалось не совсем понятным: в частности, добавление ID и обновление состояния completed у todo. Вместе эти элементы как раз и позволяют обновлять todo в интерфейсе приложения.
Для начала полезно добавить в HTML идентификатор каждой задачи todo. Это позволит позже обращаться к элементу и связывать его с соответствующей задачей в JavaScript-коде. Атрибуты данных и соответствующее dataset метод в JavaScript отлично подходит для реализации этого. При создании div элемент для каждой задачи, вы можете добавить атрибут данных с именем todo к каждому div:
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
var populateTodos = function() {
var todoContainer = document.querySelector("#todos")
todoContainer.innerHTML = null
window.todos.forEach(todo => {
var el = document.createElement("div")
el.dataset.todo = todo.id
var name = document.createElement("span")
name.textContent = todo.name
el.appendChild(name)
todoContainer.appendChild(el)
})
}
</script>
`;Внутри вашего HTML каждый div для задачи теперь содержит прикреплённый атрибут данных, который выглядит так:
<div data-todo="1"></div>
<div data-todo="2"></div>Теперь для каждого элемента todo можно создавать чекбокс. Новые todo по умолчанию будут отображаться с неотмеченным чекбоксом, однако вы можете отмечать его по мере отрисовки элемента в окне:
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
window.todos.forEach(todo => {
var el = document.createElement("div")
el.dataset.todo = todo.id
var name = document.createElement("span")
name.textContent = todo.name
var checkbox = document.createElement("input")
checkbox.type = "checkbox"
checkbox.checked = todo.completed ? 1 : 0
el.appendChild(checkbox)
el.appendChild(name)
todoContainer.appendChild(el)
})
</script>
`;Флажок настроен так, чтобы корректно отражать значение completed для каждой задачи, но пока не обновляется, когда вы фактически ставите или снимаете отметку. Чтобы это исправить, добавьте completeTodo функция в качестве обработчика события на click событие. Внутри функции проверьте элемент чекбокса, найдите его родителя (элемент todo div), и используйте его todo атрибут data, чтобы найти соответствующую задачу в массиве data. Вы можете переключить статус выполнения, обновить её свойства и перерисовать интерфейс:
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
var populateTodos = function() {
window.todos.forEach(todo => {
// Existing todo element set up code
checkbox.addEventListener("click", completeTodo)
})
}
var completeTodo = function(evt) {
var checkbox = evt.target
var todoElement = checkbox.parentNode
var newTodoSet = [].concat(window.todos)
var todo = newTodoSet.find(t => t.id == todoElement.dataset.todo)
todo.completed = !todo.completed
todos = newTodoSet
updateTodos()
}
</script>
`;Конечным результатом вашего кода является система, которая проверяет todos переменную, обновляет ваш кэш Cloudflare KV этим значением, а затем выполняет повторный рендеринг интерфейса на основе локально имеющихся данных.
6. Заключение и дальнейшие шаги
Пройдя это руководство, вы создали статическое приложение на HTML, CSS и JavaScript, которое незаметно для пользователя работает на Workers и Workers KV и полностью использует возможности глобальной сети Cloudflare.
Если вы хотите продолжать улучшать проект, можно реализовать более удачный дизайн (рабочую версию можно посмотреть по адресу todos.signalnerve.workers.dev ↗), либо внести дополнительные улучшения в безопасность и скорость работы.
Возможно, вы также захотите добавить кеширование для конкретных пользователей. Сейчас ключ кеша всегда data : это означает, что любой посетитель сайта будет видеть общий список задач вместе с другими посетителями. В своём Worker вы можете использовать значения из клиентского запроса, чтобы создавать и поддерживать списки для конкретных пользователей. Например, можно формировать ключ кеша на основе IP-адреса запроса:
export default {
async fetch(request, env, ctx) {
const defaultData = {
todos: [
{
id: 1,
name: "Finish the Cloudflare Workers blog post",
completed: false,
},
],
};
const setCache = (key, data) => env.TODOS.put(key, data);
const getCache = (key) => env.TODOS.get(key);
const ip = request.headers.get("CF-Connecting-IP");
const myKey = `data-${ip}`;
if (request.method === "PUT") {
const body = await request.text();
try {
JSON.parse(body);
await setCache(myKey, body);
return new Response(body, { status: 200 });
} catch (err) {
return new Response(err, { status: 500 });
}
}
let data;
const cache = await getCache();
if (!cache) {
await setCache(myKey, JSON.stringify(defaultData));
data = defaultData;
} else {
data = JSON.parse(cache);
}
const body = html(JSON.stringify(data.todos).replace(/</g, "\\u003c"));
return new Response(body, {
headers: {
"Content-Type": "text/html",
},
});
},
};После внесения этих изменений и повторного развёртывания Worker ваше приложение списка задач теперь поддерживает функциональность для отдельных пользователей, при этом по-прежнему полностью использует глобальную сеть Cloudflare.
Финальная версия скрипта вашего Worker должна выглядеть так:
const html = (todos) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Todos</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet"></link>
</head>
<body class="bg-blue-100">
<div class="w-full h-full flex content-center justify-center mt-8">
<div class="bg-white shadow-md rounded px-8 pt-6 py-8 mb-4">
<h1 class="block text-grey-800 text-md font-bold mb-2">Todos</h1>
<div class="flex">
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-800 leading-tight focus:outline-none focus:shadow-outline" type="text" name="name" placeholder="A new todo"></input>
<button class="bg-blue-500 hover:bg-blue-800 text-white font-bold ml-2 py-2 px-4 rounded focus:outline-none focus:shadow-outline" id="create" type="submit">Create</button>
</div>
<div class="mt-4" id="todos"></div>
</div>
</div>
</body>
<script>
window.todos = ${todos}
var updateTodos = function() {
fetch("/", { method: "PUT", body: JSON.stringify({ todos: window.todos }) })
populateTodos()
}
var completeTodo = function(evt) {
var checkbox = evt.target
var todoElement = checkbox.parentNode
var newTodoSet = [].concat(window.todos)
var todo = newTodoSet.find(t => t.id == todoElement.dataset.todo)
todo.completed = !todo.completed
window.todos = newTodoSet
updateTodos()
}
var populateTodos = function() {
var todoContainer = document.querySelector("#todos")
todoContainer.innerHTML = null
window.todos.forEach(todo => {
var el = document.createElement("div")
el.className = "border-t py-4"
el.dataset.todo = todo.id
var name = document.createElement("span")
name.className = todo.completed ? "line-through" : ""
name.textContent = todo.name
var checkbox = document.createElement("input")
checkbox.className = "mx-4"
checkbox.type = "checkbox"
checkbox.checked = todo.completed ? 1 : 0
checkbox.addEventListener("click", completeTodo)
el.appendChild(checkbox)
el.appendChild(name)
todoContainer.appendChild(el)
})
}
populateTodos()
var createTodo = function() {
var input = document.querySelector("input[name=name]")
if (input.value.length) {
window.todos = [].concat(todos, { id: window.todos.length + 1, name: input.value, completed: false })
input.value = ""
updateTodos()
}
}
document.querySelector("#create").addEventListener("click", createTodo)
</script>
</html>
`;
export default {
async fetch(request, env, ctx) {
const defaultData = {
todos: [
{
id: 1,
name: "Finish the Cloudflare Workers blog post",
completed: false,
},
],
};
const setCache = (key, data) => env.TODOS.put(key, data);
const getCache = (key) => env.TODOS.get(key);
const ip = request.headers.get("CF-Connecting-IP");
const myKey = `data-${ip}`;
if (request.method === "PUT") {
const body = await request.text();
try {
JSON.parse(body);
await setCache(myKey, body);
return new Response(body, { status: 200 });
} catch (err) {
return new Response(err, { status: 500 });
}
}
let data;
const cache = await getCache();
if (!cache) {
await setCache(myKey, JSON.stringify(defaultData));
data = defaultData;
} else {
data = JSON.parse(cache);
}
const body = html(JSON.stringify(data.todos).replace(/</g, "\\u003c"));
return new Response(body, {
headers: {
"Content-Type": "text/html",
},
});
},
};Исходный код этого проекта, а также README с инструкциями по развёртыванию можно найти на GitHub ↗.