Fullmoon ist ein schnelles und minimalistisches Web-Framework, das auf Redbean basiert-einem tragbaren, verteilten Webserver mit einem Datei.
Alles, was für die Entwicklung und Verteilung erforderlich ist, erhält eine einzige Datei ohne externe Abhängigkeiten und nach der Verpackung mit Redbean -Läufen unter Windows, Linux oder MacOS. Das Folgende ist ein vollständiges Beispiel für eine Vollmondanwendung:
local fm = require " fullmoon "
fm . setTemplate ( " hello " , " Hello, {%& name %} " )
fm . setRoute ( " /hello/:name " , function ( r )
return fm . serveContent ( " hello " , { name = r . params . name })
end )
fm . run ()
Nachdem es mit Redbean verpackt ist, kann es mit ./redbean.com
gestartet werden, der einen Server startet, der "Hallo, World" an eine an eine an http: // localhost: 8080/Hallo/Welt gesendete HTTP -Anfrage zurückgibt.
Redbean ist ein plattformübergreifender Webserver mit einzigartigen und leistungsstarken Qualitäten. Während es mehrere auf LUA-basierte Webrahmen gibt (Lapis, LOR, Seemann, Pegasus und andere), integriert sich keiner von ihnen in Redbean (obwohl es einen experimentellen Rahmen anpan gibt).
Fullmoon ist ein leichtes und minimalistisches Web -Framework, das aus der Perspektive der Präsentation aller Funktionen geschrieben wurde, die Redbean bietet, indem sie sie auf einfachste und effizienteste Weise erweitert und erweitert. Es läuft schnell und wird mit Batterien enthalten (Routen, Vorlagen, JSON -Generation und mehr).
Vollmond folgt der LUA -Philosophie und bietet einen minimalen Satz von Werkzeugen, die nach Bedarf kombiniert werden können und als Grundlage für den Aufbau verwendet werden.
fork
, socket
, gemeinsamer Speicher und mehrLaden Sie eine Kopie von Redbean herunter, indem Sie die folgenden Befehle ausführen (überspringen Sie die zweite, wenn Sie diese Befehle unter Windows ausführen):
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.com
Die neueste Versionsnummer kann mit der folgenden Anfrage abgerufen werden:
curl https://redbean.dev/latest.txt
Eine weitere Option besteht darin, Redbean aus der Quelle zu erstellen, indem sie Anweisungen für den Quellbau befolgen.
fullmoon.lua
in .lua/
Verzeichnis.init.lua
(z. B. dem in der Beschreibung angezeigten LUA -Code). Eine andere Option besteht darin, den Anwendungscode in eine separate Datei zu platzieren (z. B. .lua/myapp.lua
) und require "myapp"
zu .init.lua
hinzufügen. So werden alle eingeschlossenen Beispiele vorgestellt.
zip redbean.com .init.lua .lua/fullmoon.lua
Wenn der Anwendungscode wie oben beschrieben in einer separaten LUA -Datei gespeichert wird, stellen Sie ihn sicher, dass er in das .lua/
Verzeichnis und diese Datei zu Reißverschluss gelegt wird.
./redbean.com
Wenn dieser Befehl auf Linux ausgeführt wird und einen Fehler über das Finden von Interpreter auswirkt, sollte er durch Ausführen des folgenden Befehls behoben werden (obwohl er einen Neustart eines Systems möglicherweise nicht überlebt):
sudo sh -c " echo ':APE:M::MZqFpD::/bin/sh:' >/proc/sys/fs/binfmt_misc/register "
Wenn dieser Befehl bei Verwendung von Redbean 2.x bei WSL oder Wein rätselhafte Fehler erzeugt, können sie durch Deaktivieren von Binfmt_MISC behoben werden:
sudo sh -c ' echo -1 >/proc/sys/fs/binfmt_misc/status '
Starten Sie einen Browser, der auf http: // localhost: 8080/hello/world zeigt, und es sollte "Hallo, Welt" zurückgeben (vorausgesetzt, die Anwendung verwendet den in der Einführung angezeigten Code oder den im Nutzungsabschnitt).
Das einfachste Beispiel muss (1) das Modul laden, (2) eine Route konfigurieren und (3) die Anwendung ausführen:
local fm = require " fullmoon " -- (1)
fm . setRoute ( " /hello " , function ( r ) return " Hello, world " end ) -- (2)
fm . run () -- (3)
Diese Bewerbung beantwortet auf eine Anfrage für /hello
-URL mit Rückgabe von "Hallo, World" -Inhalten (und dem 200 -Status -Code) und reagiert mit dem 404 -Statuscode für alle anderen Anforderungen.
setRoute(route[, action])
: registriert eine Route. Wenn route
eine Zeichenfolge ist, wird sie als Routenausdruck verwendet, um den Anforderungspfad zu vergleichen. Wenn es sich um eine Tabelle handelt, sind ihre Elemente Strings, die als Routen verwendet werden, und ihre Hash -Werte sind Bedingungen, gegen die die Routen überprüft werden. Wenn der zweite Parameter eine Funktion ist, wird er ausgeführt, wenn alle Bedingungen erfüllt sind. Wenn es sich um eine Zeichenfolge handelt, wird es als Routenausdruck verwendet und die Anforderung wird so verarbeitet, als ob es auf der angegebenen Route gesendet wird (fungiert als interne Umleitung). Wenn eine Bedingung nicht erfüllt ist, wird die nächste Route überprüft. Der Routenausdruck kann mehrere Parameter und optionale Teile haben. Der Aktionshandler akzeptiert eine Anforderungstabelle, die Zugriff auf Anforderungs- und Routenparameter sowie Header, Cookies und Sitzungen bietet.
setTemplate(name, template[, parameters])
: Registriert eine Vorlage mit dem angegebenen Namen oder einer Reihe von Vorlagen aus einem Verzeichnis. Wenn template
eine Zeichenfolge ist, wird sie in einen Vorlagenhandler zusammengestellt. Wenn es sich um eine Funktion handelt, wird es gespeichert und aufgerufen, wenn das Rendern der Vorlage angefordert wird. Wenn es sich um eine Tabelle handelt, ist sein erstes Element eine Vorlage oder eine Funktion und der Rest wird als Optionen verwendet. Wenn Sie beispielsweise ContentType
als eine der Optionen angeben, legt die Content-Type
für den generierten Inhalt fest. Mehrere Vorlagen ( 500
, json
und andere) werden standardmäßig bereitgestellt und können überschrieben werden. parameters
sind eine Tabelle mit Vorlagenparametern, die als Name/Wertpaare gespeichert sind (als Variablen in der Vorlage bezeichnet).
serveResponse(status[, headers][, body])
: Sendet eine HTTP -Antwort unter Verwendung des angegebenen status
, headers
und body
. headers
ist eine optionale Tabelle, die mit HTTP -Headernamen/Wertpaaren gefüllt ist. Wenn diese Header -Set vorliegt, werden alle anderen Header, die früher während der Behandlung derselben Anfrage festgelegt wurden, entfernt . Headernamen sind von Fall unempfindlich , bereitgestellt jedoch Aliase für Header-Namen mit Striche mit Fallempfindlichkeit : {ContentType = "foo"}
ist eine alternative Form für {["Content-Type"] = "foo"}
. body
ist eine optionale Zeichenfolge.
serveContent(name, parameters)
: Rendern eine Vorlage mit den bereitgestellten Parametern. name
ist eine Zeichenfolge, die die Vorlage benennt (wie durch einen Aufruf setTemplate
festgelegt), und parameters
ist eine Tabelle mit Vorlagenparametern, die als Name/Wert -Paare gespeichert sind (als Variablen in der Vorlage bezeichnet).
run([options])
: Führen Sie den Server mit konfigurierten Routen aus. Standardmäßig hört der Server auf Localhost und Port 8080 an. Diese Werte können durch Einstellen addr
und port
in der options
-Tabelle geändert werden.
Ausführlicher Beispiele erfordert eine require
in der Datei .init.lua
, die das Modul mit jedem Beispielcode lädt. Für das in showcase.lua
, .init.lua
implementierte Showcase -Beispiel enthält die folgenden:
-- this is the content of .init.lua
require " showcase "
-- this loads `showcase` module from `.lua/showcase.lua` file,
-- which also loads its `fullmoon` dependency from `.lua/fullmoon.lua`
Das Showcase -Beispiel zeigt mehrere Vollmondfunktionen:
serveAsset
)serveRedirect
)Die folgenden Dateien müssen zu Redbean ausführbarer Datei/Archiv hinzugefügt werden:
.init.lua - Erfordernde "Showcase" .lua/fullmoon.lua .lua/showcase.lua
Das Beispiel für TechEmpower implementiert verschiedene Testtypen für die Web-Framework-Benchmarks mit Fullmonoon und einer SQLite-Datenbank in der Memory.
Dieses Beispiel zeigt mehrere Fullmona/Redbean -Funktionen:
Die folgenden Dateien müssen zu Redbean ausführbarer Datei/Archiv hinzugefügt werden:
.init.lua - Erfordernde "TechBench" .lua/fullmoon.lua .lua/techbench.lua
Das Beispiel für HTMX -Board zeigt eine einfache Anwendung, die HTML -Fragmente generiert, die mithilfe der HTMX -Bibliothek an den Client geliefert werden.
Dieses Beispiel zeigt mehrere Fullmona/Redbean -Funktionen:
Die folgenden Dateien müssen zu Redbean ausführbarer Datei/Archiv hinzugefügt werden:
.init.lua - Erfordernde "htmxboard" .lua/fullmoon.lua .lua/htmxboard.lua Assets/Styles.css TMPL/* - Alle Dateien aus Beispielen/HTMXBoard/TMPL -Verzeichnis
Anmerkung 1: Da alle Daten im Speicher gespeichert sind, wird dieses Beispiel im Uniprocess -Modus ausgeführt.
ANMERKUNG 2: In diesem Beispiel werden HTMX-, Hyperskript- und sortierbare Bibliotheken aus externen Ressourcen abgerufen. Diese Bibliotheken können jedoch auch als lokale Vermögenswerte gespeichert werden, wodurch ein völlig autarker tragbares Verteilungspaket bereitgestellt wird.
Das HTMX-SSE-Beispiel zeigt eine Möglichkeit, Server-Sent-Ereignisse (SSE) zu generieren, die an einen Client gestreamt werden können (der Ergebnisse mithilfe der HTMX-Bibliothek und seiner SSE-Erweiterung anzeigt).
Dieses Beispiel zeigt mehrere Fullmona/Redbean -Funktionen:
streamContent
)Die folgenden Dateien müssen zu Redbean ausführbarer Datei/Archiv hinzugefügt werden:
.init.lua - Erfordernde "htmxsse" .lua/fullmoon.lua .lua/htmxsse.lua
Jede Vollmond -Anwendung folgt dem gleichen Grundfluss mit fünf Hauptkomponenten:
Schauen wir uns jede der Komponenten an, die mit dem Anforderungsrouting beginnen.
Vollmonoon behandelt jede HTTP -Anforderung mit demselben Vorgang:
false
oder nil
vom Aktionshandler zurückgegeben wird (und setzt den Prozess ansonsten fort) Im Allgemeinen binden Routendefinitionen Anforderungs -URLs (und eine Reihe von Bedingungen) an Handlungshandler (die reguläre LUA -Funktionen sind). Alle Bedingungen werden in einer zufälligen Reihenfolge für jede URL überprüft, die der Routendefinition entspricht. Sobald eine Bedingung fehlschlägt, wird die Routenverarbeitung abgebrochen und die nächste Route mit einer Ausnahme überprüft: Jede Bedingung kann den otherwise
Wert festlegen, der eine Antwort mit dem angegebenen Statuscode auslöst.
Wenn keine Route mit der Anforderung übereinstimmt, wird die Standardverarbeitung 404 ausgelöst, die durch Registrieren einer benutzerdefinierten 404 -Vorlage ( fm.setTemplate("404", "My 404 page...")
) angepasst werden kann.
Jede Route nimmt einen Pfad ein, der genau übereinstimmt, so dass die Route "/hello"
Anfragen für /hello
übereinstimmt und nicht übereinstimmt /hell
, /hello-world
oder /hello/world
. Die Route unten antwortet mit "Hallo, Welt!" Für alle Anfragen, die am /hello
-Pfad gerichtet sind und 404 für alle anderen Anfragen zurückgeben.
fm . setRoute ( " /hello " , function ( r ) return " Hello, World! " end )
Um einen Pfad zu erreichen, auf dem /hello
nur ein Teil davon ist, können optionale Parameter und Splat verwendet werden.
Zusätzlich zu festen Routen kann jeder Pfad Platzhalter für Parameter enthalten, die durch a :
unmittelbar nach dem Parameternamen identifiziert werden:
fm . setRoute ( " /hello/:name " ,
function ( r ) return " Hello, " .. ( r . params . name ) end )
Jeder Parameter entspricht einem oder mehreren Zeichen außer /
, also die Route "/hello/:name"
übereinstimmt /hello/alice
, /hello/bob
, /hello/123
und stimmt nicht überein /hello/bob/and/alice
(wegen von Die nicht übereinstimmenden Vorwärts-Schrägstriche) oder /hello/
(weil die Länge des zu anpassenden Fragments Null ist).
Parameternamen können nur alphanumerische Zeichen und _
enthalten.
Parameter können mit der Anforderungstabelle und ihrer params
-Tabelle zugegriffen werden, sodass r.params.name
verwendet werden kann, um den Wert des name
aus dem früheren Beispiel zu erhalten.
Jeder angegebene Routenfragment oder Parameter kann als optional deklariert werden, indem sie in Klammern einwickelt werden:
fm . setRoute ( " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Im obigen Beispiel werden sowohl /hello
als auch /hello/Bob
akzeptiert, aber nicht /hello/
, da der nachverfolgende Schrägstrich Teil des optionalen Fragments ist und :name
immer noch einen oder mehrere Zeichen erwartet.
Jeder unübertroffene optionale Parameter wird als Wert false
. In dem obigen Fall "Hallo, Welt!" wird für die URL /hello
Anfrage zurückgegeben.
Mehr als ein optionaler Parameter kann angegeben und optionale Fragmente verschachtelt werden, sodass sowohl "/posts(/:pid/comments(/:cid))"
als auch "/posts(/:pid)/comments(/:cid)"
sind gültige Routenwerte.
Es gibt eine andere Art von Parameter namens SPLAT, die als *
geschrieben ist und mit Null oder mehr Zeichen entspricht, einschließlich eines Vorwärtsschräsers ( /
). Der Splat wird auch in der params
-Tabelle unter dem splat
-Namen gespeichert. Die Route "/download/*"
übereinstimmt /download/my/file.zip
file.zip und der Splat erhält den Wert von my/file.zip
. Wenn in derselben Route mehrere Splats benötigt werden, können Splats Namen ähnlich wie andere Parameter: /download/*path/*fname.zip
/download/*path/:fname.zip
, wie der erste SPLAT alle Pfadenteile außer dem Dateinamen erfasst).
Alle Parameter (einschließlich des SPLAT) können in jedem Teil des Pfades erscheinen und von einem anderen Text umgeben werden, der genau übereinstimmt. Dies params.splat
, dass die Route "/download/*/:name.:ext"
params.ext
my/path
/download/my/path/file.zip
params.name
zip
file
Ein weiterer Grund für die Verwendung von SPLAT besteht darin, mehrere Routen mit demselben Pfad im System registriert zu werden. Die aktuelle Implementierung überschreibt Routen mit demselben Namen und um zu vermeiden, dass ein benanntes SPLAT verwendet werden kann, um eindeutige Pfade zu erstellen. Zum Beispiel,
fm . setRoute ( " /*dosomething1 " , function ( r ) return " something 1 " end )
fm . setRoute ( " /*dosomething2 " , function ( r ) return " something 2 " end )
Dies kann in Situationen verwendet werden, in denen eine Reihe von Bedingungen vorhanden sind, die im Aktionshandler überprüft werden müssen. Auch wenn es möglich ist, beide Routen zu einem zu kombinieren, ist es manchmal sauberer, sie getrennt zu halten.
Der Standardwert für die Parameter beträgt alle Zeichen (außer /
) der Länge eins oder mehrere. Um einen anderen Satz gültiger Zeichen anzugeben, kann es am Ende des Variablennamens hinzugefügt werden. Beispielsweise ändert sich der Parameter mit :id[%d]
anstelle von :id
ändert den Parameter nur Ziffern.
fm . setRoute ( " /hello(/:id[%d]) " ,
function ( r ) return " Hello, " .. ( r . params . id or " World! " ) end )
Die folgenden LUA -Charakterklassen werden unterstützt: %w
, %d
, %a
, %l
, %u
und %x
; Jeder Interpunktionscharakter (einschließlich %
und ]
) kann ebenfalls mit %
entkommen. Negative Klassen (in Lua als %W
[^%d]
) werden nicht unterstützt , aber nicht in der SET-Syntax wird unterstützt.
Beachten Sie, dass die Anzahl der Wiederholungen nicht geändert werden kann (also :id[%d]*
ist keine gültige Möglichkeit, Null-or-More-Ziffern zu akzeptieren), da nur Sätze zulässig sind und die Werte immer noch ein oder mehrere Zeichen akzeptieren. Wenn mehr Flexibilität bei der Beschreibung akzeptabler Formate erforderlich ist, können benutzerdefinierte Validatoren verwendet werden, um die passende Logik zu erweitern.
Abfragen- und Formularparameter können auf die gleiche Weise wie die Pfadparameter mit der params
-Tabelle in der request
zugegriffen werden, die an jeden Aktionshandler übergeben wird. Beachten Sie, dass bei einem Konflikt zwischen Parameter- und Abfrage-/Formularnamen die Parameternamen Vorrang haben .
Es gibt einen Sonderfall, der zu einer Tabelle anstelle eines Zeichenfolgewerts zurückgegeben wird: Wenn der Parametername der Abfrage/Form in []
endet, werden alle übereinstimmenden Ergebnisse (eine oder mehrere) als Tabelle zurückgegeben. Zum Beispiel ist für eine Abfragezeichenfolge a[]=10&a[]&a[]=12&a[]=
Der Wert von params["a[]"]
ist {10, false, 12, ""}
.
Da das Schreiben dieser Parameternamen möglicherweise mehrere Klammern erfordern, kann params.a
als Verknüpfung für params["a[]"]
verwendet werden, wobei beide Formulare dieselbe Tabelle zurückgeben.
Multipart -Parameter werden ebenfalls verarbeitet, wenn sie angefordert werden, und kann auf die gleiche Weise wie die Rest der Parameter mithilfe der params
-Tabelle zugegriffen werden. Beispielsweise können Parameter mit simple
Namen und more
aus einer Nachricht mit multipart/form-data
-Inhaltstyp mit params.simple
und params.more
abgerufen werden.
Da einige der mehrteiligen Inhalte zusätzliche Header und Parameter innerhalb dieser Header enthalten können, können sie mit multipart
Feld der params
-Tabelle zugegriffen werden:
fm . setRoute ({ " /hello " , simple = " value " }, function ( r )
return " Show " .. r . params . simple .. " " .. r . params . multipart . more . data )
end )
Die multipart
Tabelle enthält alle Teile der mehrteiligen Meldung (so kann sie mit ipairs
iteriert werden), ermöglicht jedoch auch Zugriff mit Parameternamen ( params.multipart.more
). Jedes der Elemente ist auch eine Tabelle, die die folgenden Felder enthält:
nil
wenn nicht.nil
wenn nicht. Diese mehrteilige Verarbeitung konsumiert mehrteilige Subtypen und behandelt rekursive mehrteilige Nachrichten. Es fügt auch einen Teil mit Content-ID
Wert ein, der dem start
in die erste Position entspricht.
Trotz aller früheren Beispiele, die eine einzige Route zeigen, ist dies bei realen Anwendungen selten der Fall. Wenn mehrere Routen vorhanden sind, werden sie immer in der Reihenfolge bewertet, in der sie registriert sind .
Ein setRoute
-Aufruf kann auch mehrere Routen festlegen, wenn sie die gleichen Bedingungen haben und dieselbe Aktionshandler teilen:
fm . setRoute ({ " /route1 " , " /route2 " }, handler )
Dies entspricht zwei Aufrufen, die jede Route einzeln einstellen:
fm . setRoute ( " /route1 " , handler )
fm . setRoute ( " /route2 " , handler )
Angesichts der Tatsache, dass Routen in der Reihenfolge bewertet werden, in der sie festgelegt sind, müssen selektivere Routen zuerst festgelegt werden, da sie sonst möglicherweise keine Chance erhalten, bewertet zu werden:
fm . setRoute ( " /user/bob " , handlerBob )
fm . setRoute ( " /user/:name " , handlerName )
Wenn die Routen in der entgegengesetzten Reihenfolge festgelegt sind, wird /user/bob
möglicherweise nie überprüft, solange der Aktionshandler "/user/:name"
einige nicht false
Ergebnisse zurückgibt.
Wie bereits beschrieben, wird eine Antwort mit einem 404 -Statuscode zurückgegeben, wenn keine der Routen übereinstimmen. Es kann Fälle geben, in denen dies nicht wünschenswert ist; Wenn die Anwendung beispielsweise LUA -Skripte enthält, um Anforderungen zu bearbeiten, die nicht explizit als Routen registriert sind. In diesen Fällen kann eine Catch-All-Route hinzugefügt werden, die die Standard-Redbean-Verarbeitung implementiert (der Name des SPLAT-Parameters wird nur verwendet, um diese Route gegen andere /*
Routen zu disambiguieren, die an anderer Stelle verwendet werden können):
fm . setRoute ( " /*catchall " , fm . servePath )
Jede Route kann mit einem optionalen Namen versehen werden, der für die Verweise auf diese Route nützlich ist, wenn ihre URL basierend auf bestimmten Parameterwerten generiert werden muss. Stellen Sie makePath
-Funktion entweder einen Routennamen oder eine Routen -URL selbst sowie die Parametertabelle an und gibt einen Pfad mit besiedelten Parameter -Platzhaltern zurück:
fm . setRoute ( " /user/:name " , handlerName )
fm . setRoute ({ " /post/:id " , routeName = " post " }, handlerPost )
fm . makePath ( " /user/:name " , { name = " Bob " }) -- > /user/Bob
fm . makePath ( " /post/:id " , { id = 123 }) -- > /post/123
fm . makePath ( " post " , { id = 123 }) -- > /post/123, same as the previous one
Wenn zwei Routen denselben Namen verwenden, ist der Name dem zuletzt zugeordneten Namen zugeordnet, aber beide Routen sind noch vorhanden.
Der Routename kann auch mit externen/statischen Routen verwendet werden, die nur für die URL -Erzeugung verwendet werden.
Wenn die Route nur für die Pfaderzeugung verwendet wird, muss sie nicht einmal einen Routenhandler haben:
fm . setRoute ({ " https://youtu.be/:videoid " , routeName = " youtube " })
fm . makePath ( " youtube " , { videoid = " abc " }) -- > https://youtu.be/abc
Eine Route ohne Aktionsbehandlung wird während des Streckenanpassungsprozesses übersprungen.
Interne Routen ermöglichen es, einen Satz von URLs auf einen anderen zu lenken. Die Ziel -URL kann auf eine statische Ressource oder ein .lua
-Skript verweisen. Wenn beispielsweise Anfragen nach einem Standort in einen anderen umgeleitet werden müssen, leitet die folgende Konfiguration Anforderungen für Ressourcen unter /blog/
url auf diejenigen unter /new-blog/
URL weiter, solange die Zielressource existiert:
fm . setRoute ( " /blog/* " , " /new-blog/* " )
Diese Route akzeptiert eine Anfrage für /blog/post1
und dient /new-blog/post1
als Antwort, so lange wie /new-blog/post1
-Vermögen existiert. Wenn das Vermögen nicht vorhanden ist, wird die nächste Route überprüft. In ähnlicher Weise führt die Verwendung fm.setRoute("/static/*", "/*")
Anforderungen für /static/help.txt
, die Ressourcen /help.txt
bedient werden.
Beide URLs können Parameter enthalten, die ausgefüllt werden, wenn sie aufgelöst werden:
fm . setRoute ( " /blog/:file " , " /new-blog/:file.html " ) -- <<-- serve "nice" URLs
fm . setRoute ( " /new-blog/:file.html " , fm . serveAsset ) -- <<-- serve original URLs
In diesem Beispiel wird "schöne" URLs gelöst, die ihre "HTML" -Versionen dienen. Beachten Sie, dass dies die clientseitige Umleitung nicht durch Rückgabe des 3xx
Statuscodes auslöst , sondern die Ausführung in interner Ausführung. Beachten /new-blog/mylink.html.html
auch, dass die zweite Regel erforderlich ist, um die "ursprünglichen" URLs zu dienen, /blog/mylink.html
sie nicht von der ersten Regel behandelt werden /new-blog/mylink.html.html
, was nicht wahrscheinlich ist, sodass die Route übersprungen und die nächste überprüft wird. Wenn auch die Handhabung von Pfadabschlüssen erforderlich ist, kann *path
anstelle von :file
verwendet werden, da *
Pfadabschlüsse zulässt.
Wenn eine Anwendung je nach bestimmten Werten von Anforderungsattributen unterschiedliche Funktionen ausführen muss request.method == "GET"
z. B. eine Methode), enthält diese Bibliothek zwei Hauptoptionen request.method == "GET"
und (2) eine Bedingung hinzufügen, die Anforderungen herausfiltert, sodass nur Anfragen mit dem angegebenen Attributwert den Aktionshandler erreichen. In diesem Abschnitt wird die zweite Option ausführlicher beschrieben.
Jede registrierte Route standardmäßig reagiert auf alle HTTP -Methoden (Get, Put, Post usw.). Es ist jedoch möglich, jede Route so zu konfigurieren, dass sie nur auf bestimmte HTTP -Methoden reagieren:
fm . setRoute ( fm . GET " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
In diesem Fall konfiguriert die Syntax fm.GET"/hello(/:name)"
die Route, um nur GET
-Anfragen zu akzeptieren. Diese Syntax entspricht dem Übergeben einer Tabelle mit der Route und den zusätzlichen Filterbedingungen:
fm . setRoute ({ " /hello(/:name) " , method = " GET " },
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Wenn mehr als eine Methode angegeben werden muss, kann eine Tabelle mit einer Liste von Methoden anstelle eines String -Wertes übergeben werden:
fm . setRoute ({ " /hello(/:name) " , method = { " GET " , " POST " }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
Jede Route, die eine GET
-Anforderung (implizit) auch eine HEAD
ermöglicht, und diese Anfrage wird durch Rückgabe aller Header abgeschlossen, ohne den Körper selbst zu senden. Wenn diese implizite Handhabung aus irgendeinem Grund nicht wünschenswert ist, deaktiviert das Hinzufügen von HEAD = false
der Methode Tabelle (wie in method = {"GET", "POST", HEAD = false}
).
Beachten Sie, dass Anfragen mit nicht übereinstimmenden Methoden nicht abgelehnt werden, sondern durch die Überprüfung von anderen Strecken und den zurückgegebenen Statuscode von 404 ausgelöst werden, wenn sie nicht übereinstimmen (mit einer Ausnahme).
Zusätzlich zur method
können andere Bedingungen mit host
, clientAddr
, serverAddr
, scheme
, Anforderungsheadern und Parametern angewendet werden. Wenn Sie beispielsweise name = "Bob"
als eine der Bedingungen angeben, stellt der Wert des name
"Bob" für den aufgerufenen Aktionsindikator fest.
Jeder Anforderungsheader kann mit dem Header-Namen als Schlüssel überprüft werden. Daher ist ContentType = "multipart/form-data"
erfüllt, wenn der Wert des Content-Type
Headers multipart/form-data
ist. Beachten Sie, dass der Header-Wert andere Elemente (eine Grenze oder ein Zeichensatz als Teil des Content-Type
) enthalten kann und nur der tatsächliche Medientyp verglichen wird.
Da Namen für Header, Parameter und Eigenschaften überlappen können, werden sie in der folgenden Reihenfolge überprüft:
ContentType
,method
, port
, host
usw.) und Host
-Header wird ebenfalls zuerst überprüft (obwohl sie ein einzelnes Wort ist), sodass Host
-Filter auf der Grundlage des Header Host
verweisen, während host
auf der Grundlage des host
Bezug genommen werden.
Stringwerte sind nicht die einzigen Werte, die in bedingten Routen verwendet werden können. Wenn mehr als ein Wert akzeptabel ist, ermöglicht das Übergeben einer Tabelle eine Liste akzeptabler Werte. Wenn Bob
und Alice
beispielsweise akzeptable Werte sind, drückt name = {Bob = true, Alice = true}
dies als Bedingung aus.
Zwei in einer Tabelle übergebene spezielle Werte ermöglichen es, eine Regex- oder eine Mustervalidierung anzuwenden:
regex
: Akzeptiert eine Zeichenfolge, die einen regelmäßigen Ausdruck hat. Zum Beispiel hat name = {regex = "^(Bob|Alice)$"}
das gleiche Ergebnis wie der Hash -Check, der zuvor in diesem Abschnitt angezeigt wurdepattern
: Akzeptiert eine Zeichenfolge mit einem Lua -Musterausdruck. Zum Beispiel akzeptiert name = {pattern = "^%u%l+$"}
Werte, die mit einem Großbuchstaben beginnen, gefolgt von einem oder mehreren Kleinbuchstaben. Diese beiden Überprüfungen können mit der Tabelle Existenzprüfung kombiniert werden: name = {Bob = true, regex = "^Alice$"}
akzeptiert sowohl Bob
als auch Alice
-Werte. Wenn die erste Tabellexistenzprüfung fehlschlägt, werden die Ergebnisse des regex
oder pattern
zurückgegeben.
Der letzte Typ eines benutzerdefinierten Validators ist eine Funktion. Die bereitgestellte Funktion empfängt den zur Validierung von Wert und ihr Ergebnis als false
oder true
bewertet. Durch das Bestehen von id = tonumber
stellt beispielsweise sicher, dass der id
-Wert eine Zahl ist. Als ein weiteres Beispiel stellt clientAddr = fm.isLoopbackIp
sicher, dass die Kundenadresse eine Loopback -IP -Adresse ist.
fm . setRoute ({ " /local-only " , clientAddr = fm . isLoopbackIp },
function ( r ) return " Local content " end )
Da die Validator -Funktion dynamisch generiert werden kann, funktioniert dies auch:
local function isLessThan ( n )
return function ( l ) return tonumber ( l ) < n end
end
fm . setRoute ( fm . POST { " /upload " , ContentLength = isLessThan ( 100000 )},
function ( r ) ... handle the upload ... end )
Es ist wichtig zu bedenken, dass die Validator -Funktion tatsächlich eine Funktion zurückgibt, die während einer Anfrage zur Anwendung des Schecks aufgerufen wird. Im vorherigen Beispiel akzeptiert die zurückgegebene Funktion einen Header -Wert und vergleicht sie mit der während ihrer Erstellung übergebenen Grenze.
In einigen Fällen ist es ein ausreichender Grund, eine Reaktion an den Kunden zurückzugeben, ohne andere Wege zu überprüfen. In einem solchen Fall gibt otherwise
eine Antwort mit dem angegebenen Status oder dem Ergebnis der Funktion zurück:
local function isLessThan ( n )
return function ( l ) return tonumber ( l ) < n end
end
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = isLessThan ( 100000 ), otherwise = 413
}, function ( r ) ... handle the upload ... end )
In diesem Beispiel übereinstimmt die Routing-Engine mit der Route und validiert dann die beiden Bedingungen, in denen der Methodenwert mit POST
und dem Wert des Headers Content-Length
mit dem Ergebnis der isLessThan
-Funktion verglichen wird. Wenn eine der Bedingungen nicht übereinstimmt, wird der durch den otherwise
Wert angegebene Statuscode mit dem Rest der Antwort zurückgegeben.
Wenn die otherwise
Bedingung nur für die ContentLength
-Prüfung gelten muss, kann der otherwise
Wert zusammen mit der Validator -Funktion in eine Tabelle verschoben werden, die der ContentLength
-Prüfung zugeordnet ist:
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = { isLessThan ( 100000 ), otherwise = 413 }
}, function ( r ) ... handle the upload ... end )
Der Unterschied zwischen den letzten beiden Beispielen besteht darin, dass in diesem Beispiel nur der Versagen ContentLength
-Checks die 413 -Antwort auslöst (und alle anderen Methoden auf andere Routen fallen), während in der vorherigen method
und ContentLength
-Check -Fehlern dieselbe 413 -Antwort auslösen.
Beachten Sie, dass der nil
gegen eine Tabelle als gültig erachtet wird und die Route akzeptiert wird. Beispielsweise schlägt ein Scheck über einen optionalen Parameter, der gegen eine String ( name = "Bo"
) erstellt wurde, fehl, wenn der Wert von params.name
nil
ist, aber übergibt, wenn dieselbe Scheck gegen eine Tabelle durchgeführt wird ( name = {Bo=true, Mo=true}
), einschließlich Regex/Musterprüfungen. Wenn dies nicht wünschenswert ist, kann eine benutzerdefinierte Validator -Funktion den erwarteten Wert ausdrücklich überprüfen.
Betrachten Sie das folgende Beispiel:
fm . setRoute ({ " /hello(/:name) " ,
method = { " GET " , " POST " , otherwise = 405 }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
In diesem Fall wird der 405 -Statuscode zurückgegeben, wenn dieser Endpunkt mit der PUT
-Methode zugegriffen wird, anstatt andere Routen zu überprüfen (da die method
nicht erfüllt ist), wie mit dem angegebenen otherwise
Wert konfiguriert. Wie an anderer Stelle dokumentiert, akzeptiert diese Route auch eine HEAD
(auch wenn sie nicht aufgeführt ist), da eine GET
-Anfrage angenommen wird.
Wenn der Statuscode für 405 (schlechte Methode) zurückgegeben wird und der Header Allow
, wird er nicht festgelegt, wird er auf die Liste der von der Route zugelassenen Methoden eingestellt. In dem obigen Fall ist es so eingestellt, dass es Werte GET, POST, HEAD, OPTIONS
, da dies die Methoden sind, die durch diese Konfiguration zulässig sind. Wenn der otherwise
Wert eine Funktion ist (und nicht eine Zahl), liegt das Zurückgeben eines ordnungsgemäßen Ergebniss und das Einstellen des Allow
Headers in der Verantwortung dieser Funktion.
Der otherwise
Wert kann auch auf eine Funktion eingestellt werden, die mehr Flexibilität bietet als nur ein Statuscode. Beispielsweise löst das Einstellen otherwise = fm.serveResponse(413, "Payload Too Large")
eine Antwort mit dem angegebenen Statuscode und der angegebenen Status -Meldung aus.
Die Validierung des Handlingsformulars erfordert häufig eine Reihe von Bedingungen für denselben Parameter und eine benutzerdefinierte Fehlermeldung, die möglicherweise zurückgegeben werden muss, wenn die Bedingungen nicht erfüllt sind. Diese werden von speziellen Validatoren bereitgestellt, die von makeValidator
-Funktion zurückgegeben werden:
local validator = fm . makeValidator {
{ " name " , minlen = 5 , maxlen = 64 , msg = " Invalid %s format " },
{ " password " , minlen = 5 , maxlen = 128 , msg = " Invalid %s format " },
}
fm . setRoute ( fm . POST { " /signin " , _ = validator }, function ( r )
-- do something useful with name and password
return fm . serveRedirect ( 307 , " / " )
end )
In diesem Beispiel ist der Validator so konfiguriert, dass er zwei Parameter - "Name" und "Passwort" - für seine min- und maximalen Längen überprüft und eine Nachricht zurückgibt, wenn einer der Parameter die Prüfung ausfällt.
Da die Fehlerprüfung dazu führt, dass die Route übersprungen wird, ermöglicht der Angeben des otherwise
Wertes ermöglicht, dass der Fehler als Teil der Antwort zurückgegeben wird:
local validator = fm . makeValidator {
{ " name " , minlen = 5 , maxlen = 64 , msg = " Invalid %s format " },
{ " password " , minlen = 5 , maxlen = 128 , msg = " Invalid %s format " },
otherwise = function ( error )
return fm . serveContent ( " signin " , { error = error })
end ,
}
In diesem Fall empfängt der otherwise
Handler die Fehlermeldung (oder eine Tabelle mit Nachrichten, wenn er die unten all
Option übergeben wird), die dann als Vorlageparameter bereitgestellt und an den Client zurückgegeben werden kann.
Eine andere Option besteht darin, die Validator -Funktion direkt in einem Aktionshandler aufzurufen und seine Ergebnisse zurückzugeben:
local validator = fm . makeValidator {
{ " name " , minlen = 5 , maxlen = 64 , msg = " Invalid %s format " },
{ " password " , minlen = 5 , maxlen = 128 , msg = " Invalid %s format " },
}
fm . setRoute ( fm . POST { " /signin " }, function ( r )
local valid , error = validator ( r . params )
if valid then
return fm . serveRedirect ( " / " ) -- status code is optional
else
return fm . serveContent ( " signin " , { error = error })
end
end )
In diesem Beispiel wird der Validator direkt aufgerufen und eine Tabelle ( r.params
) mit allen Parameterwerten übergeben, damit die Validator -Funktion die Werte mit den angegebenen Regeln überprüfen kann.
Die Validator -Funktion true
dann den Signalerfolg oder nil, error
bei der Signalisierung eines Versagens bei der Überprüfung einer der Regeln. Auf diese Weise kann der Validator -Aufruf in ein assert
eingewickelt werden, wenn das Skript sofort einen Fehler zurückgeben muss:
assert ( validator ( r . params )) -- throw an error if validation fails
return fm . serveRedirect ( 307 , " / " ) -- return redirect in other cases
Die folgenden Validator -Überprüfungen sind verfügbar:
minlen
: (Ganzzahl) überprüft die minimale Länge einer Zeichenfolge.maxlen
: (Ganzzahl) überprüft die maximale Länge einer Zeichenfolge.test
: (Funktion) Aufrufe eine Funktion, die einen Parameter übergeben wird und voraussichtlich true
oder nil | false [, error]
zurückgibt nil | false [, error]
.oneof
: ( value | { table of values to be compared against }
) prüft, ob der Parameter mit einem der bereitgestellten Werte übereinstimmt.pattern
: (String) prüft, ob der Parameter mit einem LUA -Musterausdruck übereinstimmt.Zusätzlich zu den Schecks können die Regeln Optionen enthalten:
optional
: (bool) macht einen Parameter optional, wenn er nil
ist. Alle Parameter sind standardmäßig erforderlich. Mit dieser Option können die Regeln übersprungen werden, wenn der Parameter nicht bereitgestellt wird. Alle Regeln werden weiterhin angewendet, wenn der Parameter nicht nil ist.msg
: (String) fügt dafür eine Kundennachricht hinzu, wenn eine seiner Überprüfungen fehlschlägt und Nachrichten von einzelnen Überprüfungen überschreibt. Die Nachricht kann einen Platzhalter ( %s
) enthalten, der durch einen Parameternamen ersetzt wird.The validator itself also accepts several options that modify how the generated errors are returned or handled:
otherwise
: (function) sets an error handler that is called when one of the checks fails. The function receives the error(s) triggered by the checks.all
: (bool) configures the validator to return all errors instead of just the first one. By default only one (first) error is returned as a string, so if all errors are requested, they are returned as a table with each error being a separate item.key
: (bool) configures the validator to return error(s) as values in a hash table (instead of element) where the keys are parameter names. This is useful to pass the table with errors to a template that can then display errors.name
and errors.password
error messages next to their input fields. An action handler receives all incoming HTTP requests filtered for a particular route. Each of the examples shown so far includes an action handler, which is passed as a second parameter to the setRoute
method.
Multiple action handlers can be executed in the course of handling one request and as soon as one handler returns a result that is evaluated as a non- false
value, the route handling process ends. Returning false
or nil
from an action handler continues the processing, which allows implementing some common processing that applies to multiple routes (similar to what is done using "before" filters in other frameworks):
local uroute = " /user/:id "
fm . setRoute ({ uroute .. " /* " , method = { " GET " , " POST " , otherwise = 405 }},
function ( r )
-- retrieve user information based on r.params.id
-- and store in r.user (as one of the options);
-- return error if user is not found
return false -- continue handling
end )
fm . setRoute ( fm . GET ( uroute .. " /view " ), function ( r ) ... end )
fm . setRoute ( fm . GET ( uroute .. " /edit " ), function ( r ) ... end )
fm . setRoute ( fm . POST ( uroute .. " /edit " ), function ( r ) ... end )
In this example, the first route can generate three outcomes:
method
check) is not matched, then the 405 status code is returned.false
, which continues processing with other routes, or fails to retrieve the user and returns an error.In general, an action handler can return any of the following values:
true
: this stops any further processing, sets the headers that have been specified so far, and returns the generated or set response body.false
or nil
: this stops the processing of the current route and proceeds to the next one.Content-Type
is set based on the body content (using a primitive heuristic) if not set explicitly.serve*
methods): this executes the requested method and returns an empty string or true
to signal the end of the processing.true
is returned (and a warning is logged). Normally any processing that results in a Lua error is returned to the client as a server error response (with the 500 status code). To assist with local debugging, the error message includes a stack trace, but only if the request is sent from a loopback or private IP (or if redbean is launched with the -E
command line option).
It may be desirable to return a specific response through multiple layers of function calls, in which case the error may be triggered with a function value instead of a string value. For example, executing error(fm.serve404)
results in returning the 404 status code, which is similar to using return fm.serve404
, but can be executed in a function called from an action handler (and only from inside an action handler).
Here is a more complex example that returns the 404 status code if no record is fetched (assuming there is a table test
with a field id
):
local function AnyOr404(res, err)
if not res then error(err) end
-- serve 404 when no record is returned
if res == db.NONE then error(fm.serve404) end
return res, err
end
fm.setRoute("/", function(r)
local row = AnyOr404(dbm:fetchOne("SELECT id FROM test"))
return row.id
end)
This example uses the serve404
function, but any other serve* method can also be used.
Each action handler accepts a request table that includes the following attributes:
method
: request HTTP method (GET, POST, and others).host
: request host (if provided) or the bind address.serverAddr
: address to which listening server socket is bound.remoteAddr
: client ip4 address encoded as a number. This takes into consideration reverse proxy scenarios. Use formatIp
function to convert to a string representing the address.scheme
: request URL scheme (if any).path
: request URL path that is guaranteed to begin with /
.authority
: request URL with scheme, host, and port present.url
: request URL as an ASCII string with illegal characters percent encoded.body
: request message body (if present) or an empty string.date
: request date as a Unix timestamp.time
: current time as a Unix timestamp with 0.0001s precision.The request table also has several utility functions, as well as headers, cookies, and session tables that allow retrieving request headers, cookies, and session and setting of headers and cookies that are included with the response.
The same request table is given as a parameter to all (matched) action handlers, so it can be used as a mechanism to pass values between those action handlers, as any value assigned as a field in one handler is available in all other action handlers .
The headers
table provides access to the request headers. For example, r.headers["Content-Type"]
returns the value of the Content-Type
header. This form of header access is case-insensitive. A shorter form is also available ( r.headers.ContentType
), but only for registered headers and is case-sensitive with the capitalization preserved.
The request headers can also be set using the same syntax. For example, r.headers.MyHeader = "value"
sets MyHeader: value
response header. As the headers are set at the end of the action handler processing, headers set earlier can also be removed by assigning a nil
value.
Repeatable headers can also be assigned with values separated by commas: r.headers.Allow = "GET, POST"
.
The cookies
table provides access to the request cookies. For example, r.cookies.token
returns the value of the token
cookie.
The cookies can also be set using the same syntax. For example, r.cookies.token = "new value"
sets token
cookie to new value
. If the cookie needs to have its attributes set as well, then the value and the attributes need to be passed as a table: r.cookies.token = {"new value", secure = true, httponly = true}
.
The following cookie attributes are supported:
expires
: sets the maximum lifetime of the cookie as an HTTP-date timestamp. Can be specified as a date in the RFC1123 (string) format or as a UNIX timestamp (number of seconds).maxage
: sets number of seconds until the cookie expires. A zero or negative number expires the cookie immediately. If both expires
and maxage
are set, maxage
has precedence.domain
: sets the host to which the cookie is going to be sent.path
: sets the path that must be present in the request URL, or the client is not going to send the Cookie header.secure
: (bool) requests the cookie to be only send to the server when a request is made with the https: scheme.httponly
: (bool) forbids JavaScript from accessing the cookie.samesite
: ( Strict
, Lax
, or None
) controls whether a cookie is sent with cross-origin requests, providing some protection against cross-site request forgery attacks. Note that httponly
and samesite="Strict"
are set by default; a different set of defaults can be provided using cookieOptions
passed to the run method. Any attributes set with a table overwrite the default , so if Secure
needs to be enabled, make sure to also pass httponly
and samesite
options.
To delete a cookie, set its value to false
: for example, r.cookies.token = false
deletes the value of the token
cookie.
The session
table provides access to the session table that can be used to set or retrieve session values. For example, r.session.counter
returns the counter
value set previously. The session values can also be set using the same syntax. For example, r.session.counter = 2
sets the counter
value to 2
.
The session allows storing of nested values and other Lua values. If the session needs to be removed, it can be set to an empty table or a nil
value. Each session is signed with an application secret, which is assigned a random string by default and can be changed by setting session options.
The following functions are available as both request functions (as fields in the request table) and as library functions:
makePath(route[, parameters])
: creates a path from either a route name or a path string by populating its parameters using values from the parameters table (when provided). The path doesn't need to be just a path component of a URL and can be a full URL as well. Optional parts are removed if they include parameters that are not provided.makeUrl([url,] options)
: creates a URL using the provided value and a set of URL parameters provided in the options
table: scheme, user, pass, host, port, path, and fragment. The url
parameter is optional; the current request URL is used if url
is not specified. Any of the options can be provided or removed (using false
as the value). For example, makeUrl({scheme="https"})
sets the scheme for the current URL to https
.escapeHtml(string)
: escapes HTML entities ( &><"'
) by replacing them with their HTML entity counterparts ( &><"'
).escapePath(path)
: applies URL encoding ( %XX
) escaping path unsafe characters (anything other than -.~_@:!$&'()*+,;=0-9A-Za-z/
).formatHttpDateTime(seconds)
: converts UNIX timestamp (in seconds) to an RFC1123 string ( Mon, 21 Feb 2022 15:37:13 GMT
).Templates provide a simple and convenient way to return a predefined and parametrized content instead of generating it piece by piece.
The included template engine supports mixing an arbitrary text with Lua statements/expressions wrapped into {% %}
tags. All the code in templates uses a regular Lua syntax, so there is no new syntax to learn. There are three ways to include some Lua code:
{% statement %}
: used for Lua statements . For example, {% if true then %}Hello{% end %}
renders Hello
.{%& expression %}
: used for Lua expressions rendered as HTML-safe text. For example, {%& '2 & 2' %}
renders 2 & 2
.{%= expression %}
: used for Lua expressions rendered as-is (without escaping). For example, {%= 2 + 2 %}
renders 4
. Be careful, as HTML is not escaped with {%= }
, this should be used carefully due to the potential for XSS attacks.The template engine provides two main functions to use with templates:
setTemplate(name, text[, parameters])
: registers a template with the provided name and text (and uses parameters
as its default parameters). There are special cases where name
or text
parameters may not be strings, with some of those cases covered in the Loading templates section. parameters
is a table with template parameters as name/value pairs (referenced as variables in the template).render(name, parameters)
: renders a registered template using the parameters
table to set values in the template (with key/value in the table assigned to name/value in the template).There is only one template with a given name, so registering a template with an existing name replaces this previously registered template. This is probably rarely needed, but can be used to overwrite default templates.
Here is an example that renders Hello, World!
to the output buffer:
fm . setTemplate ( " hello " , " Hello, {%& title %}! " )
fm . render ( " hello " , { title = " World " })
Rendering statements using the expression syntax or expressions using the statement syntax is a syntax error that is reported when the template is registered. Function calls can be used with either syntax.
Any template error (syntax or run-time) includes a template name and a line number within the template. For example, calling fm.setTemplate("hello", "Hello, {%& if title then end %}!")
results in throwing hello:1: unexpected symbol near 'if'
error (as it inserts a Lua statement using the expression syntax).
Templates can also be loaded from a file or a directory using the same setTemplate
function, which is described later in the Loading templates section.
There are several aspects worth noting, as they may differ from how templates are processed in other frameworks:
json
and sse
templates are implemented using this approach.Each template accepts parameters that then can be used in its rendering logic. Parameters can be passed in two ways: (1) when the template is registered and (2) when the template is rendered. Passing parameters during registration allows to set default values that are used if no parameter is provided during rendering. Zum Beispiel,
fm . setTemplate ( " hello " , " Hello, {%& title %}! " , { title = " World " })
fm . render ( " hello " ) -- renders `Hello, World!`
fm . render ( " hello " , { title = " All " }) -- renders `Hello, All!`
nil
or false
values are rendered as empty strings without throwing any error, but any operation on a nil
value is likely to result in a Lua error. For example, doing {%& title .. '!' %}
(without title
set) results in attempt to concatenate a nil value (global 'title')
error.
There is no constraint on what values can be passed to a template, so any Lua value can be passed and then used inside a template.
In addition to the values that can be passed to templates, there are two special tables that provide access to cross-template values :
vars
: provides access to values registered with setTemplateVar
, andblock
: provides access to template fragments that can be overwritten by other templates. Any value registered with setTemplateVar
becomes accessible from any template through the vars
table. In the following example, the vars.title
value is set by the earlier setTemplateVar('title', 'World')
call:
fm . setTemplateVar ( ' title ' , ' World ' )
fm . setTemplate ( " hello " , " Hello, {%& vars.title %}! " )
fm . render ( " hello " ) -- renders `Hello, World!`
While undefined values are rendered as empty string by default (which may be convenient in most cases), there are still situations when it is preferrable to not allow undefined values to be silently handled. In this a special template variable ( if-nil
) can be set to handle those cases to throw an error or to log a message. For example, the following code throws an error, as the missing
value is undefined, which triggers if-nil
handler:
fm . setTemplateVar ( ' if-nil ' , function () error " missing value " end )
fm . setTemplate ( " hello " , " Hello, {%& vars.missing %}! " )
fm . render ( " hello " ) -- throws "missing value" error
Templates can be also rendered from other templates by using the render
function, which is available in every template:
fm . setTemplate ( " hello " , " Hello, {%& title %}! " )
fm . setTemplate ( " header " , " <h1>{% render('hello', {title = title}) %}</h1> " )
---- -----------------------------└──────────────────────────────┘----------
fm . render ( " header " , { title = ' World ' }) -- renders `<h1>Hello, World!</h1>`
There are no limits on how templates can be rendered from other templates, but no checks for loops are made either, so having circular references in template rendering (when a template A renders a template B, which in turn renders A again) is going to cause a Lua error.
It's worth noting that the render
function doesn't return the value of the template it renders, but instead puts it directly into the output buffer.
This ability to render templates from other templates allows producing layouts of any complexity. There are two ways to go about it:
To dynamically choose the template to use at render time, the template name itself can be passed as a parameter:
fm . setTemplate ( " hello " , " Hello, {%& title %}! " )
fm . setTemplate ( " bye " , " Bye, {%& title %}! " )
fm . setTemplate ( " header " , " <h1>{% render(content, {title = title}) %}</h1> " )
fm . render ( " header " , { title = ' World ' , content = ' hello ' })
This example renders either <h1>Hello, World!</h1>
or <h1>Bye, World!</h1>
depending on the value of the content
parameter.
Using blocks allows defining template fragments that can (optionally) be overwritten from other templates (usually called "child" or "inherited" templates). The following example demonstrates this approach:
fm . setTemplate ( " header " , [[
<h1>
{% function block.greet() %} -- define a (default) block
Hi
{% end %}
{% block.greet() %}, -- render the block
{%& title %}!
</h1>
]] )
fm . setTemplate ( " hello " , [[
{% function block.greet() %} -- overwrite the `header` block (if any)
Hello
{% end %}
{% render('header', {title=title}) %}!
]] )
fm . setTemplate ( " bye " , [[
{% function block.greet() %} -- overwrite the `header` block (if any)
Bye
{% end %}
{% render('header', {title=title}) %}!
]] )
-- normally only one of the three `render` calls is needed,
-- so all three are shown for illustrative purposes only
fm . render ( " hello " , { title = ' World ' }) -- renders <h1>Hello, World!</h1>
fm . render ( " bye " , { title = ' World ' }) -- renders `<h1>Bye, World!</h1>`
fm . render ( " header " , { title = ' World ' }) -- renders `<h1>Hi, World!</h1>`
In this example the header
template becomes the "layout" and defines the greet
block with Hi
as its content. The block is defined as a function in the block
table with the content it needs to produce. It's followed by a call to the block.greet
function to include its content in the template.
This is important to emphasize, as in addition to defining a block, it also needs to be called from the base/layout template at the point where it is expected to be rendered.
The hello
template also defines block.greet
function with a different content and then renders the header
template. When the header
template is rendered, it uses the content of the block.greet
function as defined in the hello
template. In this way, the child template "redefines" the greet
block with its own content, inserting it into the appropriate place into the parent template.
It works the same way for the bye
and header
templates. There is nothing special about these "block" functions other than the fact that they are defined in the block
table.
This concepts is useful for template composition at any depth. For example, let's define a modal template with a header and a footer with action buttons:
fm . setTemplate ( " modal " , [[
<div class="modal">
<div class="modal-title">
{% function block.modal_title() %}
Details
{% end %}
{% block.modal_title() %}
</div>
<div class="modal-content">
{% block.modal_content() %}
</div>
<div class="modal-actions">
{% function block.modal_actions() %}
<button>Cancel</button>
<button>Save</button>
{% end %}
{% block.modal_actions() %}
</div>
</div>
]] )
Now, in a template that renders the modal, the blocks can be overwritten to customize the content:
fm . setTemplate ( " page " , [[
{% function block.modal_title() %}
Insert photo
{% end %}
{% function block.modal_content() %}
<div class="photo-dropzone">Upload photo here</div>
{% end %}
{% render('modal') %}
]] )
This enables easily building composable layouts and components, such as headers and footers, cards, modals, or anything else that requires the ability to dynamically customize sections in other templates.
Here is an example to illustrate how nested blocks work together:
-- base/layout template
{ % function block . greet () % } -- 1. defines default "greet" block
Hi
{ % end % }
{ % block . greet () % } -- 2. calls "greet" block
-- child template
{ % function block . greet () % } -- 3. defines "greet" block
Hello
{ % end % }
{ % render ( ' base ' ) % } -- 4. renders "base" template
-- grandchild template
{ % function block . greet () % } -- 5. defines "greet" block
Bye
{ % end % }
{ % render ( ' child ' ) % } -- 6. renders "child" template
In this example the "child" template "extends" the base template and any block.greet
content defined in the child template is rendered inside the "base" template (when and where the block.greet()
function is called). The default block.greet
block doesn't need to be defined in the base template, but when it is present (step 1), it sets the content to be rendered (step 2) if the block is not overwritten in a child template and needs to be defined before block.greet
function is called.
Similarly, block.greet
in the child template needs to be defined before (step 3) the base template is rendered (step 4) to have a desired effect.
If one of the templates in the current render tree doesn't define the block, then the later defined block is going to be used. For example, if the grandchild template doesn't define the block in step 5, then the greet
block from the child template is going to be used when the grandchild template is rendered.
If none of the block.greet
functions is defined, then block.greet()
fails (in the base
template). To make the block optional , just check the function before calling. For example, block.greet and block.greet()
.
In those cases where the "overwritten" block may still need to be rendered, it's possible to reference that block directly from the template that defines it, as shown in the following example:
fm . setTemplate ( " header " , [[
<h1>
{% function block.greet() %}
Hi
{% end %}
{% block.greet() %}, {%& title %}!
</h1>
]] )
fm . setTemplate ( " bye " , [[
{% block.header.greet() %},
{% function block.greet() %}
Bye
{% end %}
{% render('header', {title=title}) %}!
]] )
fm . render ( " bye " , { title = ' World ' }) -- renders `<h1>Hi, Bye, World!</h1>`
In this case, {% block.header.greet() %}
in the bye
template renders the greet
block from the header
template. This only works with the templates that are currently being rendered and is intended to simulate the "super" reference (albeit with explicit template references). The general syntax of this call is block.<templatename>.<blockname>()
.
As blocks are simply regular Lua functions, there are no restrictions on how blocks can be nested into other blocks or how blocks are defined relative to template fragments or other Lua statements included in the templates.
In addition to registering templates from a string, the templates can be loaded and registered from a file or a directory using the same setTemplate
function, but passing a table with the directory and a list of mappings from file extensions to template types to load. For example, calling fm.setTemplate({"/views/", tmpl = "fmt"})
loads all *.tmpl
files from the /views/
directory (and its subdirectories) and registers each of them as the fmt
template, which is the default template type. Only those files that match the extension are loaded and multiple extension mappings can be specified in one call.
Each loaded template gets its name based on the full path starting from the specified directory: the file /views/hello.tmpl
is registered as a template with the name "hello" (without the extension), whereas the file /views/greet/bye.tmpl
is registered as a template with the name "greet/bye" (and this is the exact name to use to load the template).
There are two caveats worth mentioning, both related to the directory processing. The first one is related to the trailing slash in the directory name passed to setTemplate
. It's recommended to provide one, as the specified value is used as a prefix, so if /view
is specified, it's going to match both /view/
and /views/
directories (if present), which may or may not be the intended result .
The second caveat is related to how external directories are used during template search. Since redbean allows access to external directories when configured using the -D
option or directory
option (see Running application for details), there may be multiple locations for the same template available. The search for the template follows these steps:
setTemplate
call); This allows to have a working copy of a template to be modified and processed from the file system (assuming the -D
option is used) during development without modifying its copy in the archive.
Even though using fm.render
is sufficient to get a template rendered, for consistency with other serve* functions, the library provides the serveContent
function, which is similar to fm.render
, but allows the action handler to complete after serving the content:
fm . setTemplate ( " hello " , " Hello, {%& name %} " )
fm . setRoute ( " /hello/:name " , function ( r )
return fm . serveContent ( " hello " , { name = r . params . name })
end )
There is also one subtle difference between render
and serveContent
methods that comes into play when serving static templates . It may be tempting to directly render a static template in response to a route with something like this:
fm . setTemplate ( " hello " , " Hello, World! " )
-- option 1:
fm . setRoute ( " /hello " , fm . render ( " hello " ))
---- ---------------------└─────┘-------- not going to work
-- option 2:
fm . setRoute ( " /hello " , fm . serveContent ( " hello " ))
---- ---------------------└───────────┘-- works as expected
The first approach is not going to work, as the call to fm.render
is going to be made when setRoute
is called (and the route is only being set up) and not when a request is being handled. When the serveContent
method is using (the second option), it's implemented in a way that delays the processing until the request is handled, thus avoiding the issue. If the template content depends on some values in the request, then the serverContent
call has to be wrapped into a function to accept and pass those variables (as shown in the earlier /hello/:name
route example).
Most of the time, the library configuration is focused on handling of incoming requests, but in some cases it may be desirable to trigger and handle internal events. The library supports job scheduling using cron syntax, with configured jobs executed at the scheduled time (as long as the redbean instance is running). A new schedule can be registered using the setSchedule
method:
---- ----------- ┌─────────── minute (0-59)
---- ----------- │ ┌───────── hour (0-23)
---- ----------- │ │ ┌─────── day of the month (1-31)
---- ----------- │ │ │ ┌───── month (1-12 or Jan-Dec)
---- ----------- │ │ │ │ ┌─── day of the week (0-6 or Sun-Mon)
---- ----------- │ │ │ │ │ --
---- ----------- │ │ │ │ │ --
fm . setSchedule ( " * * * * * " , function () fm . logInfo ( " every minute " ) end )
All the standard and some non-standard cron expressions are supported:
*
: describes any values in the allowed range.,
: uses to form a list of items, for example, 1,2,3
.-
: creates an (inclusive) range; for example, 1-3
is equivalent to 1,2,3
. Open ranges are allowed as well, so -3
is equivalent to 1-3
for months and 0-3
for minutes and hours./
: describes a step for ranges. It selects a subset of the values in the range, using the step value; for example, 2-9/3
is equivalent to 2,5,8
(it starts with 2, then adds a step value to get 5 and 8). Non-numeric values are supported for months ( Jan-Dec
) and days of week ( Sun-Mon
) in any capitalization. Using 7
for Sun
is supported too.
By default all functions are executed in a separate (forked) process. If the execution within the same process is needed, then setSchedule
can be passed a third parameter (a table) to set sameProc
value as one of the options: {sameProc = true}
.
Some of the caveats to be aware of:
OnServerHeartbeat
hook, so a version of Redbean that provides that (v2.0.16+) should be used.and
(instead of an or
), so when both are specified, the job is executed when both are satisfied (and not when both or either are specified). In other words, * * 13 * Fri
is only valid on Friday the 13th and not on any Friday. If the or
behavior is needed, then the schedule can be split into two to handle each condition separately.sameProc = true
option to avoid forking.Sun
available on both ends (as 0 or 7), so it's better to use closed ranges in this case to avoid ambiguity.6-100
for months is corrected to 6-12
.Each action handler generates some sort of response to send back to the client. In addition to strings, the application can return the following results:
serveResponse
),serveContent
),serveRedirect
),serveAsset
),serveError
),serveIndex
), andservePath
). Each of these methods can be used as the return value from an action handler. serveAsset
, servePath
, and serveIndex
methods can also be used as action handlers directly:
fm . setRoute ( " /static/* " , fm . serveAsset )
fm . setRoute ( " /blog/ " , fm . serveIndex ( " /new-blog/ " ))
The first route configures all existing assets to be served from /static/*
location; the second route configures /blog/
URL to return the index ( index.lua
or index.html
resource) from /new-blog/
directory.
serveResponse(status[, headers][, body])
: sends an HTTP response using provided status
, headers
, and body
values. headers
is an optional table populated with HTTP header name/value pairs. If provided, this set of headers removes all other headers set earlier during the handling of the same request. Similar to the headers set using the request.headers
field, the names are case-insensitive , but provided aliases for header names with dashes are case-sensitive : {ContentType = "foo"}
is an alternative form for {["Content-Type"] = "foo"}
. body
is an optional string.
Consider the following example:
return fm . serveResponse ( 413 , " Payload Too Large " )
This returns the 413 status code and sets the body of the returned message to Payload Too Large
(with the header table not specified).
If only the status code needs to be set, the library provides a short form using the serve###
syntax:
return fm . serve413
It can also be used as the action handler itself:
fm . setRoute ( fm . PUT " /status " , fm . serve402 )
serveContent(name, parameters)
renders a template using provided parameters. name
is a string that names the template (as set by a setTemplate
call) and parameters
is a table with template parameters (referenced as variables in the template).
Fullmoon's function makeStorage
is a way to connect to, and use a SQLite3
database. makeStorage
returns a database management table which contains a rich set of functions to use with the connected database.
The run
method executes the configured application. By default the server is launched listening on localhost and port 8080. Both of these values can be changed by passing addr
and port
options:
fm . run ({ addr = " localhost " , port = 8080 })
The following options are supported; the default values are shown in parentheses and options marked with mult
can set multiple values by passing a table:
addr
: sets the address to listen on (mult)brand
: sets the Server
header value ( "redbean/v# fullmoon/v#"
)cache
: configures Cache-Control
and Expires
headers (in seconds) for all static assets served. A negative value disables the headers. Zero value means no cache.certificate
: sets the TLS certificate value (mult)directory
: sets local directory to serve assets from in addition to serving them from the archive within the executable itself (mult)headers
: sets default headers added to each response by passing a table with HTTP header name/value pairslogMessages
: enables logging of response headerslogBodies
: enables logging of request bodies (POST/PUT/etc.)logPath
: sets the log file path on the local file systempidPath
: sets the pid file path on the local file systemport
: sets the port number to listen on (8080)privateKey
: sets the TLS private key value (mult)sslTicketLifetime
: sets the duration (in seconds) of the ssl ticket (86400)trustedIp
: configures IP address to trust (mult). This option accepts two values (IP and CIDR values), so they need to be passed as a table within a table specifying multiple parameters: trustedIp = {{ParseIp("103.31.4.0"), 22}, {ParseIp("104.16.0.0"), 13}}
tokenBucket
: enables DDOS protection. This option accepts zero to 5 values (passed as a table within a table); an empty table can be passed to use default values: tokenBucket = {{}}
Each option can accept a simple value ( port = 80
), a list of values ( port = {8080, 8081}
) or a list of parameters. Since both the list of values and the list of parameters are passed as tables, the list of values takes precedence, so if a list of parameters needs to be passed to an option (like trustedIp
), it has to be wrapped into a table: trustedIp = {{ParseIp("103.31.4.0"), 22}}
. If only one parameter needs to be passed, then both trustedIp = {ParseIp("103.31.4.0")}
and trustedIp = ParseIp("103.31.4.0")
can work.
The key
and certificate
string values can be populated using the getAsset
method that can access both assets packaged within the webserver archive and those stored in the file system.
There are also default cookie and session options that can be assigned using cookieOptions
and sessionOptions
tables described below.
cookieOptions
sets default options for all cookie values assigned using request.cookie.name = value
syntax ( {httponly=true, samesite="Strict"}
). It is still possible to overwrite default values using table assignment: request.cookie.name = {value, secure=false}
.
sessionOptions
sets default options for the session value assigned using request.session.attribute = value
syntax ( {name="fullmoon_session", hash="SHA256", secret=true, format="lua"}
). If the secret
value is set to true
, then a random key is assigned each time the server is started ; if verbose logging is enabled (by either adding -v
option for Redbean or by using fm.setLogLevel(fm.kLogVerbose)
call), then a message is logged explaining how to apply the current random value to make it permanent.
Setting this value to false
or an empty string applies hashing without a secret key.
The results shown are from runs in the same environment and on the same hardware as the published redbean benchmark (thanks to @jart for executing the tests!). Even though these tests are using pre-1.5 version of redbean and 0.10 version of Fullmoon, the current versions of redbean/Fullmoon are expected to deliver similar performance.
The tests are using exactly the same code that is shown in the introduction with one small change: using {%= name %}
instead of {%& name %}
in the template, which skips HTML escaping. This code demonstrates routing, parameter handling and template processing.
$ wrk -t 12 -c 120 http://127.0.0.1:8080/user/paul Running 10s test @ http://127.0.0.1:8080/user/paul 12 threads and 120 connections Thread Stats Avg Stdev Max +/- Stdev Latency 312.06us 4.39ms 207.16ms 99.85% Req/Sec 32.48k 6.69k 71.37k 82.25% 3913229 requests in 10.10s, 783.71MB read Requests/sec: 387477.76 Transfer/sec: 77.60MB
The following test is using the same configuration, but redbean is compiled with MODE=optlinux
option:
$ wrk -t 12 -c 120 http://127.0.0.1:8080/user/paul Running 10s test @ http://127.0.0.1:8080/user/paul 12 threads and 120 connections Thread Stats Avg Stdev Max +/- Stdev Latency 346.31us 5.13ms 207.31ms 99.81% Req/Sec 36.18k 6.70k 90.47k 80.92% 4359909 requests in 10.10s, 0.85GB read Requests/sec: 431684.80 Transfer/sec: 86.45MB
The following two tests demonstrate the latency of the request handling by Fullmoon and by redbean serving a static asset (no concurrency):
$ wrk -t 1 -c 1 http://127.0.0.1:8080/user/paul Running 10s test @ http://127.0.0.1:8080/user/paul 1 threads and 1 connections Thread Stats Avg Stdev Max +/- Stdev Latency 15.75us 7.64us 272.00us 93.32% Req/Sec 65.54k 589.15 66.58k 74.26% 658897 requests in 10.10s, 131.96MB read Requests/sec: 65241.45 Transfer/sec: 13.07MB
The following are the results from redbean itself on static compressed assets:
$ wrk -H 'Accept-Encoding: gzip' -t 1 -c 1 htt://10.10.10.124:8080/tool/net/demo/index.html Running 10s test @ htt://10.10.10.124:8080/tool/net/demo/index.html 1 threads and 1 connections Thread Stats Avg Stdev Max +/- Stdev Latency 7.40us 1.95us 252.00us 97.05% Req/Sec 129.66k 3.20k 135.98k 64.36% 1302424 requests in 10.10s, 1.01GB read Requests/sec: 128963.75 Transfer/sec: 102.70MB
Berwyn Hoyt included Redbean results in his lua server benchmark results, which shows redbean outperforming a comparable nginx/openresty implementation.
Highly experimental with everything being subject to change.
The core components are more stable and have been rarely updated since v0.3. Usually, the documented interfaces are much more stable than undocumented ones. Those commits that modified some of the interfaces are marked with COMPAT
label, so can be easily identified to review for any compatibility issues.
Some of the obsolete methods are still present (with a warning logged when used) to be removed later.
Paul Kulchenko ([email protected])
See LICENSE.