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 动词定义操作。你会发现这些包装器最常见的动词:getpostdeleteput。对于任何其他动词,你需要使用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

请求对象具有以下参数:

  1. self.params 一个包含所有GETPOSTURL 参数的表

  2. self.req 原始请求表(从ngx状态生成)

  3. self.res 原始响应表(从ngx状态生成)

  4. self.app 应用程序的实例

  5. self.cookies cookie 表,可以分配设置新的cookie。 只支持字符串作为值

  6. self.session session表, 可以存储任何能够 被JSON encode 的值。 由Cookie支持

  7. self.route_name 匹配请求的路由的名称(如果有)

  8. self.options 控制请求如何呈现的选项集,通过write设置

  9. self.buffer 输出缓冲区,通常你不需要手动设置,通过write设置

此外,请求对象具有以下方法:

write(options, ...) 指示请求如何呈现结果

url_for(route, params, ...) 根据命名路由或对象来获取 URL

build_url(path, params) 根据 pathparams 构建一个完整的URL

html(fn) 使用HTML构建语法生成字符串

@req

原始请求表 self.req 封装了 ngx 提供的一些数据。 以下是可用属性的列表。

self.req.headers 请求头的表

self.req.parsed_url 解析请求的url,这是一个包含scheme, path, host, portquery 属性的表

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_sessioncookie 中。 您可以使用配置变量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...)

一下列出它的所有参数。 根据每个参数的类型执行不同的操作。

  1. string 字符串追加到输出缓冲区

  2. function (或者是可调用表) 函数被输出缓冲区调用,结果递归传递给write

  3. 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 上调用 此tableurl_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 。 当前请求的URIb被用于构建URL

例如,如果我们在 localhost:8080 上运行我们的服务器:

self:build_url() --> http://localhost:8080
self:build_url("hello") --> http://localhost:8080/hello

渲染选项

每当写一个表时,键/值对(对于是字符串的键)被复制到 self.options。 例如,在以下操作中,将复制renderstatus 属性。 在请求处理的生命周期结束时使用options表来创建适当的响应。

app:match("/", function(self)
  return { render = "error", status = 404}
end)

以下是可以写入的 options的字段列表

  1. status 设置 http 状态码 (eg. 200,404,500

  2. render 导致一个视图被请求渲染。 如果值为 true,则使用路由的名称作为视图名称。 否则,该值必须是字符串或视图类。

  3. content_type 设置Content-type

  4. header 要添加到响应的响应头

  5. json 导致此请求返回 JSON encode的值。 content-type被设置为 application / json

  6. layout 更改app默认定义layout

  7. 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 模块增加了错误处理程序以在数据库中记录错误。 它也可以当有异常时向您发送电子邮件。


youyu岁月
489 声望45 粉丝

不要用执行上的勤奋来掩盖思考上的懒惰