lapis请求处理
每个被Lapis
处理的HTTP
请求在被Nginx
处理后都遵循相同的基本流程。第一步是路由。路由是 url
必须匹配的模式。当你定义一个路由时,你也得包括一个处理函数。这个处理函数是一个常规的Lua/MoonScript
函数,如果相关联的路由匹配,则将调用该函数。
所有被调用的处理函数都具有一个参数(一个请求对象)。请求对象将存储您希望在处理函数和视图之间共享的所有数据。此外,请求对象是您向Web服务器
了解如何将结果发送到客户端的接口。
处理函数的返回值用于渲染输出。字符串返回值将直接呈现给浏览器。table
的返回值将用作[渲染选项]()。如果有多个返回值,则所有这些返回值都合并到最终结果中。您可以返回字符串和table
以控制输出。
如果没有匹配请求的路由,则执行默认路由处理程序,在[application callbacks]()了解更多。
Routes 和 URL 模式
路由模式 使用特殊语法来定义URL
的动态参数 并为其分配一个名字。最简单的路由没有参数:
local lapis = require("lapis")
local app = lapis.Application()
app:match("/", function(self) end)
app:match("/hello", function(self) end)
app:match("/users/all", function(self) end)
这些路由与URL
逐字匹配。 /
路由是必需的。路由必须匹配请求的整个路径。这意味着对 /hello/world
的请求将不匹配 /hello
。
您可以在:
后面理解跟上一个名称来指定一个命名参数。该参数将匹配除/的所有字符(在一般情况下):
app:match("/page/:page", function(self)
print(self.params.page)
end)
app:match("/post/:post_id/:post_name", function(self) end)
在上面的例子中,我们调用 print 函数来调试,当在openresty中运行时,print的输出是被发送到nginx的notice级别的日志中去的
捕获的路由参数的值按其名称保存在请求对象的 params
字段中。命名参数必须至少包含1个字符,否则将无法匹配。
splat
是另一种类型的模式,将尽可能匹配,包括任何/
字符。 splat
存储在请求对象的 params
表中的 splat
命名参数中。它只是一个单一 *
app:match("/browse/*", function(self)
print(self.params.splat)
end)
app:match("/user/:name/file/*", function(self)
print(self.params.name, self.params.splat)
end)
如果将任何文本直接放在splat
或命名参数之后,它将不会包含在命名参数中。例如,您可以将以.zip
结尾的网址与/files/:filename.zip
进行匹配(那么.zip
就不会包含在命名参数 filename
中)
可选路由组件
圆括号可用于使路由的一部分可选:
/projects/:username(/:project)
以上将匹配 /projects/leafo
或 /projects/leafo/lapis
。可选组件中不匹配的任何参数在处理函数中的值将为nil。
这些可选组件可以根据需要嵌套和链接:
/settings(/:username(/:page))(.:format)
参数字符类
字符类可以应用于命名参数,以限制可以匹配的字符。语法建模在 Lua
的模式字符类之后。此路由将确保该 user_id
命名参数只包含数字:
/color/:hex[a-fA-F%d]
这个路由只匹配十六进制参数的十六进制字符串。
/color/:hex[a-fA-F%d]
路由优先级
首先按优先顺序搜索路由,然后按它们定义的顺序搜索。从最高到最低的路由优先级为:
精确匹配的路由 /hello/world
变化参数的路由 /hello/:variable
贪婪匹配的路由 /hello/*
命名路由
为您的路由命名是有用的,所以只要知道网页的名称就可以生成到其他网页的链接,而不是硬编码 URL
的结构。
应用程序上定义新路由的每个方法都有第二个形式,它将路由的名称作为第一个参数:
local lapis = require("lapis")
local app = lapis.Application()
app:match("index", "/", function(self)
return self:url_for("user_profile", { name = "leaf" })
end)
app:match("user_profile", "/user/:name", function(self)
return "Hello " .. self.params.name .. ", go home: " .. self:url_for("index")
end)
我们可以使用self:url_for()
生成各种操作的路径。第一个参数是要调用的路由的名称,第二个可选参数是用于填充 参数化路由 的值的表。
点击[url_for]() 去查看不同方式去生成 URL
的方法。
处理HTTP动词
根据请求的 HTTP
动词,进行不同的处理操作是很常见的。 Lapis
有一些小帮手,让写这些处理操作很简单。 respond_to
接收由 HTTP
动词索引的表,当匹配对应的动词执行相应的函数处理
local lapis = require("lapis")
local respond_to = require("lapis.application").respond_to
local app = lapis.Application()
app:match("create_account", "/create-account", respond_to({
GET = function(self)
return { render = true }
end,
POST = function(self)
do_something(self.params)
return { redirect_to = self:url_for("index") }
end
}))
respond_to
也可以采用自己的 before
过滤器,它将在相应的 HTTP
动词操作之前运行。我们通过指定一个 before
函数来做到这一点。与过滤器相同的语义适用,所以如果你调用 self:write()
,那么其余的动作将不会运行.
local lapis = require("lapis")
local respond_to = require("lapis.application").respond_to
local app = lapis.Application()
app:match("edit_user", "/edit-user/:id", respond_to({
before = function(self)
self.user = Users:find(self.params.id)
if not self.user then
self:write({"Not Found", status = 404})
end
end,
GET = function(self)
return "Edit account " .. self.user.name
end,
POST = function(self)
self.user:update(self.params.user)
return { redirect_to = self:url_for("index") }
end
}))
在任何 POST
请求,无论是否使用 respond_to
,如果 Content-type
头设置为 application/x-www-form-urlencoded
,那么请求的主体将被解析,所有参数将被放入 self.params
。
您可能还看到了 app:get()
和 app:post()
方法在前面的示例中被调用。这些都是封装了 respond_to
方法,可让您快速为特定 HTTP
动词定义操作。你会发现这些包装器最常见的动词:get
,post
,delete
,put
。对于任何其他动词,你需要使用respond_to
。
app:get("/test", function(self)
return "I only render for GET requests"
end)
app:delete("/delete-account", function(self)
-- do something destructive
end)
Before Filters
有时你想要一段代码在每个操作之前运行。一个很好的例子是设置用户会话。我们可以声明一个 before 过滤器,或者一个在每个操作之前运行的函数,像这样:
local app = lapis.Application()
app:before_filter(function(self)
if self.session.user then
self.current_user = load_user(self.session.user)
end
end)
app:match("/", function(self)
return "current user is: " .. tostring(self.current_user)
end)
你可以通过多次调用 app:before_filter
来随意添加。它们将按照注册的顺序运行。
如果一个 before_filter
调用 self:write()
方法,那么操作将被取消。例如,如果不满足某些条件,我们可以取消操作并重定向到另一个页面:
local app = lapis.Application()
app:before_filter(function(self)
if not user_meets_requirements() then
self:write({redirect_to = self:url_for("login")})
end
end)
app:match("login", "/login", function(self)
-- ...
end)
self:write()
是处理一个常规动作的返回值,所以同样的事情你可以返回一个动作,可以传递给 self:write()
请求对象
每个操作在调用时会请求对象作为其第一个参数传递。由于调用第一个参数 self
的约定,我们在一个操作的上下文中将请求对象称为 self
。
请求对象具有以下参数:
self.params
一个包含所有GET
,POST
和URL
参数的表self.req
原始请求表(从ngx状态生成)self.res
原始响应表(从ngx状态生成)self.app
应用程序的实例self.cookies
cookie
表,可以分配设置新的cookie
。 只支持字符串作为值self.session
session
表, 可以存储任何能够 被JSON encode
的值。 由Cookie
支持self.route_name
匹配请求的路由的名称(如果有)self.options
控制请求如何呈现的选项集,通过write
设置self.buffer
输出缓冲区,通常你不需要手动设置,通过write
设置
此外,请求对象具有以下方法:
write(options, ...)
指示请求如何呈现结果
url_for(route, params, ...)
根据命名路由或对象来获取 URL
build_url(path, params)
根据 path
和 params
构建一个完整的URL
html(fn)
使用HTML
构建语法生成字符串
@req
原始请求表 self.req
封装了 ngx
提供的一些数据。 以下是可用属性的列表。
self.req.headers
请求头的表
self.req.parsed_url
解析请求的url
,这是一个包含scheme
, path
, host
, port
和 query
属性的表
self.req.params_post
POST
请求的参数表
self.req.params_get
GET
请求的参数表
Cookies
请求中的 self.cookies
表允许您读取和写入Cookie
。 如果您尝试遍历表以打印 Cookie
,您可能会注意到它是空的:
app:match("/my-cookies", function(self)
for k,v in pairs(self.cookies) do
print(k, v)
end
end)
现有的 Cookie
存储在元表的 __index
中。 之这样做,是因为我们可以知道在操作期间分配了哪些 Cookie
,因为它们将直接在 self.cookies
表中。
因此,要设置一个 cookie
,我们只需要分配到 self.cookies
表:
app:match("/sets-cookie", function(self)
self.cookies.foo = "bar"
end)
默认情况下,所有 Cookie
都有额外的属性 Path = /
; HttpOnly
(创建一个session cookie
)。 您可以通过重写 app.cookie_attributes
函数来配置 cookie
的设置。 以下是一个向 cookies
添加过期时间以使其持久化的示例:
local date = require("date")
local app = lapis.Application()
app.cookie_attributes = function(self)
local expires = date(true):adddays(365):fmt("${http}")
return "Expires=" .. expires .. "; Path=/; HttpOnly"
end
cookie_attributes
方法将请求对象作为第一个参数(self
),然后是要处理的 cookie
的名称和值。
Session
self.session
是一种更先进的方法,通过请求来持久化数据。 会话的内容被序列化为 JSON
并存储在特定名称的 cookie
中。 序列化的 Cookie
使用您的应用程序密钥签名,因此不会被篡改。 因为它是用 JSON
序列化的,你可以存储嵌套表和其他原始值。
session
可以像 Cookie
一样设置和读取:
app.match("/", function(self)
if not self.session.current_user then
self.session.current_user = "Adam"
end
end)
默认情况下,session
存储在名为 lapis_session
的 cookie
中。 您可以使用配置变量session_name
覆盖 session
的名称。 session
使用您的应用程序密钥(存储在配置的secret
中)进行签名。 强烈建议更改它的默认值。
-- config.lua
local config = require("lapis.config").config
config("development", {
session_name = "my_app_session",
secret = "this is my secret string 123456"
})
请求对象的方法
write(things...)
一下列出它的所有参数。 根据每个参数的类型执行不同的操作。
string
字符串追加到输出缓冲区function
(或者是可调用表) 函数被输出缓冲区调用,结果递归传递给write
table
键/值对将会被分配到self.options
中 ,所有其他值递归传递给write
在大多数情况下,没有必要调用 write
,因为处理函数的返回值会自动传递给 write
。 在before filter
中 ,write
具有写入输出和取消任何进一步操作的双重目的。
url_for(name_or_obj, params, query_params=nil, ...)
依据 路由的name
或一个对象生成 url
url_for 有点用词不当,因为它通常生成到请求的页面的路径。 如果你想得到整个 URL,你可以与build_url函数和一起使用。
如果 name_or_obj
是一个字符串,那么使用 params
中的值来查找和填充该名称的路由。 如果路由不存在,则抛出错误。
给定以下路由:
app:match("index", "/", function()
-- ...
end)
app:match("user_data", "/data/:user_id/:data_field", function()
-- ...
end)
到页面的 URL
可以这样生成:
-- returns: /
self:url_for("index")
-- returns: /data/123/height
self:url_for("user_data", { user_id = 123, data_field = "height"})
如果提供了第三个参数 query_params
,它将被转换为查询参数并附加到生成的 URL
的末尾。 如果路由不接受任何参数,则第二个参数必须被设置为 nil
或 空对象 :
-- returns: /data/123/height?sort=asc
self:url_for("user_data", { user_id = 123, data_field = "height"}, { sort = "asc" })
-- returns: /?layout=new
self:url_for("index", nil, {layout = "new"})
如果提供了所有封闭的参数,则只包括路由的任何可选组件。 如果 optinal
组件没有任何参数,那么它将永远不会被包括。
给定以下路由:
app:match("user_page", "/user/:username(/:page)(.:format)", function(self)
-- ...
end)
可以生成以下 URL
:
-- returns: /user/leafo
self:url_for("user_page", { username = "leafo" })
-- returns: /user/leafo/projects
self:url_for("user_page", { username = "leafo", page = "projects" })
-- returns: /user/leafo.json
self:url_for("user_page", { username = "leafo", format = "json" })
-- returns: /user/leafo/code.json
self:url_for("user_page", { username = "leafo", page = "code", format = "json" })
如果路由包含了 splat
,则可以通过名为 splat
的参数提供该值:
app:match("browse", "/browse(/*)", function(self)
-- ...
end)
-- returns: /browse
self:url_for("browse")
-- returns: /browse/games/recent
self:url_for("browse", { splat = "games/recent" })
将对象传递给 url_for
如果 name_or_obj
是一个 table
,那么在该 table
上调用 此table
的url_params
方法,并将返回值传递给 url_for
。
url_params
方法接受请求对象作为参数,其次是任何传递给 url_for
的东西。
通常在 model
上实现 url_params
,让他们能够定义它们代表的页面。 例如,为User model
定义了一个 url_params
方法,该方法转到用户的配置文件页面:
local Users = Model:extend("users", {
url_params = function(self, req, ...)
return "user_profile", { id = self.id }, ...
end
})
我们现在可以将User
实例直接传递给 url_for
,并返回 user_profile
路径的l路由:
local user = Users:find(100)
self:url_for(user)
-- could return: /user-profile/100
你可能会注意到我们将 ...
传递给 url_params
方法返回值。 这允许第三个 query_params
参数仍然起作用:
local user = Users:find(1)
self:url_for(user, { page = "likes" })
-- could return: /user-profile/100?page=likes
使用 url_key
方法
如果 params
中参数的值是一个字符串,那么它会被直接插入到生成的路径中。 如果它的值是一个 table
,那么将在此 table
上面调用url_key
方法,并将此方法的返回值插入到路径中。
例如,我们为 User
模型定义一个我们的 url_key
方法:
local Users = Model:extend("users", {
url_key = function(self, route_name)
return self.id
end
})
如果我们想生成一个user_profile
文件的路径,我们通常可以这样写:
local user = Users:find(1)
self:url_for("user_profile", {id = user.id})
我们定义的 url_key
方法让我们直接传递 User
对象作为 id
参数,它将被转换为 id
:
local user = Users:find(1)
self:url_for("user_profile", {id = user})
url_key
方法将路由的名称作为第一个参数,因此我们可以根据正在处理的路由更改我们返回的内容。
build_url(path,[options])
依据 path
构建一个绝对 URL
。 当前请求的URI
b被用于构建URL
。
例如,如果我们在 localhost:8080
上运行我们的服务器:
self:build_url() --> http://localhost:8080
self:build_url("hello") --> http://localhost:8080/hello
渲染选项
每当写一个表时,键/值对(对于是字符串的键)被复制到 self.options
。 例如,在以下操作中,将复制render
和 status
属性。 在请求处理的生命周期结束时使用options
表来创建适当的响应。
app:match("/", function(self)
return { render = "error", status = 404}
end)
以下是可以写入的 options
的字段列表
status
设置http
状态码 (eg. 200,404,500
)render
导致一个视图被请求渲染。 如果值为true
,则使用路由的名称作为视图名称。 否则,该值必须是字符串或视图类。content_type
设置Content-type
头header
要添加到响应的响应头json
导致此请求返回JSON encode
的值。content-type
被设置为application / json
。layout
更改app
默认定义layout
redirect_to
将状态码设置为302
,并设置Location
头。 支持相对和绝对URL
。 (结合status
执行301
重定向)
当渲染 JSON
时,确保使用 json
渲染选项。 它将自动设置正确的content-type
并禁用 layout
:
app:match("/hello", function(self)
return { json = { hello = "world" } }
end)
应用程序回调
应用程序回调是一种特殊方法,它可以在需要处理某些类型的请求时调用。可以被应用程序覆盖, 虽然它们是存储在应用程序上的函数,但它们被称为是常规操作,这意味着函数的第一个参数是请求对象的实例。
默认操作
当请求与您定义的任何路由不匹配时,它将运行默认处理函数。 Lapis
附带了一个默认操作,预定义如下:
app.default_route = function(self)
-- strip trailing /
if self.req.parsed_url.path:match("./$") then
local stripped = self.req.parsed_url:match("^(.+)/+$")
return {
redirect_to = self:build_url(stripped, {
status = 301,
query = self.req.parsed_url.query,
})
}
else
self.app.handle_404(self)
end
end
如果它注意到URL
尾部跟随 一个/
,它将尝试重定向到尾部没有/
的版本。 否则它将调用app
上的handle_404
方法。
这个方法default_route
只是 app
的一个普通方法。 你可以覆盖它来做任何你喜欢的。 例如,添加个日志记录:
app.default_route = function(self)
ngx.log(ngx.NOTICE, "User hit unknown path " .. self.req.parsed_url.path)
-- call the original implementaiton to preserve the functionality it provides
return lapis.Application.default_route(self)
end
你会注意到在default_route
的预定义版本中,另一个方法handle_404
被引用。 这也是预定义的,如下所示:
app.handle_404 = function(self)
error("Failed to find route: " .. self.req.cmd_url)
end
这将在每个无效请求上触发 500
错误和 stack trance
。 如果你想做一个 404
页面,这b便是你能实现的地方。
覆盖handle_404
方法而不是default_route
允许我们创建一个自定义的404
页面,同时仍然保留上面的尾部/
删除代码。
这里有一个简单的404
处理程序,只打印文本Not Found
!
app.handle_404 = function(self)
return { status = 404, layout = false, "Not Found!" }
end
错误处理
Lapis
执行的每个处理函数都被 xpcall
包装。 这确保可以捕获到致命错误,并且可以生成有意义的错误页面,而不是 Nginx
默认错误信息。
错误处理程序应该仅用于捕获致命和意外错误,预期错误在[异常处理指南]()中讨论
Lapis
自带一个预定义的错误处理程序,提取错误信息并渲染模板 lapis.views.error
。 此错误页面包含报错的堆栈和错误消息。
如果你想有自己的错误处理逻辑,你可以重写方法handle_error
:
-- config.custom_error_page is made up for this example
app.handle_error = function(self, err, trace)
if config.custom_error_page then
return { render = "my_custom_error_page" }
else
return lapis.Application.handle_error(self, err, trace)
end
end
传递给错误处理程序的请求对象或 self
不是失败了的请求创建的请求对象。 Lapis
提供了一个新的,因为之前的可能已经写入失败了。
您可以使用self.original_request
访问原始请求对象
Lapis
的默认错误页面显示整个错误堆栈,因此在生产环境中建议将其替换自定义堆栈跟踪,并在后台记录异常。
lapis-exceptions
模块增加了错误处理程序以在数据库中记录错误。 它也可以当有异常时向您发送电子邮件。