Lapis-Request和Action
【原文】 http://leafo.net/lapis/reference/actions.html
每一个 Lapis
的 Http
请求都会遵循 Nginx
的基础流程。第一步:路由;一个路由必须有一个匹配的 URL
与之对应。从语法角度,你定义一个路由同时你需要绑定一个关联的 Action
。当一个请求匹配到你之前定义的路由的时候,对应的 Action
的方法就会被调用。
所有 Action
被调用时都会传入一个 Request
对象参数。这个 Request
对象主要用于 Action
和 view
的数据传输。另外,Request
对象还充当着 WebServer
返回 Client
结果的接口。
这些 Action
的返回结果通常会被用于渲染输出。字符串的返回结果通常直接交由浏览器直接解析。table
的返回类型通常是会被当作渲染的可选参数。如果返回值不止一个,后期会将它们合并成一个结果集。所以输出结果可以返回字符串加 table
。
如果路由文件内没有定义与当前请求匹配的路由,会进入到默认的路由,在后面的 application callback
中会继续介绍。
路由
路由定义有它自己的语法规则。下面用一段简单的事例做一下介绍:
1 | local lapis = require("lapis") |
上面的路由规则是一个精确匹配的路由定义。比如:一个 /hello/world
不会匹配到 /hello
的路由上。
当然你可以指定一个命名参数和 :
跟着这个名称。这个参数会匹配除了 /
之外的任意字符(一般情况下):
1 | app:match("/page/:page", function(self) |
注意: 我们可以调用 print
进行 debug
。在 OpenResty
内运行的时候,print
的输出会发送到 Nginx
的 notice
日志。
捕获的路由参数的值会被存储到 Request
对象的 params
字段中。当然每个命名参数至少包含一个字符,否则会匹配失败。
splat
是另外一种匹配模式,它可以匹配所有甚至包含 /
字符。所有通过 splat
规则匹配到的数据会被存储到 Request
对象的名叫 params
的 table
中的 splat
的参数中。它的语法上就是一个 *
:
1 | app:match("/browse/*", function(self) |
如果你直接把长文本直接拼在地址后面,将不会匹配成功。当然有其他的办法,你可以在URL参数的最后加上 .zip
后缀以及加上 /files/:filename.zip
的路由规则。
路由可选组件
括号可以让路由规则变得可选:
1 | /projects/:username(/:project) |
上面的路由规则可以匹配 /projects/leafo
也可以匹配 /projects/leafo/lapis
。可选项如果没有匹配到数据的话会是一个 nil
值。
当然路由的可选规则还可以嵌套,比如:
1 | /settings(/:username(/:page))(.:format) |
路由参数字符类
一个字符类可以像 Lua
语法一样限制一个参数的规则。下面的这路由例子就是为了确保 user_id
是一个数字:
1 | /user/:user_id[%d]/posts |
下面这个路由就是限制是一个 16进制 的参数。
1 | /color/:hex[a-fA-F%d] |
路由优先级
首先第一步会把所有的匹配到的路由都匹配进来,然后进行从高到低排序:
- 精确路由
/hello/world
- 可变参路由
/hello/:variable
- 每添加一个可变参数会降低一下路由的优先级
- 模糊路由
/hello/*
- 每加一个模糊匹配的规则它的优先级就会升高一级
比如:/hello/*spat
和 /hello/*spat/world/*rest
第二个的优先级要比第一个更高。
以上就是路由的优先级排序:精确路由 >
可变参数路由 >
模糊路由
路由名称
路由名称不再是一个硬编码的 URL
结构的一部分,而是可以通过路由的名称可以直观的知道路由对应的 Action
是干嘛的,同时可以方便重定向到其它的路由。
在定义路由的时候的第一个参数就是用于命名路由的:
1 | local lapis = require("lapis") |
我们可以用 self:url_for()
用于重定向操作。第一个参数为重定向的路由的名称,第二个参数为可选参数可以用于传路由的参数。
注意:后续会继续介绍 url_for
的不同的生成 URL
的用法。
Http的请求方式
通常一个 URL
地址可以通过定义不同的 Http
请求方式来定义出不同的 Action
。Lapis
内部的方法 respond_to
可以完成上述的目标。
1 | local lapis = require("lapis") |
respond_to
也可以设置一个前置过滤器用于在请求前的一些操作。我们可以定义一个 before
函数。同时这个和前置过滤器有相同的语义,如果你调用了 self:write()
此调用往后的逻辑讲不会继续被执行。
1 | local lapis = require("lapis") |
当然还有一个特殊情况:任意的 POST
请求如果它的 Content-type
头信息被设置为 application/x-www-form-urlencoded
,不管有没有 respond_to
,所有的请求体会被转义同时所有的参数会赋值给 self.params
。
在此之前你可以看到过调用 app:get()
和 app:post()
方法的例子。它们都是基于 respond_to
做了一些封装,这样你可以快速的定义一个 Http
请求。比如 :get
、post
、delete
、put
。
1 | app:get("/test", function(self) |
前置过滤器
有时候你需要在执行某个 Action
之前运行一段代码。一个很常见的例子,比如:设置用户的 session
。我们可以定义一个前置过滤器或者一个函数在 Action
之前,比如:
1 | local app = lapis.Application() |
这样就相当于调用了 app:before_filter
。这样里面的逻辑就会按照既定的顺序执行。
如果在前置过滤器中调用了 self:write()
方法,然后这个 action
的逻辑就会运行至此结束。比如我们可以结束某个 Action
的后续逻辑或者没有满足某个条件是重定向到其他的页面:
1 | local app = lapis.Application() |
注意: self:write()
是一个常规的 Action
返回结果的过程,所以在 self:write()
中你可以返回你过去在 Action
里面的返沪的数据。
Request
对象
每个 Action
的第一个参数就是 Request
对象。也就是上面例子中的 self
。
Request
对象有如下的参数:
self.params
– 一个table
,包含所有GET
、POST
的请求参数self.req
– 原生的request table
(由ngx
生成)self.res
– 原生的response table
(用于更新ngx
)self.app
– 应用的实例self.cookies
–cookies
的table
,可用于设置Cookies
数据,且值仅支持字符串self.session
– 和Cookie
对应的,可以存储任何可以被JSON
编码的类型self.route_name
– 与请求匹配的路由名称self.options
– 控制请求的选项,底层都通过write
self.buffer
– 输出的缓冲,不需要通过人为的干涉,底层依然通过write
Request
对象还有其他的方法:
write(options, ...)
– 告知请求如何渲染结果url_for(route, params, ...)
– 获取一个命名的路由的URL
地址,或者一个对象build_url(path, params)
–build
一个完全限定的URL
地址html(fn)
– 通过Html
构造器的语法生成一个字符串
@req
这是对 ngx
中的原生的 self.req
进行的一次封装。以下是它的相关的特性。
self.req.headers
–Request
的header
的table
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
内部的数据的时候需要判断空的情况:
1 | app:match("/my-cookies", function(self) |
现有的 cookie
会存储在 metatable
的 __index
中。因为所有的数据都会存储在 self.cookies
中,所以我们可以直接在 action
中就使用。
因此,操作 cookie
的代码就会变得下面这么简洁:
1 | app:match("/sets-cookie", function(self) |
通常情况下所有的 cookie
还有其它的属性 Path=/; HttpOnly
(这会创建一个 Session Cookie
)。你可以通过 cookie_attributes
函数配置你应用里面的 Cookie
设置。下面是一个简单的设置过期时间的例子:
1 | local date = require("date") |
这个 cookie_attributes
方法将 Request
对象作为它的第一个参数,然后下面就是具体的 Cookie
的设置操作。
Session
self.session
是一个更高级的请求方式。session
的内容会被序列化成 JSON
然后存储到特定的 cookie
中。这些序列化的 cookie
通常是经过签名的,所以通常这些数据不能随意篡改的。
session
的读写方式和 cookie
的方式很像:
1 | app.match("/", function(self) |
默认 session
的名称会存储在叫 lapis_session
的 cookie
中。当然可以通过修改配置文件中的 session_name
变量来修改 session
的名称。session
是通过你的应用的密钥进行签名,它存储在你的配置文件变量 secret
中。强烈建议替换掉这个默认值。
1 | -- config.lua |
Request
对象的方法
write(things...)
写所有的参数。不同的 action
根据参数的类型进行区分。
string
– 字符串附加到输出缓冲区function
(或者可以调用的table
) – 在输出缓冲区被调用,同时结果递归传给write
table
– 键值对会分配给self.options
,其它的值会递归传给write
大多数的情况下,将调用 write
作为 action
的返回值传给 write
这样的方式没有必要。在前置过滤器中,write
有双重作用,不但可以输出内容,还可以取消正在运行的 action
。
url_for(name_or_obj, params, query_params=nil, ...)
给 name_or_obj
生成一个 URL
。
注意:url_for
命名上有点不太恰当,因为通常它会生成一个页面的路径。如果你想得到一个完整的路径,你可以使用 build_for
函数。
如果 name_or_obj
是一个字符串,然后会去查找这个名称的 route
,参数就是二个参数的值。如果没有指定名称的路由,会抛出一个错误。
事例如下:
1 | app:match("index", "/", function() |
具体的 url_for
的使用如下:
1 | -- returns: / |
如果第三个参数 query_params
有值。将会转成具体的 URL
参数拼接在地址后面。如果 route
没有任何的参数,第二个参数仍要传一个 nil
:
1 | -- returns: /data/123/height?sort=asc |
当然 route
的所有的可选组件只有在被传值之后才会引入进来。如果没有被赋值就会被忽略。
比如,给出的 route
规则如下:
1 | app:match("user_page", "/user/:username(/:page)(.:format)", function(self) |
以下是 URL
的生成逻辑:
1 | -- returns: /user/leafo |
如果一个 route
包含一个通用匹配符,在传值的过程中你可以将数据赋值给 splat
参数:
1 | app:match("browse", "/browse(/*)", function(self) |
1 | -- returns: /browse |
url_for
的第一个参数为对象
如果 name_or_obj
是一个 table
,然后在这个 table
中调用了 url_params
方法,最终返回值传递给 url_for
。
url_params
方法里面的 request
对象参数以及其它的参数都会传递给 url_for
。
我们常见的方式是在 model
中实现 url_params
,同时赋予相关的功能。比如:一个 User
model
定义了一个 url_params
方法,实现查看用户详情页的功能:
1 | local Users = Model:extend("users", { |
结合上面的 url_for
实例演示一下:
1 | local user = Users:find(100) |
你可能注意到传递给 url_params
一个 ...
参数以及返回值里面也有 ...
。意思是可以传递 query_params
参数:
1 | local user = Users:find(1) |
使用 url_key
方法
如果 params
的参数的值是一个字符串类型,会直接赋值到生成的地址中。如果是一个 table
类型,在 table
中会调用 url_key
方法,然后返回值同样会赋值到生成的地址中。
举个例子,在一个 user
model
中使用 url_key
方法:
1 | local Users = Model:extend("users", { |
如果我们想要生成一个用户详情页的路径,通常会这么写:
1 | local user = Users:find(1) |
因为上面的 url_key
的例子中 User
对象等同于 id
参数,所以写法上可以变换一下:
1 | local user = Users:find(1) |
注意:url_key
方法的第一个参数是路径名称,所以我们可以根据自己的需求修改相应的 route
的句柄。
build_for(path, [options])
构建一个绝对地址。
比如,我们的服务运行在 localhost:8080
上面:
1 | self:build_url() --> http://localhost:8080 |
渲染(render
)的可选项
当一个 table
被写入时,它们的键值对(仅是字符串类型的key
)会被拷贝到 self.options
中。比如,下面的例子中的 render
和 status
属性会被拷贝。这个 table
仅会在 action
生命周期结束的时候使用,用于返回数据。
1 | app:match("/", function(self) |
以下就是可选值清单:
status
– 设置一个Http
状态值 (例如:200、404、500 …)render
– 视图的名称,必须是一个字符串或者是一个视图类content_type
– 用于设置Content-type
头信息headers
– 返回数据的header
,table
类型json
– 返回的json
字符串layout
– 修改应用的默认的layout
redirect_to
– 重定向到其它的地址,支持相对路径和绝对路径
当使用 json
渲染的时候,需要确保内容的类型,以及应用的 layout
属性会被禁用:
1 | app:match("/hello", function(self) |
应用回调
应用回调是一个特殊的方法,在我们的应用中处理某些特定类型的请求的时候可以覆盖相应的方法。这些函数在我们的应用中可以正常的调用,这也意味着这些函数的第一个参数是 request
对象的实例。
默认 action
当一个请求不能匹配给定的所有的路由规则时,它会去运行一个默认的 action
。Lapis
会预定义一个 action
,事例如下:
1 | function app:default_route() |
在上面的例子中,以 /
为后缀的请求会被重定向到没有斜杠的位置,其他的请求会调用应用的 handle_404
方法。
default_route
方法也是应用中的一个普通的方法。你可以根据你的实际的业务需求覆盖这个方法。比如,添加日志:
1 | function app:default_route() |
你会注意到除了 default_route
还有另外一个方法 handle_404
也可以进行预定义操作:
1 | function app:handle_404() |
上面的代码会触发一个 500
的错误和一个无效请求的堆栈的跟踪记录。如果你想让 404
页面变得更友好一点,你需要按照下面的操作进行重写。
通过覆盖 handle_404
方法进行自定义 404
页面。
下面是一个简单的 404
页面,仅输出 "Not Found!"
。
1 | function app:handle_404() |
错误处理
每个 Lapis
的 action
的执行都会被 xpcall
进行封装。这样可以确保那些致命错误可以输出一些可读性更高的信息,而不是一些 Nginx
的默认的错误信息。
错误处理主要处理那些意想不到的错误,该知识点后面还会进一步展开。
Lapis
的预定义了错误处理相关的操作,比如所有的额错误信息会被渲染到 "lapis.views.error"
中。错误页面包含调用栈以及错误信息。
如果你想自定义错误信息,你可以覆盖 handle_error
方法:
1 | -- config.custom_error_page is made up for this example |
request
对象或者 self
在系统异常的时候会传递失败。Lapis
提供了一个其他的方式获取 request
对象。
你可以使用 self.original_request
获取原始的 request
对象。
因为在错误页面里面会把全部的调用栈全部打出来,所以在线上环境建议设置自己的自定义错误页面,同时在日志记录相关的异常信息。
lapis-exception
模块增加了错误记录数据库的功能,同时可以发送通知邮件。