lua-resty-route
支持可插拔匹配引擎的 OpenResty URL 路由库
$ opm get DevonStrawn/lua-resty-route
lua-resty-route
lua-resty-route 是一个用于 OpenResty 的 URL 路由库,支持多种路由匹配器、中间件以及 HTTP 和 WebSockets 处理程序,仅举其功能的一部分。
匹配器
lua-resty-route
支持在路由上使用多种不同的匹配器。目前我们支持以下匹配器:
前缀(区分大小写和不区分大小写)
等于(区分大小写和不区分大小写)
匹配(使用 Lua 的
string.match
函数)正则表达式(区分大小写和不区分大小写)
简单匹配(区分大小写和不区分大小写)
匹配器通过路由模式中的前缀进行选择,它们在某种程度上遵循 Nginx 的 location
块前缀。
前缀 | 匹配器 | 区分大小写 | 默认使用 ---------|---------|----------------|---------------- [none]
| 前缀 | ✓ | ✓ *
| 前缀 | | =
| 等于 | ✓ | =*
| 等于 | | #
| 匹配 | ¹ | ~
| 正则表达式 | ✓ | ~*
| 正则表达式 | | @
| 简单匹配 | ✓ | @*
| 简单匹配 | |
¹ Lua string.match
可以区分大小写或不区分大小写。
前缀匹配器
顾名思义,前缀匹配器仅匹配实际位置的前缀。前缀匹配器仅接受静态字符串前缀。如果您需要更高级的功能,请查看正则表达式匹配器。可以通过在开头添加 *
来不区分大小写地匹配前缀,:-)。让我们看看它的实际应用。
route "/users" (function(self) end)
此路由匹配以下位置:
/users
/users/edit
/users_be_aware
但它不匹配以下位置路径:
/Users
/USERS/EDIT
但这些仍然可以通过不区分大小写的方式匹配。
route "*/users" (function(self) end)
等于匹配器
此匹配器的工作方式与前缀匹配器相同,但它匹配确切的位置。要使用此匹配器,请在路由前添加 =
。
route "=/users" {
get = function(self) end
}
此路由仅匹配此位置:
/users
也可以使用不区分大小写的变体。
route "=*/users" {
get = function(self) end
}
当然,这也匹配以下位置:
/users
/USERS
/usErs
匹配匹配器
此匹配器使用 Lua 的 string.match
函数匹配模式。此匹配器的一个优点是它接受模式并提供捕获。请查看 Lua 文档以了解定义模式的各种方法。以下是一些示例:
route "#/files/(%w+)[.](%w+)" {
get = function(self, file, ext) end
}
这将匹配以下位置路径:
/files/test.txt
等。
在这种情况下,提供的函数(在此示例中仅响应 HTTP GET
请求)也将被调用,并带有以下捕获:"test"
(函数参数 file
)和 txt
(函数参数 ext
)。
对于许多人来说,正则表达式更为熟悉且功能更强大。接下来我们将介绍正则表达式。
正则表达式匹配器
正则表达式是进行模式匹配的常用方法。OpenResty 支持与 PCRE 兼容的正则表达式,此匹配器特别使用 ngx.re.match
函数。
route [[~^/files/(\w+)[.](\w+)$]] {
get = function(self, file, ext) end
}
与上面的匹配匹配器示例一样,最终结果相同,并且函数将被调用并带有捕获。
对于正则表达式匹配器,我们也有不区分大小写版本。
route [[~*^/files/(\w+)[.](\w+)$]] {
get = function(self, file, ext) end
}
简单匹配器
此匹配器是正则表达式匹配器的专门且有限版本,但它有一个优点。它自动处理类型转换,目前仅支持将整数转换为 Lua 数字。例如:
route:get "@/users/:number" (function(self, id) end)
您可能会有以下位置路径:
/users/45
上面的函数将获得 45
作为 Lua number
。
支持的简单捕获器有:
:string
,等效于此正则表达式[^/]+
(一个或多个字符,不包括/
):number
,等效于此正则表达式\d+
(一个或多个数字,可以使用tonumber
函数转换为 Lua 数字)
将来,我们可能会添加其他捕获快捷方式。
当然,此匹配器也有不区分大小写版本。
route:get "@*/users/:number" (function(self, id) end)
简单匹配器始终从开头到结尾匹配位置(不考虑部分匹配)。
路由
在 lua-resty-route
中定义路由的方法有很多种。可以说它有点像用于定义路由的 Lua DSL。
要定义路由,您首先需要一个新的路由实例。此实例可以与不同的请求共享。您可以在 init_by_lua*
中创建路由。这里我们定义一个新的路由实例:
local route = require "resty.route".new()
现在我们有了这个 route
实例,我们可以继续下一节,"HTTP 路由"。
注意:分派时,路由将按照添加顺序进行尝试。这与 Nginx 本身处理 location
块的方式不同。
HTTP 路由
HTTP 路由是在 Web 相关路由中最常见的事情。因此,HTTP 路由是 lua-resty-route
中的默认路由方式。其他类型的路由包括例如"WebSockets 路由"。
最常见的 HTTP 请求方法(有时称为动词)是:
方法 | 定义 ---------|----------- GET
| 读取 POST
| 创建 PUT
| 更新或替换 PATCH
| 更新或修改 DELETE
| 删除
虽然这些是最常见的,但 lua-resty-route
绝不限于此。您可以像使用这些常见方法一样使用任何请求方法。但为了简单起见,我们只在示例中使用这些方法。
路由的通用模式
route(...)
route:method(...)
或
route(method, pattern, func)
route:method(pattern, func)
例如:
route("get", "/", function(self) end)
route:get("/", function(self) end)
只有第一个函数参数是必需的。这就是为什么我们可以以相当灵活的方式调用这些函数。对于某些 methods
(例如 websocket),我们可以传递一个 table
而不是 function
作为路由处理程序。接下来,我们将介绍调用这些函数的不同方法。
将路由定义为表
route "=/users" {
get = function(self) end,
post = function(self) end
}
local users = {
get = function(self) end,
post = function(self) end
}
route "=/users" (users)
route("=/users", users)
使用 Lua 包进行路由
route "=/users" "controllers.users"
route("=/users", "controllers.users")
这些与以下内容相同:
route("=/users", require "controllers.users")
一次定义多个方法
route { "get", "head" } "=/users" (function(self) end)
一次定义多个路由
route {
["/"] = function(self) end,
["=/users"] = {
get = function(self) end,
post = function(self) end
}
}
路由所有 HTTP 请求方法
route "/" (function(self) end)
route("/", function(self) end)
捕获所有路由
route(function(self) end)
路由疯狂
route:as "@home" (function(self) end)
route {
get = {
["=/"] = "@home",
["=/users"] = function(self) end
},
["=/help"] = function(self) end,
[{ "post", "put"}] = {
["=/me"] = function(self)
end
},
["=/you"] = {
[{ "get", "head" }] = function(self) end
},
[{ "/files", "/cache" }] = {
-- requiring controllers.filesystem returns a function
[{"get", "head" }] = "controllers.filesystem"
}
}
如您所见,这非常疯狂。但这实际上并没有到此结束。我甚至还没有提到可调用 Lua 表(即带有元方法 __call
的表)或 WebSockets 路由。它们也受支持。
WebSockets 路由
文件系统路由
文件系统路由基于文件系统树。这可以被认为是约定路由。文件系统路由依赖于LuaFileSystem 模块或首选且与 LFS 兼容的ljsyscall。
例如,假设我们有以下类型的文件树:
/routing/
├─ index.lua
├─ users.lua
└─ users/
│ ├─ view@get.lua
│ ├─ edit@post.lua
│ └─ #/
│ └─ index.lua
└─ page/
└─ #.lua
此文件树将为您提供以下路由:
@*/
→index.lua
@*/users
→users.lua
@*/users/view
→users/view@get.lua
(仅路由 GET 请求)@*/users/edit
→users/edit@post.lua
(仅路由 POST 请求)@*/users/:number
→users/#/index.lua
@*/page/:number
→page/#.lua
这些文件可能如下所示(只是一个示例):
index.lua
:
return {
get = function(self) end,
post = function(self) end
}
users.lua
:
return {
get = function(self) end,
post = function(self) end,
delete = function(self) end
}
users/view@get.lua
:
return function(self) end
users/edit@post.lua
:
return function(self) end
users/#/index.lua
:
return {
get = function(self, id) end,
put = function(self, id) end,
post = function(self, id) end,
delete = function(self, id) end
}
page/#.lua
:
return {
get = function(self, id) end,
put = function(self, id) end,
post = function(self, id) end,
delete = function(self, id) end
}
要根据文件系统树定义路由,您需要调用 route:fs
函数:
-- Here we assume that you do have /routing directory
-- on your file system. You may use whatever path you
-- like, absolute or relative.
route:fs "/routing"
使用文件系统路由,您只需将新文件添加到文件系统树中,它们就会自动添加为路由。
命名路由
您可以定义命名路由处理程序,然后在实际路由中重用它们。
route:as "@home" (function(self) end)
(使用 @
作为命名路由的前缀是可选的)
这里我们实际将其附加到路由:
route:get "/" "@home"
您也可以一次定义多个命名路由:
route:as {
home = function(self) end,
signin = function(self) end,
signout = function(self) end
}
或者如果您想使用前缀:
route:as {
["@home"] = function(self) end,
["@signin"] = function(self) end,
["@signout"] = function(self) end
}
必须在路由中引用命名路由之前定义它们。命名路由还有其他用途或将有其他用途。待办事项列表中包括反向路由和将路由转发到命名路由等功能。
中间件
lua-resty-route
中的中间件可以在每个请求或每个路由的基础上定义。中间件是在请求处理管道中添加的过滤器。由于 lua-resty-route
尽可能不加限制,因此我们不会真正限制过滤器的作用或编写方式。中间件可以像路由一样灵活地插入,并且它们实际上共享许多逻辑。有一个重要的区别。管道中可以有多个中间件,但只会执行一个匹配路由。中间件也可以被 yield(coroutine.yield
),这允许在路由之前和之后运行代码(您也可以 yield 路由,但这永远不会恢复)。如果您不 yield,则中间件被视为前置过滤器。
最常见的中间件类型是请求级中间件:
route:use(function(self)
-- This code will be run before router:
-- ...
self.yield() -- or coroutine.yield()
-- This code will be run after the router:
-- ...
end)
现在,正如您已经提示的那样,您也可以为特定路由添加过滤器:
route.filter "=/" (function(self)
-- this middleware will only be called on a specific route
end)
您可以在那里使用与路由相同的规则,例如:
route.filter:post "middleware.csrf"
当然,您也可以执行以下操作:
route.filter:delete "@/users/:number" (function(self, id)
-- here we can say prevent deleting the user who
-- issued the request or something.
end)
所有匹配的中间件都会在每个请求上运行,除非其中一个决定 exit
,但我们始终会尝试为已经运行并 yield 的中间件运行后置过滤器。但我们将以相反的顺序调用它们。
中间件 1 运行并 yield
中间件 2 运行(并完成)
中间件 3 运行并 yield
路由运行
中间件 3 恢复
中间件 1 恢复
中间件的顺序按范围排列
请求级中间件首先执行
路由级中间件其次执行
如果有多个请求或路由级中间件,则它们将按照添加到特定范围的顺序执行。yield 的中间件将以相反的顺序执行。yield 的中间件只会恢复一次。
在内部,我们确实使用了 Lua 的优秀 协程
。
我们将在将来支持一堆预定义的中间件。
事件
事件允许您为不同的 HTTP 状态代码或其他预定义事件代码注册专门的处理程序。每个代码或代码组只能有一个处理程序。
例如,您可以像这样定义 404
(即路由未找到)处理程序:
route:on(404, function(self) end)
一些组是预定义的,例如:
info
,状态代码 100 – 199success
,状态代码 200 – 299redirect
,状态代码 300 – 399client error
,状态代码 400 – 499server error
,状态代码 500 – 599error
,状态代码 400 – 599
您可以像这样使用组:
route:on "error" (function(self, code) end)
您也可以一次定义多个事件处理程序:
route:on {
error = function(self, code) end,
success = function(self, code) end,
[302] = function(self) end
}
然后有一个通用的捕获所有事件处理程序:
route:on(function(self, code) end)
我们将按以下顺序查找正确的事件处理程序:
如果有特定代码的特定处理程序,我们将调用该处理程序。
如果有特定代码的组处理程序,我们将调用该处理程序。
如果有捕获所有处理程序,我们将调用该处理程序。
每个事件只调用其中一个。
将来我们可能会添加其他处理程序,您可以在其中挂钩。
路由器 API
您可能在之前的示例中看到过,函数的第一个参数是self
。self
代表一个router
,其中包含许多下面记录的实用函数。
虽然上面提到的Route API
用于定义路由,但Router API
实际上是关于运行路由的。
router.context
这是一个非常强大的概念,用于在不同的路由和函数之间共享数据。许多中间件将被插入到context中。
例如,一个Redis中间件可以将redis
对象添加到context
中,这样您就可以
local ok, err = self.redis:set("cat", "tommy")
Redis连接的打开和关闭是由中间件在场景之前自动完成的。这意味着您不需要初始化或关闭与Redis服务器的连接,而是由这个小型框架
来处理。如您所见,此self
参数会自动传递到框架的不同层,并且此context使得在它们之间传递数据变得容易。
router.yield()
类似于coroutine.yield()
,但如您在上面中间件部分所见,只需调用self.yield()
即可将中间件拆分为前置和后置过滤器
,这使得我们能够在将来添加例如调试/分析代码。self.yield()
更能说明发生了什么,并使代码更易于阅读(可能只是主观意见)。
router:redirect(uri, code)
类似于ngx.redirect
,但在实际调用ngx.redirect
并结束处理程序之前,运行重定向事件处理程序和后置过滤器,其中code
(如果未指定,则为ngx.HTTP_MOVED_TEMPORARILY
)。
router:exit(uri, code)
类似于ngx.exit
,但在实际调用ngx.exit
并结束处理程序之前,运行事件处理程序和后置过滤器,其中code
(如果未指定,则为ngx.OK
)。
router:exec(uri, args)
类似于ngx.exec
,但在实际调用ngx.exec
并结束处理程序之前,运行事件处理程序和后置过滤器。
router:done()
类似于使用ngx.HTTP_OK
的ngx.exit
,但在实际调用ngx.exit
并结束处理程序之前,运行事件处理程序和后置过滤器。
router:abort()
这是为ngx.on_abort
使用保留的(NYI)。目前仅在运行事件处理程序和后置过滤器后调用ngx.exit(499)
。
router:fail(error, code)
如果error
是字符串,则将其记录到错误日志。否则,它类似于ngx.exit(code)
(默认情况下,code
为ngx.HTTP_INTERNAL_SERVER_ERROR
),但在实际调用ngx.exit
并结束处理程序之前,运行事件处理程序和后置过滤器。
router:to(location, method)
允许您执行另一个路由(由route
定义)。
router:render(content, context)
将内容写入输出流。如果存在context.template
,则它将调用context.template.render(content, context 或 self.context)
。
router:json(data)
将数据编码为JSON,添加application/json
内容类型标头并输出JSON。
router:*
这里可以添加更多内容以减少代码重复,但也可以通过注入self.context
来完成很多事情。
路线图
这是一些想法的集合,可能作为lua-resty-route
的一部分实现,也可能不实现。
添加文档
添加测试
重写当前中间件并添加新的中间件
重写当前的WebSocket处理程序
添加路由统计信息
添加自动路由清理和重定向(可能可配置)(清理函数已编写)
添加自动斜杠处理和重定向(可能可配置)
添加一种更自动化的方式来定义重定向
添加对路由缓存的支持
添加按主机路由的支持
添加按标头路由的支持
添加对Nginx阶段的支持
添加对轻松定义Web Hook路由的支持
添加对轻松定义服务器发送事件路由的支持
添加对“提供程序”的支持,例如渲染器(?)
添加对条件的支持,例如内容协商
添加对路由分组的支持(在Nginx配置级别已可能)
添加对反向路由的支持
添加对表单方法欺骗的支持
添加对客户端连接中止事件处理程序(
ngx.on_abort
)的支持添加对主机(以及可能)其他标头过滤的支持
添加对基本身份验证的支持
添加对JWT/OpenID Connect身份验证的支持
添加来自Nginx配置的引导功能
添加对资源(或视图集)的支持(更自动化的REST路由)
添加对资源(或视图集)的文件系统路由支持
另请参阅
lua-resty-reqargs — 请求参数解析器
lua-resty-session — 会话库
lua-resty-template — 模板引擎
lua-resty-validation — 验证和过滤库
许可证
lua-resty-route
使用双条款 BSD 许可证。
Copyright (c) 2015 – 2017, Aapo Talvensaari
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES`
作者
Aapo Talvensaari (@bungle)
许可证
2bsd
版本
-
支持可插拔匹配引擎的 OpenResty URL 路由库 2020-01-27 23:15:31