Eine meiner Lieblingsentwicklungen in der Softwareentwicklung war das Aufkommen von Serverless. Als Entwickler, der dazu neigt, sich in den Details von Deployment und DevOps zu verheddern, ist es erfrischend, eine Möglichkeit zu haben, Webanwendungen zu erstellen, die Skalierung und Infrastruktur einfach von mir abstrahieren. Serverless hat mich besser darin gemacht, Projekte tatsächlich auszuliefern!
Davon abgesehen, dass es erfrischend ist, mag es für Neulinge im Bereich Serverless unklar sein, wie die Dinge, die man bereits kennt, in ein neues Paradigma übersetzt werden können. Wenn Sie ein Front-End-Entwickler sind, haben Sie vielleicht keine Erfahrung mit dem, was Serverless von Ihnen abstrahiert – wie fängt man also überhaupt an?
Heute werde ich versuchen, den praktischen Teil der Arbeit mit Serverless zu entmystifizieren, indem ich ein Projekt von der Idee bis zur Produktion nehme und dabei Cloudflare Workers verwende. Unser Projekt wird eine tägliche Rangliste namens „Repo Hunt“ sein, inspiriert von Seiten wie Product Hunt und Reddit, auf der Benutzer coole Open-Source-Projekte von GitHub und GitLab einreichen und hochwählen können. Die endgültige Version der Seite, veröffentlicht hier, können Sie hier sehen.

