← Cloudflare Workers / workers / tutorials
Vytvořte aplikaci Jamstack se seznamem úkolů
V tomto tutoriálu sestavíte aplikaci to-do list pomocí HTML, CSS a JavaScriptu. Data aplikace budou uložena v Workers KV.
Než začnete s tímto projektem, měli byste mít určité zkušenosti s HTML, CSS a JavaScriptem. Naučíte se:
- Jak vám vývoj pomocí Workers umožňuje soustředit se na psaní kódu a dodávat hotové produkty.
- Jak přidání Workers KV dělá z tohoto tutoriálu skvělý úvod do vytváření kompletních, na datech založených aplikací.
Pokud si chcete prohlédnout hotový kód tohoto projektu, najdete ho projekt na GitHubu ↗ a přečtěte si živé demo ↗ a projděte si, co budete sestavovat.
Než začnete
Všechny návody předpokládají, že jste již dokončili Úvodní návod, který vás provede nastavením účtu Cloudflare Workers, C3 ↗, a Wrangler.
1. Vytvořte nový projekt Workers
Nejprve použijte create-cloudflare ↗ nástroj CLI k vytvoření nového projektu Cloudflare Workers s názvem todos. V tomto tutoriálu použijete výchozí Hello World šablonu k vytvoření projektu Workers.
npm create cloudflare@latest -- todosPři nastavení vyberte následující možnosti:
- Pro S čím byste chtěli začít?, vyberte
Hello World example. - Pro Jakou šablonu chcete použít?, vyberte
Worker only. - Pro Jaký jazyk chcete použít?, vyberte
JavaScript. - Pro Chcete používat git pro správu verzí?, vyberte
Yes. - Pro Chcete nasadit svou aplikaci?, vyberte
No(před nasazením provedeme ještě několik změn).
Přejděte do nově vytvořeného adresáře:
cd todosUvnitř svého nového todos adresáři projektu Workeru, index.js představuje vstupní bod vaší aplikace Cloudflare Workers.
Všechny příchozí HTTP požadavky na Worker jsou předány do fetch() handler jako požadavek objekt. Poté, co Worker přijme požadavek, se uživateli vrátí odpověď sestavená vaší aplikací. Tento návod vás provede fungováním vzoru request/response a tím, jak jej využít k vytváření plnohodnotných aplikací.
export default {
async fetch(request, env, ctx) {
return new Response("Hello World!");
},
};Ve výchozí index.js soubor, můžete vidět tento vzor požadavku a odpovědi v praxi. Soubor fetch vytvoří nový Response s textem těla 'Hello World!'.
Když Worker přijme request, Worker vrátí klientovi nově vytvořenou odpověď. Váš Worker bude nové odpovědi obsluhovat přímo z Globální síť Cloudflare ↗ místo pokračování na váš origin server. Standardní server by požadavky přijal a vrátil odpovědi. Cloudflare Workers umožňuje odpovídat sestavením odpovědí přímo v globální síti Cloudflare.
2. Zkontrolujte podrobnosti projektu
Jakýkoli projekt, který nasadíte do Cloudflare Workers, může využívat moderní nástroje pro JavaScript, jako je ES moduly, npm balíčky a async/await ↗ funkce k vytvoření vaší aplikace. Kromě psaní Workers můžete Workers použít k vytvářet kompletní aplikace pomocí stejných nástrojů a postupu jako v tomto tutoriálu.
V tomto tutoriálu sestavíte aplikaci to-do list běžící na Workers, která umožňuje číst data z KV uložiště a použití těchto dat k naplnění HTML odpovědi odeslané klientovi.
Práce potřebná k vytvoření této aplikace je rozdělena do tří úkolů:
- Zapisujte data do KV.
- Vykreslování dat z KV.
- Přidávání úkolů z uživatelského rozhraní aplikace.
Ve zbývající části tohoto tutoriálu postupně dokončíte jednotlivé úkoly, budete svou aplikaci dále vylepšovat a nakonec ji publikujete na vlastní doméně.
3. Zapište data do KV
Na začátek potřebujete pochopit, jak naplnit seznam úkolů skutečnými daty. K tomu použijte Cloudflare Workers KV : úložiště klíč-hodnota, ke kterému máte ve svém Workeru přístup pro čtení a zápis dat.
Chcete-li začít s KV, nastavte namespace. Všechna vaše cache data se budou ukládat do tohoto namespace a po nakonfigurování k němu budete moci přistupovat uvnitř Workeru pomocí předdefinované proměnné. Pomocí Wrangler vytvořte nový namespace s názvem TODOS hodnotou kv namespace create příkaz a získejte přidružené ID oboru názvů spuštěním následujícího příkazu v terminálu:
npx wrangler kv namespace create "TODOS" --previewPřidružený jmenný prostor lze zkombinovat s --preview příznak pro práci s přípravným jmenným prostorem místo produkčního. Jmenné prostory můžete do aplikace přidat jejich definováním v konfiguraci Wrangler. Zkopírujte ID nově vytvořeného jmenného prostoru a ve svém Konfigurační soubor Wrangler, definujte kv_namespaces klíč pro nastavení vašeho namespace:
{
"kv_namespaces": [
{
"binding": "TODOS",
"id": "<YOUR_ID>",
"preview_id": "<YOUR_PREVIEW_ID>"
}
]
}[[kv_namespaces]]
binding = "TODOS"
id = "<YOUR_ID>"
preview_id = "<YOUR_PREVIEW_ID>"Definovaný namespace, TODOS, nyní bude k dispozici ve vaší codebase. Nyní je čas porozumět KV API. KV namespace nabízí tři hlavní metody, kterými můžete pracovat s cache: get, put, a delete.
Začněte ukládat data definováním počáteční sady dat, kterou vložíte do mezipaměti pomocí put metoda. Následující příklad definuje defaultData objekt namísto pole položek todo. Později možná budete chtít do tohoto objektu mezipaměti ukládat metadata a další informace. S ohledem na tento datový objekt použijte JSON.stringify pro přidání řetězce do mezipaměti:
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 je eventuálně konzistentní globální úložiště dat. Zápisy v rámci jednoho regionu se v tomto regionu projeví okamžitě, v ostatních regionech ale hned dostupné nebudou. Nakonec však budou dostupné všude, a v tu chvíli Workers KV zaručuje, že data budou v každém regionu konzistentní.
Vzhledem k tomu, že data jsou v mezipaměti přítomna a že mezipaměť je nakonec konzistentní, je potřeba tento kód mírně upravit: aplikace by měla zkontrolovat mezipaměť a použít její hodnotu, pokud daný klíč existuje. Pokud neexistuje, použijete defaultData jako zdroj dat (v budoucnu by se mělo nastavit) a zapíše ji do cache pro budoucí použití. Po rozdělení kódu do několika funkcí kvůli přehlednosti vypadá výsledek takto:
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));
},
};Vykreslit data z KV
Vzhledem k tomu, že váš kód obsahuje data, tedy objekt s daty uloženými v mezipaměti vaší aplikace, měli byste tato data převzít a vykreslit v uživatelském rozhraní.
K tomu vytvořte nový html proměnnou ve vašem Workers skriptu a použijte ji k sestavení statické šablony HTML, kterou můžete poskytovat klientovi. V fetch, vytvořte nový Response s Content-Type: text/html hlavičku a doručit ji klientovi:
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'
}
});
}Máte vykreslovaný statický HTML web a můžete jej začít plnit daty. Do body přidejte div tag s id z 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>
`;Přidejte <script> element na konec obsahu body, který přijímá todos pole. Pro každý todo v poli vytvořte div element a připojí ho k todos HTML element:
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>
`;Vaše statická stránka může přijímat window.todos a na jejich základě vykreslit HTML, ale zatím jste nepředali žádná data z KV. K tomu budete muset provést několik změn.
Nejprve váš html proměnná se změní na funkci. Tato funkce bude přijímat todos argument, který naplní window.todos proměnná ve výše uvedené ukázce kódu:
const html = (todos) => `
<!doctype html>
<html>
<!-- existing content -->
<script>
window.todos = ${todos}
var todoContainer = document.querySelector("#todos")
// ...
<script>
</html>
`;V fetch, použijte získaná data KV k volání html funkci a vygenerovat Response na jeho základě:
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. Přidejte úkoly z uživatelského rozhraní (UI)
V tomto okamžiku už máte vytvořený Cloudflare Worker, který načítá data z Cloudflare KV a na jejich základě vykresluje statickou stránku. Tato statická stránka data čte a na jejich základě vytváří seznam úkolů. Zbývá umožnit vytváření úkolů přímo z rozhraní aplikace. Úkoly můžete přidávat pomocí KV API: mezipaměť aktualizujete spuštěním env.TODOS.put(newData).
Chcete-li aktualizovat položku úkolu, přidáte do skriptu Workers druhý handler, který bude sledovat PUT požadavky na /. Jakmile na tuto URL dorazí tělo požadavku, Worker odešle nová data úkolu do vašeho KV store.
Přidejte tuto novou funkcionalitu do fetch: pokud je metoda požadavku PUT, převezme tělo požadavku a aktualizuje mezipaměť.
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
},
};Zkontrolujte, že se jedná o požadavek typu PUT a zbytek kódu zabalte do try...catch blok. Nejprve zpracujte tělo příchozího požadavku a ověřte, že jde o JSON, než aktualizujete cache novými daty a vrátíte je uživateli. Pokud se něco pokazí, vraťte 500 stavovým kódem. Pokud je trasa zasažena jinou HTTP metodou než PUT , například POST nebo DELETE, return a 404 chyba.
S tímto skriptem teď můžete do své HTML stránky přidat dynamickou funkčnost, která tuto route skutečně zavolá. Nejprve vytvořte vstupní pole pro název úkolu a tlačítko pro jeho odeslání.
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>
`;S tímto vstupem a tlačítkem přidejte odpovídající funkci JavaScript, která bude sledovat kliknutí na tlačítko: jakmile uživatel na tlačítko klikne, prohlížeč PUT na / a odešlete úkol (todo).
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>
`;Tento kód aktualizuje mezipaměť. Pamatujte, že mezipaměť KV je pouze eventuálně konzistentní: i kdybyste Worker upravili tak, aby z mezipaměti četl a vracel data, nemáte žádnou záruku, že budou skutečně aktuální. Místo toho aktualizujte seznam úkolů lokálně tak, že původní kód pro vykreslení seznamu úkolů převedete na znovupoužitelnou funkci nazvanou populateTodos, a jeho volání při načtení stránky a po dokončení požadavku na cache:
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>
`;Jakmile máte klientský kód na místě, nasazení nové verze funkce by mělo vše propojit dohromady. Výsledkem je skutečný dynamický seznam úkolů.
5. Aktualizujte úkoly z uživatelského rozhraní aplikace
Poslední částí vašeho seznamu úkolů je schopnost aktualizovat úkoly, konkrétně možnost označit je jako dokončené.
Naštěstí je velká část potřebné infrastruktury už připravena. Data seznamu úkolů můžete aktualizovat přímo v cache, jak dokládá váš createTodo funkci. Provádění aktualizací úkolu je spíše úkolem na straně klienta než na straně Workeru.
Nejprve populateTodos funkci lze upravit tak, aby generovala div pro každou položku todo. Kromě toho přesuňte název položky todo do podřízeného prvku tohoto 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>
`;Klientskou část tohoto kódu jste navrhli tak, aby zpracovávala pole úkolů (todos) a vykreslovala seznam HTML elementů. Dělali jste přitom několik věcí, pro které jste zatím nenašli přesné využití, konkrétně zahrnutí ID a aktualizaci stavu dokončení úkolu. Tyto věci spolu dobře fungují a ve výsledku umožňují aktualizaci úkolů v uživatelském rozhraní aplikace.
Nejprve bude užitečné připojit ID každého úkolu (todo) do HTML. Díky tomu pak budete moci na daný prvek později odkázat a přiřadit ho k úkolu v části kódu psané v JavaScriptu. Datové atributy a odpovídající dataset metody v JavaScriptu jsou ideálním způsobem, jak toho dosáhnout. Když vytváříte svůj div element pro každý úkol můžete ke každému 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>
`;Ve svém HTML každý div pro úkol nyní obsahuje připojený datový atribut, který vypadá takto:
<div data-todo="1"></div>
<div data-todo="2"></div>Nyní můžete pro každý prvek todo vygenerovat zaškrtávací pole. U nových úkolů bude toto pole ve výchozím stavu nezaškrtnuté, ale při vykreslení prvku v okně jej můžete označit jako zaškrtnuté:
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>
`;Zaškrtávací políčko je nastaveno tak, aby správně odráželo hodnotu completed u každého úkolu, ale zatím se neaktualizuje, když políčko skutečně zaškrtnete. Abyste toho dosáhli, připojte completeTodo funkci jako posluchače události na click událost. Uvnitř funkce zkontrolujte prvek zaškrtávacího políčka, najděte jeho rodiče (položku todo div), a použít jeho todo atribut data k nalezení odpovídajícího úkolu v poli dat. Můžete přepnout stav dokončení, aktualizovat jeho vlastnosti a znovu vykreslit uživatelské rozhraní:
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>
`;Výsledkem vašeho kódu je systém, který kontroluje todos proměnnou, aktualizuje vaši Cloudflare KV cache touto hodnotou a poté provede překreslení UI na základě dat, která má lokálně k dispozici.
6. Závěr a další kroky
Dokončením tohoto tutoriálu jste vytvořili statickou aplikaci HTML, CSS a JavaScript, kterou transparentně pohánějí Workers a Workers KV a která plně využívá globální síť Cloudflare.
Pokud chcete na projektu dál pracovat, můžete implementovat lepší design (živou verzi najdete na todos.signalnerve.workers.dev ↗), nebo provést další vylepšení zabezpečení a rychlosti.
Můžete také zvážit přidání cachování specifického pro jednotlivé uživatele. Klíč cache je v tuto chvíli vždy data : to znamená, že každý návštěvník webu bude sdílet stejný seznam úkolů s ostatními návštěvníky. Ve svém Workeru můžete použít hodnoty z klientského požadavku k vytvoření a správě seznamů pro jednotlivé uživatele. Můžete například vygenerovat klíč mezipaměti na základě IP adresy požadavku:
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",
},
});
},
};Po provedení těchto změn a opětovném nasazení Workeru bude vaše aplikace se seznamem úkolů obsahovat funkce pro jednotlivé uživatele a přitom bude nadále plně využívat globální síť Cloudflare.
Finální verze vašeho skriptu Workeru by měla vypadat takto:
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",
},
});
},
};Zdrojový kód tohoto projektu i soubor README s pokyny k nasazení najdete v na GitHubu ↗.