lapis請求處理

lapis請求處理

每一個被Lapis處理的HTTP請求在被Nginx處理後都遵循相同的基本流程。第一步是路由。路由是 url 必須匹配的模式。當你定義一個路由時,你也得包括一個處理函數。這個處理函數是一個常規的Lua/MoonScript函數,若是相關聯的路由匹配,則將調用該函數。html

全部被調用的處理函數都具備一個參數(一個請求對象)。請求對象將存儲您但願在處理函數和視圖之間共享的全部數據。此外,請求對象是您向Web服務器瞭解如何將結果發送到客戶端的接口。nginx

處理函數的返回值用於渲染輸出。字符串返回值將直接呈現給瀏覽器table 的返回值將用做[渲染選項]()。若是有多個返回值,則全部這些返回值都合併到最終結果中。您能夠返回字符串和table以控制輸出。數據庫

若是沒有匹配請求的路由,則執行默認路由處理程序,在[application callbacks]()瞭解更多。json

Routes 和 URL 模式

路由模式 使用特殊語法來定義URL的動態參數 併爲其分配一個名字。最簡單的路由沒有參數:api

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個字符,不然將沒法匹配。cookie

splat是另外一種類型的模式,將盡量匹配,包括任何/字符。 splat存儲在請求對象的 params 表中的 splat 命名參數中。它只是一個單一 *session

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 中)app

可選路由組件

圓括號可用於使路由的一部分可選:

/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 模塊增長了錯誤處理程序以在數據庫中記錄錯誤。 它也能夠當有異常時向您發送電子郵件。

相關文章
相關標籤/搜索