Workers ist eine Serverless-Anwendungsplattform, die auf Cloudflares Netzwerk aufbaut. Wenn Sie ein Projekt auf Cloudflare Workers veröffentlichen, wird es sofort über 180 (und wachsende) Städte auf der ganzen Welt verteilt. Das bedeutet, dass Ihre Workers-Anwendung unabhängig davon, wo sich Ihre Benutzer befinden, von einem nahegelegenen Cloudflare-Server mit extrem niedriger Latenz bedient wird. Darüber hinaus hat sich das Workers-Team voll und ganz der Entwicklererfahrung verschrieben: Unsere neueste Version hat Anfang dieses Monats ein voll funktionsfähiges Befehlszeilentool namens Wrangler eingeführt, das das Erstellen, Hochladen und Veröffentlichen Ihrer Serverless-Anwendungen mit wenigen einfach zu erlernenden und leistungsstarken Befehlen verwaltet.
Das Endergebnis ist eine Plattform, die es Ihnen ermöglicht, einfach JavaScript zu schreiben und es an eine URL zu deployen – keine Sorgen mehr darüber, was „Docker“ bedeutet oder ob Ihre Anwendung abstürzt, wenn sie es auf die Titelseite von Hacker News schafft!
Wenn Sie zu den Leuten gehören, die das Projekt lieber zuerst sehen möchten, bevor Sie sich in ein langes Tutorial stürzen, haben Sie Glück! Der Quellcode für dieses Projekt ist auf GitHub verfügbar. Damit wollen wir nun in die Kommandozeile springen und etwas Großartiges bauen.
Wrangler installieren und unseren Arbeitsbereich vorbereiten
Wrangler ist das Befehlszeilentool zum Generieren, Erstellen und Veröffentlichen von Cloudflare Workers-Projekten. Wir haben die Installation extrem einfach gemacht, besonders wenn Sie bereits mit npm gearbeitet haben.
npm install -g @cloudflare/wrangler
Sobald Sie Wrangler installiert haben, können Sie den Befehl generate verwenden, um ein neues Projekt zu erstellen. Wrangler-Projekte verwenden „Vorlagen“, die wiederverwendbare Code-Repositories sind, die von Entwicklern, die mit Workers arbeiten, erstellt wurden. Wir pflegen eine wachsende Liste von Vorlagen, die Ihnen helfen, alle Arten von Projekten in Workers zu erstellen: Schauen Sie sich unsere Vorlagengalerie an, um loszulegen!
In diesem Tutorial verwenden wir die Vorlage „Router“, die es Ihnen ermöglicht, URL-basierte Projekte auf Basis von Workers zu erstellen. Der Befehl generate nimmt zwei Argumente entgegen: zuerst den Namen Ihres Projekts (ich verwende repo-hunt) und eine Git-URL. Das ist mein Lieblingsteil des generate-Befehls: Sie können alle Arten von Vorlagen verwenden, indem Sie Wrangler auf eine GitHub-URL verweisen, sodass das Teilen, Forken und Kollaborieren an Vorlagen extrem einfach ist. Lassen Sie uns jetzt den Befehl generate ausführen.
wrangler generate repo-hunt https://github.com/cloudflare/worker-template-router
cd repo-hunt
Die Router-Vorlage unterstützt die Erstellung von Projekten mit webpack, sodass Sie npm-Module zu Ihrem Projekt hinzufügen und alle bekannten JavaScript-Tools verwenden können. Darüber hinaus enthält die Vorlage, wie Sie vielleicht erwarten, eine Router-Klasse, die es Ihnen ermöglicht, Routen in Ihrem Worker zu verarbeiten und sie einer Funktion zuzuordnen. Schauen wir uns ein einfaches Beispiel an: Instanziieren eines Router, Verarbeiten einer GET-Anfrage an / und Zurückgeben einer Antwort an den Client.
// index.js
const Router = require('./router')
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
try {
const r = new Router()
r.get('/', () => new Response("Hello, world!"))
const resp = await r.route(request)
return resp
} catch (err) {
return new Response(err)
}
}
Alle Workers-Anwendungen beginnen damit, auf das fetch-Ereignis zu hören, das eine eingehende Anfrage von einem Client an Ihre Anwendung ist. Innerhalb dieses Ereignis-Listeners ist es gängige Praxis, eine Funktion handleRequest aufzurufen, die die eingehende Anfrage prüft und bestimmt, wie darauf reagiert werden soll. Bei der Verarbeitung eines eingehenden fetch-Ereignisses, das eine eingehende Anfrage signalisiert, sollte ein Workers-Skript immer eine Response an den Benutzer zurückgeben: Dies ist ein ähnliches Anfrage/Antwort-Muster wie bei vielen Web-Frameworks, wie z. B. Express. Wenn Sie also bereits mit Web-Frameworks gearbeitet haben, sollte sich das vertraut anfühlen!
In unserem Beispiel werden wir einige Routen nutzen: eine „Root“-Route (/), die die Homepage unserer Seite rendert; ein Formular zum Einreichen neuer Repos unter /post und eine spezielle Route zur Annahme von POST-Anfragen, wenn ein Benutzer ein Repo über das Formular einreicht, unter /repo.
Eine Route erstellen und eine Vorlage rendern
Die erste Route, die wir einrichten werden, ist die „Root“-Route unter dem Pfad /. Hier werden die von der Community eingereichten Repos gerendert. Vorerst üben wir uns darin, eine Route zu definieren und einfachen HTML zurückzugeben. Dieses Muster ist in Workers-Anwendungen ausreichend verbreitet, dass es sinnvoll ist, es zuerst zu verstehen, bevor wir zu einigen interessanteren Dingen übergehen!
Zuerst aktualisieren wir index.js, um eine Instanz eines Router einzurichten, GET-Anfragen an / zu verarbeiten und die Funktion index aus handlers/index.js aufzurufen (mehr dazu gleich).
// index.js
const Router = require('./router')
const index = require('./handlers/index')
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
function handleRequest(request) {
try {
const r = new Router()
r.get('/', index)
return r.route(request)
} catch (err) {
return new Response(err)
}
}
Wie im Beispiel index.js im vorherigen Abschnitt hört unser Code auf ein fetch-Ereignis und antwortet durch den Aufruf der Funktion handleRequest. Die Funktion handleRequest richtet eine Instanz eines Router ein, der die Funktion index bei jeder GET-Anfrage an / aufruft. Nach der Einrichtung des Routers leiten wir die eingehende Anfrage mithilfe von r.route weiter und geben sie als Antwort an den Client zurück. Wenn etwas schiefgeht, umschließen wir einfach den Inhalt der Funktion in einem try/catch-Block und geben den err an den Client zurück (ein Hinweis: in Produktionsanwendungen möchten Sie hier vielleicht etwas Robusteres haben, wie z. B. Logging an ein Tool zur Ausnahmemonitoring).
Um mit der Einrichtung unseres Routenhandlers fortzufahren, erstellen wir eine neue Datei, handlers/index.js, die die eingehende Anfrage entgegennimmt und eine HTML-Antwort an den Client zurückgibt.
// handlers/index.js
const headers = { 'Content-Type': 'text/html' }
const handler = () => {
return new Response("Hello, world!", { headers })
}
module.exports = handler
Unsere handler-Funktion ist einfach: Sie gibt eine neue Instanz von Response mit dem Text „Hello, world!“ sowie ein headers-Objekt zurück, das den Content-Type-Header auf text/html setzt – dies weist den Browser an, die eingehende Antwort als HTML-Dokument zu rendern. Das bedeutet, wenn ein Client eine GET-Anfrage an die Route / sendet, wird eine neue HTML-Antwort mit dem Text „Hello, world!“ konstruiert und an den Benutzer zurückgegeben.
Wrangler verfügt über eine preview-Funktion, die sich perfekt zum Testen der HTML-Ausgabe unserer neuen Funktion eignet. Lassen Sie uns diese jetzt ausführen, um sicherzustellen, dass unsere Anwendung wie erwartet funktioniert.
wrangler preview
Der Befehl preview sollte einen neuen Tab in Ihrem Browser öffnen, nachdem Ihre Workers-Anwendung erstellt und auf unsere Test-Spielwiese hochgeladen wurde. Im Vorschaufenster sollten Sie Ihre gerenderte HTML-Antwort sehen.

