Fullmoon是基于RedBean的快速而简约的Web框架,该框架是一种便携式,单文件分发的Web服务器。
开发和分发所需的一切都在一个没有外部依赖项的单个文件中,并且在Windows,Linux或MacOS上包装了RedBean运行后。以下是全月应用程序的完整示例:
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 ()
用RedBean打包后,可以使用./redbean.com
启动它,该服务器启动了将“ Hello,World”返回到http(S)请求发送到http:// localhost:8080/hello/hello/world的服务器。
RedBean是一款具有独特和强大质量的单文件分发跨平台Web服务器。尽管有几个基于LUA的Web框架(Lapis,Lor,Sailor,Pegasus等),但它们都没有与Redbean集成(尽管有实验框架ANPAN)。
Fullmoon是一个轻巧,简约的Web框架,它是从展示RedBean提供的所有功能的角度来写入的,它通过以最简单和最有效的方式扩展和增强它们来扩展它们。它运行迅速,并带有包括电池(路线,模板,JSON Generation等)。
Fullmoon遵循LUA哲学,并提供了一组最少的工具,可以根据需要组合并用作建立基础的基础。
fork
, socket
,共享内存等通过运行以下命令下载RedBean的副本(如果在Windows上运行这些命令,则跳过第二个命令):
curl -o redbean.com https://redbean.dev/redbean-2.2.com
chmod +x redbean.com
最新版本号可以通过以下请求检索:
curl https://redbean.dev/latest.txt
另一个选择是通过遵循源构建的说明来构建RedBean。
fullmoon.lua
到.lua/
目录.init.lua
的文件(例如,描述中显示的LUA代码)。另一个选项是将应用程序代码放入单独的文件(例如.lua/myapp.lua
)中,并添加require "myapp"
到.init.lua
。这就是所有包含的示例的方式。
zip redbean.com .init.lua .lua/fullmoon.lua
如果将应用程序代码存储在单独的LUA文件中,如上所述,请确保将其放在.lua/
Directory中,并将其放在该文件中。
./redbean.com
如果此命令是在Linux上执行的,并引发了有关未找到解释器的错误,则应通过运行以下命令来修复该命令(尽管请注意,它可能无法在系统重新启动时幸存下来):
sudo sh -c " echo ':APE:M::MZqFpD::/bin/sh:' >/proc/sys/fs/binfmt_misc/register "
如果此命令在使用RedBean 2.x时会在WSL或葡萄酒上产生令人困惑的错误,则可以通过禁用BINFMT_MISC来确定它们:
sudo sh -c ' echo -1 >/proc/sys/fs/binfmt_misc/status '
启动指向http:// localhost:8080/hello/world的浏览器,它应该返回“ Hello,World”(假设应用程序使用了引言中显示的代码或使用段部分中的代码)。
最简单的示例需要(1)加载模块,(2)配置一个路线,(3)运行应用程序:
local fm = require " fullmoon " -- (1)
fm . setRoute ( " /hello " , function ( r ) return " Hello, world " end ) -- (2)
fm . run () -- (3)
此应用程序对返回的“ Hello,World”内容(和200个状态代码)的任何请求/hello
URL响应,并使用所有其他请求的404状态代码响应。
setRoute(route[, action])
:注册路线。如果route
是字符串,则将其用作路由表达式,以将请求路径与之比较。如果是一个表,则其元素是用作路由的字符串,其哈希值是对路由相反的条件。如果第二个参数是一个函数,则如果满足所有条件,则执行它。如果是字符串,则将其用作路由表达式,并将请求处理好像在指定的路由上发送(充当内部重定向)一样。如果不满足任何条件,则将检查下一个路线。路由表达式可以具有多个参数和可选零件。操作处理程序接受请求表,该表提供了对请求和路由参数的访问,以及标题,cookie和会话。
setTemplate(name, template[, parameters])
:注册一个带有指定名称或目录中的模板集的模板。如果template
是字符串,则将其编译到模板处理程序中。如果是一个函数,则在请求模板的渲染时将其存储并调用。如果是表,则其第一个元素是模板或函数,其余的用作选项。例如,将ContentType
指定为一个选项之一,为生成的内容设置了Content-Type
标题。默认情况下提供了几个模板( 500
, json
等),并且可以被覆盖。 parameters
是一个表,具有存储为名称/值对的模板参数(在模板中称为变量)。
serveResponse(status[, headers][, body])
:使用提供的status
, headers
和body
值发送HTTP响应。 headers
是一个可选的表,填充了HTTP标头名称/值对。如果提供,这组标头将在处理相同请求期间更早设置的所有其他标头。标题名称是不敏感的,但是提供带破折号的标题名称的别名是案例敏感的: {ContentType = "foo"}
是{["Content-Type"] = "foo"}
的替代形式。 body
是一个可选的字符串。
serveContent(name, parameters)
:使用提供的参数呈现模板。 name
是一个字符串,该字符串名称模板(如setTemplate
调用设置),并且parameters
是一个表格,该表具有存储为名称/值对的模板参数(在模板中称为变量)。
run([options])
:使用配置的路由运行服务器。默认情况下,服务器在LocalHost和port 8080上听。可以通过在options
表中设置addr
和port
值来更改这些值。
运行示例需要在.init.lua
文件中包含一个require
语句,该文件将模块加载每个示例代码,因此对于showcase.lua
, .init.lua
中实现的示例示例,包括以下内容:
-- 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`
展示示例展示了几个全月功能:
serveAsset
)serveRedirect
)需要将以下文件添加到RedBean可执行/存档:
.init.lua-需要“展示” .lua/fullmoon.lua .lua/showcase.lua
TechEmpower示例使用Fullmoon和内存中的SQLITE数据库实现了Web框架基准测试的各种测试类型。
此示例演示了几个满月/redbean功能:
需要将以下文件添加到RedBean可执行/存档:
.init.lua-要求“ TechBench” .lua/fullmoon.lua .lua/techbench.lua
HTMX板示例演示了一个简单的应用程序,该应用程序生成了使用HTMX库传递给客户端的HTML片段。
此示例演示了几个满月/redbean功能:
需要将以下文件添加到RedBean可执行/存档:
.init.lua-要求“ htmxboard” .lua/fullmoon.lua .lua/htmxboard.lua 资产/样式 tmpl/* - 示例/htmxboard/tmpl目录的所有文件
注意1:由于所有数据都存储在内存中,因此此示例是在单次处理模式下执行的。
注2:此示例从外部资源中检索HTMX,Hyperscript和可排序的库,但是这些库也可以存储为本地资产,从而提供了完全自给自足的便携式分发包。
HTMX SSE示例演示了一种可以将服务器式事件(SSE)传输到客户端的方法(使用HTMX库及其SSE扩展名显示结果)。
此示例演示了几个满月/redbean功能:
streamContent
)需要将以下文件添加到RedBean可执行/存档:
.init.lua-要求“ htmxsse” .lua/fullmoon.lua .lua/htmxse.lua
每个满月应用程序都遵循相同的基本流,并具有五个主要组成部分:
让我们看一下从请求路由开始的每个组件。
满月使用相同的过程处理每个HTTP请求:
false
或nil
从操作处理程序中返回(并继续该过程),则可以提供响应(否则)通常,路由定义将请求URL(以及一组条件)绑定到操作处理程序(这是常规LUA函数)。每个条件均以与路由定义相匹配的每个URL的随机顺序检查。一旦任何条件失败,路由处理就会中止,并在一个例外检查下一个路线:任何条件都可以设置otherwise
值,该值触发了使用指定的状态代码的响应。
如果没有路由与请求匹配,则会触发默认的404处理,可以通过注册自定义404模板( fm.setTemplate("404", "My 404 page...")
)来自定义。
每条路线都采用完全匹配的路径,因此路由"/hello"
匹配/hello
和不匹配/hell
, /hello-world
或/hello/world
的请求。下面的路线以“你好,世界!”做出回应。对于所有针对/hello
路径的请求,并返回所有其他请求的404。
fm . setRoute ( " /hello " , function ( r ) return " Hello, World! " end )
要匹配/hello
只是其中的一部分的路径,可以使用可选参数和SPLAT。
除固定路线外,任何路径还可以包括参数的占位符,这些参数由A :
立即识别为参数名称:
fm . setRoute ( " /hello/:name " ,
function ( r ) return " Hello, " .. ( r . params . name ) end )
每个参数都匹配一个或多个字符以外/
字符,因此"/hello/:name"
匹配/hello/alice
/hello/bob
, /hello/123
/hello/bob/and/alice
(由于不匹配的前向斜线)或/hello/
(因为要匹配的片段的长度为零)。
参数名称只能包括字母数字字符和_
。
可以使用请求表及其params
表访问参数,以便可以使用r.params.name
从早期示例中获取name
参数的值。
任何指定的路由片段或参数都可以通过将其包裹到括号中来声明为可选:
fm . setRoute ( " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
在上面的示例中, /hello
和/hello/Bob
都被接受,但 /Hello /not /hello/
,因为Tailding Slash是可选片段的一部分,并且:name
仍然期望一个或多个字符。
任何无与伦比的可选参数都会false
以为其值,因此在上面的“你好,世界!”的情况下。返回/hello
请求URL。
可以指定多个可选参数,并且可以嵌套可选的片段,因此"/posts(/:pid/comments(/:cid))"
和"/posts(/:pid)/comments(/:cid)"
是有效的路由值。
还有另一种称为splat的参数,它写为*
,匹配零或更多字符,包括前向斜杠( /
)。 SPLAT还存储在splat
名称下的params
表中。例如,路由"/download/*"
匹配/download/my/file.zip
和splat获取my/file.zip
的值。如果在同一路线中需要多个夹子,则可以为类似于其他参数的名称分配: /download/*path/*fname.zip
(虽然可以使用/download/*path/:fname.zip
可以实现相同的结果,作为第一个SPLAT捕获除文件名之外的所有路径部分)。
所有参数(包括SPLAT)都可以出现在路径的任何部分中,并且可以被其他文本包围,这些文本需要完全匹配。这意味着路由"/download/*/:name.:ext"
匹配/download/my/path/file.zip
和params.name
get file
, params.ext
get zip
和params.splat
获取my/path
值。
使用SPLAT的另一个原因是允许多个路由具有相同的路径在系统中注册。当前的实现覆盖具有相同名称的路由,并避免可以使用指定的SPLAT来创建唯一的路径。例如,
fm . setRoute ( " /*dosomething1 " , function ( r ) return " something 1 " end )
fm . setRoute ( " /*dosomething2 " , function ( r ) return " something 2 " end )
当需要在操作处理程序中检查一组条件时,可以在情况下使用此情况,虽然可以将这两种路线组合到一条路线时,有时会更干净以使它们分开。
参数的默认值是一个或多个长度的所有字符( / /
)。要指定不同的有效字符集,可以在变量名称的末尾添加;例如,使用:id[%d]
而不是:id
更改参数以匹配数字。
fm . setRoute ( " /hello(/:id[%d]) " ,
function ( r ) return " Hello, " .. ( r . params . id or " World! " ) end )
支持以下LUA字符类: %w
, %d
, %a
, %l
, %u
和%x
;任何标点符号(包括%
和]
)也可以通过%
逃脱。不支持负类(用LUA AS %W
编写),但支持不合时宜的语法,因此[^%d]
匹配一个不包含任何数字的参数。
请注意,重复的数量无法更改(因此:id[%d]*
不是接受零或摩尔数字的有效方法),因为仅允许集合,并且值仍然接受一个或多个字符。如果需要更灵活地描述可接受的格式,则可以使用自定义验证器来扩展匹配逻辑。
可以使用传递给每个操作处理程序的request
表中的params
表以与路径参数表的方式访问查询和表单参数。请注意,如果参数和查询/表单名称之间存在冲突,则参数名称优先。
有一种特殊情况可能会导致返回的表而不是字符串值:如果查询/表单参数名称在[]
中结束,则所有匹配结果(一个或多个)作为表返回。例如,对于查询字符串a[]=10&a[]&a[]=12&a[]=
params["a[]"]
的值为{10, false, 12, ""}
。
由于编写这些参数名称可能需要几个括号, params.a
可以用作params["a[]"]
的快捷方式,两种表单都返回同一表。
当要求时,也可以处理多个参数,并且可以使用参数表以使用params
表以相同的方式访问。例如,可以使用params.simple
和params.more
从带有multipart/form-data
Content类型的消息中检索具有名称simple
且more
的参数。
由于某些Multipart内容可能包括这些标题内的其他标头和参数,因此可以使用params
表的multipart
字段访问它们:
fm . setRoute ({ " /hello " , simple = " value " }, function ( r )
return " Show " .. r . params . simple .. " " .. r . params . multipart . more . data )
end )
multipart
表包含多部分消息的所有部分(因此可以在使用ipairs
上迭代),但它还允许使用参数名称( params.multipart.more
)访问。每个元素也是一个包含以下字段的表:
nil
如果不是。nil
如果不是。该多部分处理会消耗任何多部分子类型并处理递归多部分消息。它还将匹配start
参数匹配的Content-ID
值插入到第一个位置。
尽管所有较早的示例显示了一条路线,但在实际应用中很少情况。当存在多个路线时,始终按照注册的顺序进行评估。
当一个setRoute
呼叫具有相同的条件集并共享相同的操作处理程序时,也可以设置多个路线:
fm . setRoute ({ " /route1 " , " /route2 " }, handler )
这相当于两个单独设置每个路由的两个呼叫:
fm . setRoute ( " /route1 " , handler )
fm . setRoute ( " /route2 " , handler )
鉴于按照设置的顺序评估路线,需要首先设置更多选择性的路线,否则它们可能没有机会得到评估:
fm . setRoute ( " /user/bob " , handlerBob )
fm . setRoute ( " /user/:name " , handlerName )
如果以相反的顺序设置路线,则只要"/user/:name"
操作处理程序返回一些非false
结果,就永远不会检查/user/bob
。
如前所述,如果均无匹配的路线,则返回具有404状态代码的响应。在某些情况下,这是不可取的;例如,当应用程序包含LUA脚本以处理未明确注册为路由的请求时。在那些情况下,可以添加一条接收路线,以实现默认的redbean处理(SPLAT参数的名称仅用于将此路线限制在其他地方可以使用的其他/*
路由上):
fm . setRoute ( " /*catchall " , fm . servePath )
每个路由都可以提供一个可选名称,该名称在需要基于特定参数值生成时引用该路由时很有用。提供的makePath
功能接受路由名称或路由URL本身以及参数表,并返回带有填充参数占位符的路径:
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
如果两个路由使用相同的名称,则该名称与上次注册的路由相关联,但仍然存在这两个路线。
路由名称也可以与仅用于URL生成的外部/静态路由一起使用。
如果路由仅用于路径生成,那么它甚至不需要一个路由处理程序:
fm . setRoute ({ " https://youtu.be/:videoid " , routeName = " youtube " })
fm . makePath ( " youtube " , { videoid = " abc " }) -- > https://youtu.be/abc
在路线匹配过程中,没有任何动作处理程序的路线会跳过。
内部路由允许将一组URL重定向到另一组URL。目标URL可以指向静态资源或.lua
脚本。例如,如果需要将一个位置/new-blog/
请求重定向到另一个/blog/
,则只要存在目标资源:
fm . setRoute ( " /blog/* " , " /new-blog/* " )
只要存在/new-blog/post1
Asset,此路线接受/blog/post1
的请求,并提供/new-blog/post1
POST1作为其响应。如果资产不存在,则将检查下一条路线。同样,使用fm.setRoute("/static/*", "/*")
导致对/static/help.txt
的请求提供资源/help.txt
。
这两个URL都可以包含填充的参数,如果解决了:
fm . setRoute ( " /blog/:file " , " /new-blog/:file.html " ) -- <<-- serve "nice" URLs
fm . setRoute ( " /new-blog/:file.html " , fm . serveAsset ) -- <<-- serve original URLs
此示例解决了服务“ HTML”版本的“ NICE” URL。请注意,这不会通过返回3xx
状态代码来触发客户端重定向,而是内部处理重新路由。还要注意,要使用“原始” URL需要第二个规则,因为它们不是由第一个规则处理的,因为如果请求是/blog/mylink.html
,则重定向的URL为/new-blog/mylink.html.html
,不可能存在,因此该路线被跳过并检查下一个路线。如果还需要处理路径分离器,则可以使用*path
代替:file
,因为*
允许路径分离器。
如果应用程序需要根据请求属性的特定值(例如一种方法)执行不同的函数,则该库提供了两个主要选项:(1)检查属性值一个操作处理程序(例如,使用request.method == "GET"
检查)和(2)添加一个条件,该条件过滤了请求,以便仅使用指定属性值到达操作处理程序的请求。本节更详细地描述了第二个选项。
默认情况下,每条注册的路由均响应所有HTTP方法(获取,put,发布等),但是可以配置每个路由以仅响应特定的HTTP方法:
fm . setRoute ( fm . GET " /hello(/:name) " ,
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
在这种情况下,语法fm.GET"/hello(/:name)"
配置仅接受GET
请求的路由。该语法等同于通过路由和任何其他过滤条件的表格通过表格:
fm . setRoute ({ " /hello(/:name) " , method = " GET " },
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
如果需要指定多个方法,则可以传递具有方法列表的表,而不是一个字符串值:
fm . setRoute ({ " /hello(/:name) " , method = { " GET " , " POST " }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
每条允许GET
请求的路线(隐式)允许HEAD
请求,并且该请求是通过返回所有标题而无需发送身体本身来处理的。如果由于某种原因不需要此隐式处理,则将HEAD = false
添加到方法表(如method = {"GET", "POST", HEAD = false}
)。
请注意,使用非匹配方法的请求不会被拒绝,而是通过其他路线检查并触发如果不匹配的404状态代码(一个例外)。
除了method
外,还可以使用host
, clientAddr
, serverAddr
, scheme
,请求标头和参数应用其他条件。例如,指定name = "Bob"
作为条件之一,可确保为“ bob”的name
参数的值,以便将其调用为“ bob”。
可以使用标题名称作为密钥检查任何请求标头,因此,如果Content-Type
标头的值为multipart/form-data
则可以满足ContentType = "multipart/form-data"
。请注意,标题值可能包括其他元素(作为Content-Type
值的一部分)的其他元素(边界或字符集),并且仅比较实际的媒体类型。
由于标题,参数和属性的名称可以重叠,因此按以下顺序检查它们:
ContentType
,method
, port
, host
等),还首先检查了Host
标头(尽管是一个单词),因此,基于标题Host
引用Host
过滤器,同时根据属性host
引用host
过滤器。
字符串值不是在条件路由中使用的唯一值。如果一个以上的值是可以接受的,则通过表可以提供可接受的值列表。例如,如果Bob
和Alice
是可接受的值,则name = {Bob = true, Alice = true}
将其表示为条件。
表中传递的两个特殊值允许应用正则验证或模式验证:
regex
:接受具有正则表达式的字符串。例如, name = {regex = "^(Bob|Alice)$"}
结果与本节前面显示的哈希检查相同pattern
:接受带有LUA模式表达式的字符串。例如, name = {pattern = "^%u%l+$"}
接受以大写字符开始的值,然后是一个或多个小写字符。这两项检查可以与表格存在检查结合: name = {Bob = true, regex = "^Alice$"}
接受Bob
和Alice
值。如果第一个表格检查失败,则返回regex
则pattern
的结果。
自定义验证器的最后类型是函数。提供的功能接收到验证的值,其结果被评估为false
或true
。例如,通过id = tonumber
确保id
值为数字。作为另一个示例, clientAddr = fm.isLoopbackIp
确保客户端地址是环回IP地址。
fm . setRoute ({ " /local-only " , clientAddr = fm . isLoopbackIp },
function ( r ) return " Local content " end )
由于可以动态生成验证器函数,因此这也有效:
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 )
重要的是要记住,验证器函数实际上返回了在应用支票请求期间调用的函数。在上一个示例中,返回的函数接受标头值,并将其与创建期间通过的限制进行比较。
在某些情况下,不满足条件是一个足够的理由,可以在不检查其他路线的情况下返回对客户的响应。在这样的情况下,将otherwise
设置为一个数字或功能将返回具有指定状态的响应或函数结果:
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 )
在此示例中,路由引擎匹配路由,然后验证两个条件,将方法值与POST
和Content-Length
标头的值与isLessThan
函数的结果进行了比较。如果其中一个条件不匹配,则在其余响应的其余部分中返回了由otherwise
值指定的状态代码。
如果otherwise
条件只需要应用于ContentLength
检查,则可以将otherwise
值以及验证器函数以及与ContentLength
Check关联的表移动:
fm . setRoute ( fm . POST { " /upload " ,
ContentLength = { isLessThan ( 100000 ), otherwise = 413 }
}, function ( r ) ... handle the upload ... end )
最后两个示例之间的区别在于,在此示例中,只有ContentLength
检查失败触发了413响应(并且所有其他方法都落在其他路线上),而在上一个方法中, method
和ContentLength
检查失败触发了相同的413响应。
请注意,当检查值为nil
,对表的检查被认为是有效的,并且该路由被接受。例如,如果params.name
的值为nil
,则针对字符串( name = "Bo"
)的可选参数的检查失败,但是如果对表进行了相同的检查( name = {Bo=true, Mo=true}
),包括正则拨号/模式检查。如果这不是可取的,则可以自定义验证器函数明确检查预期值。
考虑以下示例:
fm . setRoute ({ " /hello(/:name) " ,
method = { " GET " , " POST " , otherwise = 405 }},
function ( r ) return " Hello, " .. ( r . params . name or " World! " ) end )
在这种情况下,如果使用PUT
方法访问此端点,则不再检查其他路由(因为不满足method
条件),而是返回405状态代码,则以otherwise
的价值配置为配置。如其他地方的记录,此路线也接受了HEAD
请求(即使未列出),因为接受了GET
请求。
当返回405(不良方法)状态代码并且未设置Allow
头时,将其设置为路由允许的方法列表。在上面的情况下,它设置为GET, POST, HEAD, OPTIONS
值,因为这些方法是此配置允许的方法。如果otherwise
值为函数(而不是数字),则返回适当的结果并设置Allow
标头是此功能的责任。
otherwise
值也可以将其设置为一个函数,该函数提供的灵活性不仅仅是设置状态代码。例如,设置otherwise = fm.serveResponse(413, "Payload Too Large")
会触发使用指定的状态代码和消息的响应。
处理表单验证通常需要指定相同参数的一组条件,并且在不满足条件时可能需要返回的自定义错误消息,并且由makeValidator
函数返回的特殊验证器提供:
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 )
在此示例中,将验证器配置为检查两个参数:“名称”和“密码” - 最小长度和最大长度,并在其中一个参数失败时返回消息。
由于失败的检查会导致要跳过的路由,因此提供otherwise
值允许返回错误作为响应的一部分:
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 ,
}
在这种情况下, otherwise
处理程序会收到错误消息(或带有消息(通过传递下面涵盖的all
选项)的表格),然后可以作为模板参数提供并返回给客户端。
另一个选项是直接在操作处理程序中调用验证器函数并返回其结果:
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 )
在此示例中,验证器被直接调用,并通过所有参数值传递一个表( r.params
),以允许验证器函数根据指定的规则检查值。
然后,验证器函数true
返回到信号成功或nil, error
以发出未能检查其中一个规则的错误。如果脚本需要立即返回错误,则可以将验证器调用包装到assert
中:
assert ( validator ( r . params )) -- throw an error if validation fails
return fm . serveRedirect ( 307 , " / " ) -- return redirect in other cases
可用以下验证器检查:
minlen
:(整数)检查字符串的最小长度。maxlen
:(整数)检查字符串的最大长度。test
:(函数)调用通过一个参数传递的函数,并有望返回true
或nil | false [, error]
。oneof
:( value | { table of values to be compared against }
)检查该参数是否匹配了提供的值之一。pattern
:(字符串)检查参数是否匹配LUA模式表达式。除支票外,规则还包括选项:
optional
:( bool)在nil
时可选。默认情况下需要所有参数,因此此选项允许在未提供参数时跳过规则。如果参数不是零,则所有规则仍然适用。msg
:(字符串)如果其中一张检查失败,则为此添加一个客户消息,从而覆盖了单个检查中的消息。该消息可能包括一个占位符( %s
),该占位符将被参数名称替换。验证器本身还接受几个选项,这些选项修改了如何返回或处理生成的错误:
otherwise
:(函数)设置一个错误处理程序,该处理程序在其中一项检查失败时被调用。 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.例如,
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.
考虑以下示例:
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])
请参阅许可证。