Nachdem unsere HTML-Antwort im Browser angezeigt wird, wollen wir unsere handler-Funktion etwas spannender gestalten, indem wir schönes HTML zurückgeben. Dazu richten wir eine entsprechende index-„Vorlage“ für unseren Routenhandler ein: Wenn eine Anfrage beim index-Handler eingeht, ruft er die Vorlage auf und gibt einen HTML-String zurück, um dem Client eine ordnungsgemäße Benutzeroberfläche als Antwort zu bieten. Beginnen wir damit, handlers/index.js zu aktualisieren, um eine Antwort mit unserer Vorlage zurückzugeben (und zusätzlich einen try/catch-Block einzurichten, um Fehler abzufangen und sie als Antwort zurückzugeben).
// handlers/index.js
const headers = { 'Content-Type': 'text/html' }
const template = require('../templates/index')
const handler = async () => {
try {
return new Response(template(), { headers })
} catch (err) {
return new Response(err)
}
}
module.exports = handler
Wie Sie sich vorstellen können, müssen wir eine entsprechende Vorlage einrichten! Wir erstellen eine neue Datei, templates/index.js, und geben einen HTML-String zurück, indem wir ES6-Vorlagen-Strings verwenden.
// templates/index.js
const template = () => {
return <code><h1>Hello, world!</h1>`
}
module.exports = template
Unsere template-Funktion gibt einen einfachen HTML-String zurück, der in den Body unserer Response in handlers/index.js gesetzt wird. Als letztes Code-Snippet für unser Templating für unsere erste Route wollen wir etwas Interessanteres machen: Erstellen einer Datei templates/layout.js, die das Basis-„Layout“ sein wird, in das alle unsere Vorlagen gerendert werden. Dies ermöglicht uns, einige konsistente Stile und Formatierungen für alle Vorlagen festzulegen. In templates/layout.js:
// templates/layout.js
const layout = body => `
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Repo Hunt</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
</head>
<body>
<div class="container">
<div class="navbar">
<div class="navbar-brand">
Repo Hunt
</div>
<div class="navbar-menu">
<div class="navbar-end">
<div class="navbar-item">
Post a repository
</div>
</div>
</div>
</div>
<div class="section">
${body}
</div>
</div>
</body>
</html>
`
module.exports = layout
Dies ist ein großer HTML-Codeblock, aber wenn wir ihn aufschlüsseln, gibt es nur wenige wichtige Dinge zu beachten: Erstens ist diese layout-Variable eine Funktion! Eine body-Variable wird übergeben, die dazu bestimmt ist, in einem div mitten im HTML-Snippet enthalten zu sein. Darüber hinaus schließen wir das Bulmahttps://bulma.io) CSS-Frameworkhttps://bulma.io) für etwas einfache Formatierung in unserem Projekt und eine Navigationsleiste ein, um den Benutzern mitzuteilen, *was* diese Seite ist, mit einem Link zum Einreichen neuer Repositories.
Um unsere layout-Vorlage zu verwenden, importieren wir sie in templates/index.js und umschließen unseren HTML-String damit.
// templates/index.js
const layout = require('./layout')
const template = () => {
return layout(`<h1>Hello, world!</h1>`)
}
module.exports = template
Damit können wir wrangler preview erneut ausführen, um unsere schön gerenderte HTML-Seite mit etwas Hilfe von Bulma zu sehen.

Daten mit Workers KV speichern und abrufen
Die meisten Webanwendungen sind ohne eine gewisse Datenpersistenz nicht sehr nützlich. Workers KV ist ein Key-Value-Speicher, der für die Verwendung mit Workers entwickelt wurde – stellen Sie ihn sich als ein extrem schnelles und global verteiltes Redis vor. In unserer Anwendung werden wir KV verwenden, um alle Daten für unsere Anwendung zu speichern: Jedes Mal, wenn ein Benutzer ein neues Repository einreicht, wird es in KV gespeichert, und wir generieren auch ein tägliches Array von Repositories, die auf der Homepage gerendert werden sollen.
Ein kurzer Hinweis: zum Zeitpunkt des Schreibens erfordert die Nutzung von Workers KV einen kostenpflichtigen Workers-Plan. Lesen Sie mehr im Abschnitt „Preise“ in der Workers-Dokumentation hier.
Innerhalb einer Workers-Anwendung können Sie auf einen vordefinierten KV-Namespace verweisen, den wir in der Cloudflare-Benutzeroberfläche erstellen und unserer Anwendung zuordnen, sobald sie auf der Workers-Anwendung bereitgestellt wurde. In diesem Tutorial verwenden wir einen KV-Namespace namens REPO_HUNT und stellen im Rahmen des Deployment-Prozesses sicher, dass wir ihn unserer Anwendung zuordnen, damit alle Verweise im Code auf REPO_HUNT korrekt auf den KV-Namespace aufgelöst werden.
Bevor wir mit der Erstellung von Daten in unserem Namespace beginnen, werfen wir einen Blick auf die Grundlagen der Arbeit mit KV in Ihrer Workers-Anwendung. Angenommen, Sie haben einen Namespace (z. B. REPO_HUNT), können wir einen Schlüssel mit einem gegebenen Wert mithilfe von put setzen.
const string = "Hello, world!"
REPO_HUNT.put("myString", string)
Wir können den Wert für diesen Schlüssel auch abrufen, indem wir async/await verwenden und darauf warten, dass das Promise aufgelöst wird.
const getString = async () => {
const string = await REPO_HUNT.get("myString")
console.log(string) // "Hello, world!"
}
Die API ist extrem einfach, was großartig für Webentwickler ist, die Anwendungen mit der Workers-Plattform erstellen möchten, ohne sich mit relationalen Datenbanken oder irgendeiner Art von externem Datenservice beschäftigen zu müssen. In unserem Fall speichern wir die Daten für unsere Anwendung, indem wir
- Ein Repo-Objekt, das unter dem Schlüssel
repos:$idgespeichert wird, wobei$ideine generierte UUID für ein neu eingereichtes Repo ist. - Ein Tag-Array, das unter dem Schlüssel
$date(z. B.„6/24/2019“) gespeichert wird und eine Liste von Repo-IDs enthält, die die eingereichten Repos für diesen Tag anzeigen.
Wir beginnen mit der Implementierung der Unterstützung für das Einreichen von Repositories und nehmen unsere ersten Schreibvorgänge in unseren KV-Namespace vor, indem wir die Repository-Daten in dem oben angegebenen Objekt speichern. Auf dem Weg dorthin erstellen wir eine einfache JavaScript-Klasse für die Schnittstelle zu unserem Speicher – wir werden diese Klasse wiederverwenden, wenn wir mit dem Rendern der Homepage fortfahren, wo wir die Repository-Daten abrufen, eine Benutzeroberfläche erstellen und unsere Beispielanwendung fertigstellen.
Benutzereingaben zulassen
Unabhängig von der Anwendung müssen Webentwickler immer Formulare schreiben. In unserem Fall erstellen wir ein einfaches Formular, damit Benutzer Repositories einreichen können.
Zu Beginn dieses Tutorials haben wir index.js so eingerichtet, dass es eingehende GET-Anfragen an die Root-Route (`/`) verarbeitet. Um Benutzern das Hinzufügen neuer Repositories zu ermöglichen, fügen wir eine weitere Route hinzu, GET /post, die Benutzern eine Formularvorlage rendert. In index.js:
// index.js
// ...
const post = require('./handlers/post')
// ...
function handleRequest(request) {
try {
const r = new Router()
r.get('/', index)
r.get('/post', post)
return r.route(request)
} catch (err) {
return new Response(err)
}
}
Zusätzlich zu einem neuen Routenhandler in index.js fügen wir auch handlers/post.js hinzu, einen neuen Funktionshandler, der eine zugehörige Vorlage als HTML-Antwort für den Benutzer rendert.
// handlers/post.js
const headers = { 'Content-Type': 'text/html' }
const template = require('../templates/post')
const handler = request => {
try {
return new Response(template(), { headers })
} catch (err) {
return new Response(err)
}
}
module.exports = handler
Das letzte Puzzleteil ist die HTML-Vorlage selbst – wie unser vorheriges Vorlagenbeispiel werden wir die erstellte layout-Vorlage wiederverwenden und ein einfaches Formular mit drei Feldern darum herumwickeln, und den HTML-String aus templates/post.js exportieren.
// templates/post.js
const layout = require('./layout')
const template = () =>
layout(`
<div>
<h1>Post a new repo</h1>
<form action="/repo" method="post">
<div class="field">
<label class="label" for="name">Name</label>
<input class="input" id="name" name="name" type="text" placeholder="Name" required></input>
</div>
<div class="field">
<label class="label" for="description">Description</label>
<input class="input" id="description" name="description" type="text" placeholder="Description"></input>
</div>
<div class="field">
<label class="label" for="url">URL</label>
<input class="input" id="url" name="url" type="text" placeholder="URL" required></input>
</div>
<div class="field">
<div class="control">
<button class="button is-link" type="submit">Submit</button>
</div>
</div>
</form>
</div>
<code>)
module.exports = template
Mit wrangler preview können wir zum Pfad /post navigieren und unser gerendertes Formular sehen.

Wenn Sie sich die Definition des tatsächlichen form-Tags in unserer Vorlage ansehen, werden Sie feststellen, dass wir eine POST-Anfrage an den Pfad /repo senden. Um die Formulardaten zu empfangen und sie in unserem KV-Speicher zu speichern, werden wir einen weiteren Handler hinzufügen. In index.js:
// index.js
// ...
const create = require('./handlers/create')
// ...
function handleRequest(request) {
try {
const r = new Router()
r.get('/', index)
r.get('/post', post)
r.post('/repo', create)
return r.route(request)
} catch (err) {
return new Response(err)
}
}
Wenn ein Formular an einen Endpunkt gesendet wird, wird es als Query String gesendet. Um uns das Leben zu erleichtern, werden wir die Bibliothek qs in unser Projekt aufnehmen, mit der wir einfach den eingehenden Query String als JS-Objekt parsen können. In der Kommandozeile fügen wir qs einfach durch die Verwendung von npm hinzu. Während wir dabei sind, installieren wir auch das Paket node-uuid, das wir später verwenden werden, um IDs für neue eingehende Daten zu generieren. Um beide zu installieren, verwenden Sie den install --save-Unterbefehl von npm.
npm install --save qs uuid
Damit können wir den entsprechenden Handler für POST /repo implementieren. In handlers/create.js:
// handlers/create.js
const qs = require('qs')
const handler = async request => {
try {
const body = await request.text()
if (!body) {
throw new Error('Incorrect data')
}
const data = qs.parse(body)
// TODOs:
// - Create repo
// - Save repo
// - Add to today's repos on the homepage
return new Response('ok', { headers: { Location: '/' }, status: 301 })
} catch (err) {
return new Response(err, { status: 400 })
}
}
module.exports = handler
Unsere Handler-Funktion ist ziemlich unkompliziert – sie ruft text auf dem request auf, wartet auf die Auflösung des Promises, um unseren Query String body zu erhalten. Wenn kein Body-Element mit der Anfrage bereitgestellt wird, löst der Handler einen Fehler aus (der dank unseres try/catch-Blocks mit dem Statuscode 400 zurückgegeben wird). Bei einem gültigen Body rufen wir parse auf dem importierten qs-Paket auf und erhalten einige Daten zurück. Vorerst haben wir unsere Absichten für den Rest dieses Codes angedeutet: Zuerst erstellen wir ein Repo basierend auf den Daten. Wir speichern dieses Repo und fügen es dann dem Array der heutigen Repos hinzu, die auf der Homepage gerendert werden sollen.
Um unsere Repo-Daten in KV zu schreiben, erstellen wir zwei einfache ES6-Klassen, um eine leichte Validierung durchzuführen und einige Persistenzmethoden für unsere Datentypen zu definieren. Obwohl Sie REPO_HUNT.put direkt aufrufen könnten, ist es bei der Arbeit mit großen Mengen ähnlicher Daten schön, so etwas wie new Repo(data).save() zu tun – tatsächlich implementieren wir etwas fast genau so, damit die Arbeit mit einem Repo unglaublich einfach und konsistent ist.
Definieren wir store/repo.js, das eine Repo-Klasse enthält. Mit dieser Klasse können wir neue Repo-Objekte instanziieren und mithilfe der constructor-Methode Daten übergeben und diese validieren, bevor wir sie weiter in unserem Code verwenden.
// store/repo.js
const uuid = require('uuid/v4')
class Repo {
constructor({ id, description, name, submitted_at, url }) {
this.id = id || uuid()
this.description = description
if (!name) {
throw new Error(`Missing name in data`)
} else {
this.name = name
}
this.submitted_at = submitted_at || Number(new Date())
try {
const urlObj = new URL(url)
const whitelist = ['github.com', 'gitlab.com']
if (!whitelist.some(valid => valid === urlObj.host)) {
throw new Error('The URL provided is not a repository')
}
} catch (err) {
throw new Error('The URL provided is not valid')
}
this.url = url
}
save() {
return REPO_HUNT.put(`repos:${this.id}`, JSON.stringify(this))
}
}
module.exports = Repo
Selbst wenn Sie mit der constructor-Funktion in einer ES6-Klasse nicht sehr vertraut sind, sollte dieses Beispiel immer noch ziemlich einfach zu verstehen sein. Wenn wir eine neue Instanz eines Repo erstellen möchten, übergeben wir die relevanten Daten als Objekt an constructor und verwenden die Destrukturierungszuweisung von ES6, um jeden Wert in einen eigenen Schlüssel zu extrahieren. Mit diesen Variablen gehen wir jeden davon durch und weisen this.$key (z. B. this.name, this.description usw.) dem übergebenen Wert zu.
Viele dieser Werte haben einen „Standardwert“: Wenn beispielsweise keine ID an den Konstruktor übergeben wird, generieren wir eine neue, indem wir die v4-Variante unseres zuvor gespeicherten uuid-Pakets verwenden, um eine neue UUID mithilfe von uuid() zu generieren. Für submitted_at generieren wir eine neue Instanz von Date und konvertieren sie in einen Unix-Timestamp, und für url stellen wir sicher, dass die URL sowohl gültig ist *als auch* von github.com oder gitlab.com stammt, um sicherzustellen, dass Benutzer echte Repos einreichen.
Damit fügt die Funktion save, die auf einer Instanz von Repo aufgerufen werden kann, eine JSON-stringifizierte Version der Repo-Instanz in KV ein und setzt den Schlüssel als repos:$id. Zurück in handlers/create.js importieren wir die Repo-Klasse und speichern ein neues Repo unter Verwendung unserer zuvor geparsten data.
// handlers/create.js
// ...
const Repo = require('../store/repo')
const handler = async request => {
try {
// ...
const data = qs.parse(body)
const repo = new Repo(data)
await repo.save()
// ...
} catch (err) {
return new Response(err, { status: 400 })
}
}
// ...
Damit sollte ein neues Repo, das auf eingehenden Formulardaten basiert, tatsächlich in Workers KV gespeichert werden! Während das Repo gespeichert wird, möchten wir auch ein weiteres Datenmodell einrichten, Day, das eine einfache Liste der Repositories enthält, die von Benutzern für ein bestimmtes Datum eingereicht wurden. Erstellen wir eine weitere Datei, store/day.js, und füllen sie aus.
// store/day.js
const today = () => new Date().toLocaleDateString()
const todayData = async () => {
const date = today()
const persisted = await REPO_HUNT.get(date)
return persisted ? JSON.parse(persisted) : []
}
module.exports = {
add: async function(id) {
const date = today()
let ids = await todayData()
ids = ids.concat(id)
return REPO_HUNT.put(date, JSON.stringify(ids))
}
}
Beachten Sie, dass der Code dafür keine Klasse ist – es ist ein Objekt mit Schlüssel-Wert-Paaren, wobei die Werte Funktionen sind! Wir werden hier bald mehr hinzufügen, aber die einzige Funktion, die wir definiert haben, add, lädt alle vorhandenen Repos von heute (unter Verwendung der Funktion today, um einen Datumsstring zu generieren, der als Schlüssel in KV verwendet wird) und fügt ein neues Repo hinzu, basierend auf der ID, die der Funktion übergeben wird. Zurück in handlers/create.js stellen wir sicher, dass wir diese neue Funktion importieren und aufrufen, damit alle neuen Repos sofort zur Liste der heutigen Repos hinzugefügt werden.
// handlers/create.js
// ...
const Day = require('../store/day')
// ...
const handler = async request => {
try {
// ...
await repo.save()
await Day.add(repo.id)
return new Response('ok', { headers: { Location: '/' }, status: 301 })
} catch (err) {
return new Response(err, { status: 400 })
}
}
// ...
Unsere Repo-Daten werden nun in KV gespeichert *und* sie werden einer Liste der von Benutzern für das heutige Datum eingereichten Repos hinzugefügt. Lassen Sie uns zum letzten Teil unseres Tutorials übergehen, um diese Daten zu nehmen und sie auf der Homepage zu rendern.
Daten rendern
An diesem Punkt haben wir das Rendern von HTML-Seiten in einer Workers-Anwendung implementiert und auch eingehende Daten übernommen und sie in Workers KV persistiert. Es sollte Sie nicht überraschen zu erfahren, dass die Übernahme dieser Daten aus KV und das Rendern einer HTML-Seite damit, unserer Homepage, ziemlich ähnlich zu allem ist, was wir bisher getan haben. Denken Sie daran, dass der Pfad / mit unserem index-Handler verknüpft ist: In dieser Datei möchten wir die Repos für das heutige Datum laden und sie an die Vorlage übergeben, um sie zu rendern. Es gibt ein paar Teile, die wir implementieren müssen, um das zum Laufen zu bringen – zunächst sehen wir uns handlers/index.js an.
// handlers/index.js
// ...
const Day = require('../store/day')
const handler = async () => {
try {
let repos = await Day.getRepos()
return new Response(template(repos), { headers })
} catch (err) {
return new Response(`Error! ${err} for ${JSON.stringify(repos)}`)
}
}
// ...
Während die allgemeine Struktur des Funktionshandlers gleich bleiben sollte, sind wir nun bereit, echte Daten in unsere Anwendung einzuspeisen. Wir sollten das Day-Modul importieren und innerhalb des Handlers await Day.getRepos aufrufen, um eine Liste von repos zurückzubekommen (keine Sorge, wir werden die entsprechenden Funktionen bald implementieren). Mit diesem Satz von repos übergeben wir sie an unsere template-Funktion, was bedeutet, dass wir sie tatsächlich innerhalb des HTML rendern können.
Innerhalb von Day.getRepos müssen wir die Liste der Repo-IDs aus KV laden und für jede von ihnen die entsprechenden Repo-Daten aus KV laden. In store/day.js:
// store/day.js
const Repo = require('./repo')
// ...
module.exports = {
getRepos: async function() {
const ids = await todayData()
return ids.length ? Repo.findMany(ids) : []
},
// ...
}
Die Funktion getRepos nutzt unsere zuvor definierte Funktion todayData wieder, die eine Liste von ids zurückgibt. Wenn diese Liste *irgendwelche* IDs enthält, möchten wir diese Repositories tatsächlich abrufen. Wieder rufen wir eine Funktion auf, die wir noch nicht vollständig definiert haben, importieren die Repo-Klasse und rufen Repo.findMany auf und übergeben unsere Liste von IDs. Wie Sie sich vorstellen können, sollten wir zu store/repo.js gehen und die begleitende Funktion implementieren.
// store/repo.js
class Repo {
static findMany(ids) {
return Promise.all(ids.map(Repo.find))
}
static async find(id) {
const persisted = await REPO_HUNT.get(`repos:${id}`)
const repo = JSON.parse(persisted)
return persisted ? new Repo({ ...repo }) : null
}
// ...
}
Um alle Repos für eine Menge von IDs zu finden, definieren wir zwei Klassen-Level- oder static-Funktionen, find und findMany, die Promise.all verwenden, um find für jede ID in der Menge aufzurufen und darauf zu warten, dass sie alle abgeschlossen sind, bevor das Promise aufgelöst wird. Der Großteil der Logik in find sucht das Repo anhand seiner ID (unter Verwendung des zuvor definierten Schlüssels repos:$id), parst den JSON-String und gibt eine neu instanziierte Instanz von Repo zurück.
Jetzt, da wir Repositories aus KV abrufen können, sollten wir diese Daten nehmen und sie tatsächlich in unserer Vorlage rendern. In handlers/index.js haben wir das repos-Array an die template-Funktion übergeben, die in templates/index.js definiert ist. In dieser Datei nehmen wir das repos-Array und rendern HTML-Schnipsel für jedes repo darin.
// templates/index.js
const layout = require('./layout')
const dateFormat = submitted_at =>
new Date(submitted_at).toLocaleDateString('en-us')
const repoTemplate = ({ description, name, submitted_at, url }) =>
`<div class="media">
<div class="media-content">
<p>
${name}
</p>
<p>
${description}
</p>
<p>
Submitted ${dateFormat(submitted_at)}
</p>
</div>
</div>
`
const template = repos => {
const renderedRepos = repos.map(repoTemplate)
return layout(`
<div>
${
repos.length
? renderedRepos.join('')
: `<p>No repos have been submitted yet!</p>`
}
</div>
`)
}
module.exports = template
Wenn wir diese Datei aufschlüsseln, haben wir zwei Hauptfunktionen: template (eine aktualisierte Version unserer ursprünglichen exportierten Funktion), die ein Array von repos nimmt, sie durchläuft und repoTemplate aufruft, um ein Array von HTML-Strings zu generieren. Wenn repos ein leeres Array ist, gibt die Funktion einfach ein p-Tag mit einem leeren Zustand zurück. Die Funktion repoTemplate verwendet Destrukturierungszuweisung, um die Variablen description, name, submitted_at und url aus dem an die Funktion übergebenen Repo-Objekt zu setzen und rendert jeden davon in relativ einfachem HTML, wobei sie sich auf Bulmas CSS-Klassen stützt, um schnell ein Media Object-Layout zu definieren.
Und damit sind wir mit dem Schreiben von Code für unser Projekt fertig! Nachdem wir eine ziemlich umfassende Full-Stack-Anwendung auf Basis von Workers entwickelt haben, kommen wir zum letzten Schritt: der Bereitstellung der Anwendung auf der Workers-Plattform.
Ihre Seite auf workers.dev bereitstellen
Jeder Workers-Benutzer kann eine kostenlose Workers.dev-Subdomain beanspruchen, nachdem er sich für ein Cloudflare-Konto registriert hat. In Wrangler haben wir es extrem einfach gemacht, Ihre Subdomain zu beanspruchen und zu konfigurieren, indem wir den Unterbefehl subdomain verwenden. Jedes Konto erhält eine Workers.dev-Subdomain, wählen Sie also mit Bedacht!
wrangler subdomain my-cool-subdomain
Mit einer konfigurierten Subdomain können wir nun unseren Code bereitstellen! Die Eigenschaft name in wrangler.toml gibt die endgültige URL an, unter der unsere Anwendung bereitgestellt wird: In meiner Codebasis ist der name auf repo-hunt eingestellt und meine Subdomain ist signalnerve.workers.dev, sodass meine endgültige URL für mein Projekt repo-hunt.signalnerve.workers.dev lautet. Lassen Sie uns das Projekt mit dem Befehl publish bereitstellen.
wrangler publish
Bevor wir das Projekt im Browser anzeigen können, müssen wir noch einen weiteren Schritt abschließen: In die Cloudflare-UI gehen, einen KV-Namespace erstellen und ihn mit unserem Projekt verknüpfen. Um diesen Prozess zu beginnen, melden Sie sich bei Ihrem Cloudflare-Dashboard an und wählen Sie rechts auf der Seite den Menüpunkt „Workers“ aus.

Im Workers-Bereich Ihres Dashboards finden Sie den Menüpunkt „KV“ und erstellen Sie einen neuen Namespace, der dem Namespace in Ihrem Codebase entspricht (wenn Sie den Codebeispielen gefolgt sind, ist dies REPO_HUNT).

In der Liste der KV-Namespaces kopieren Sie Ihre Namespace-ID. Zurück in unserem Projekt fügen wir einen Schlüssel `kv-namespaces` zu unserer `wrangler.toml` hinzu, um unseren neuen Namespace im Codebase zu verwenden.
# wrangler.toml
[[kv-namespaces]]
binding = "REPO_HUNT"
id = "$yourNamespaceId"
Um sicherzustellen, dass Ihr Projekt den neuen KV-Namespace verwendet, veröffentlichen Sie Ihr Projekt ein letztes Mal.
wrangler publish
Damit sollte Ihre Anwendung erfolgreich aus Ihrem KV-Namespace lesen und schreiben können. Wenn Sie die URL meines Projekts öffnen, sollten Sie die endgültige Version unseres Projekts sehen – eine vollständige, datengesteuerte Anwendung, ohne dass Sie Server verwalten müssen, komplett auf der Workers-Plattform aufgebaut!

Was kommt als Nächstes?
In diesem Tutorial haben wir eine Full-Stack-Serverless-Anwendung auf der Workers-Plattform mit Wrangler, Cloudflares Befehlszeilentool zum Erstellen und Bereitstellen von Workers-Anwendungen, erstellt. Es gibt eine Menge Dinge, die Sie tun könnten, um diese Anwendung weiter auszubauen: zum Beispiel die Möglichkeit, Einreichungen hochzuwählen oder sogar Kommentare und andere Daten zuzulassen. Wenn Sie den fertigen Code für dieses Projekt sehen möchten, schauen Sie sich das GitHub-Repo an!
Das Workers-Team pflegt eine ständig wachsende Liste neuer Vorlagen für den Einstieg in die Projektentwicklung. Wenn Sie sehen möchten, was Sie erstellen können, schauen Sie sich unsere Vorlagengalerie an. Darüber hinaus sollten Sie sich einige der Tutorials in der Workers-Dokumentation ansehen, wie z. B. den Bau eines Slack-Bots oder einen QR-Code-Generator.
Wenn Sie das gesamte Tutorial durchlaufen haben (oder wenn Sie coole Dinge bauen, die Sie teilen möchten), würde ich gerne hören, wie es gelaufen ist auf Twitter. Wenn Sie an Serverless interessiert sind und über neue Tutorials, die ich veröffentliche, auf dem Laufenden bleiben möchten, melden Sie sich unbedingt für meinen Newsletter an und abonnieren Sie meinen YouTube-Kanal!