Mark24
RodaPlugin之hash_route说明
前言
这里主要讲解下 hash_route 如何使用,官网介绍的不是很清楚。至少我看的一脸懵逼。
这里主要是通过源码进行分析得出的结论。
1.为什么要用hash_route 而不是 multi_route
不论是文档还是 MasteringRoda 一书中都会提到要用hash_route去替换multi_route,为什么?
简单可以看下源码,可以理解为,multi_route存取查询route的时候是通过正则表达式。这就必然很慢。
而hash_route是通过一个约定把路由名字生成固定的命名法的key,和对应的handler对应起来。这样子路由进入解析,直接处理,把path当成key调用方法就可以了。
这个方法最快复杂度只有 O(1)。 所以推荐 hash_route 来拆解复杂路由。
2.只管看下 hash_route 到底做了什么
我们让Roda自己告诉我们
require "roda"
class App < Roda
plugin :hash_routes
plugin :json
hash_branch('test') do |r|
# 定义 ....
end
route do |r|
r.root do
# 通过源码我们知道 hash_route 在 Roda class上挂载了opts,而这里会保存着路径和方法名的映射。可以访问首页打印出注册路由名称的关系。
self.class.opts
end
r.hash_routes
end
end
用以下代码举例子:
require "roda"
class App < Roda
plugin :hash_routes
plugin :json
hash_branch("home") do |r|
r.is "welcome" do
"home - welcome"
end
end
hash_path("/login") do |r|
"login"
end
hash_path("/logout") do |r|
"logout"
end
route do |r|
r.root do
self.class.opts
end
r.hash_routes
end
end
打开浏览器访问 http://0.0.0.0:9292
我们可以得到
{
"hash_branches":
{
"":
{
"/home": "_roda_hash_branch__/home_1"
}
},
"hash_paths":
{
"":
{
"/login": "_roda_hash_path__/login_2",
"/logout": "_roda_hash_path__/logout_3"
}
},
"hash_routes_methods":
{},
"json_result_classes": ["Array", "Hash"],
"json_result_serializer": "#<Proc:0x00000001010bb718(&:to_json) (lambda)>",
"json_result_content_type": "application/json"
}
3.namespace 和 自定义
源码中 namespace、path 是两个不同的概念,不要混淆。
比如:
hash_branch("home") do |r|
r.is "welcome" do
"home - welcome"
end
end
当他是一个参数的时候,其实默认有一个未知参数是 namespace 他的值默认是 ""
。所以你在观察前面 JSON 的时候可以看到很多方法挂载空字符串下。
这是它默认的工作方式。
我们可以通过覆盖第一个参数,来提供名字空间。
hash_branch("roda_blog", "home") do |r|
r.is "welcome" do
"home - welcome"
end
end
这时候输出变成
{
"hash_branches":
{
"roda_blog":
{
"/home": "_roda_hash_branch_roda_blog_/home_1"
}
},
"hash_paths":
{
"":
{
"/login": "_roda_hash_path__/login_2",
"/logout": "_roda_hash_path__/logout_3"
}
},
"hash_routes_methods":
{},
"json_result_classes": ["Array", "Hash"],
"json_result_serializer": "#<Proc:0x000000010259f738(&:to_json) (lambda)>",
"json_result_content_type": "application/json"
}
我们可以看到 roda_blog
有了自己名字空间。
4.使用namespace
如果我们知道了namespace,来看下Roda是如何来构建复杂的应用程序。
namespace 其实就是一个名字,我们使用 hash_*
类方法的时候,可以通过第一参数命名这个路由。
而我们也可以在 r.hash_*
RodaRequest实例方法 里面通过 namespace 显示的指定在这儿处理的路由。
require "roda"
class App < Roda
plugin :hash_routes
plugin :json
hash_branch("roda_blog", "roda_home") do |r|
r.is "welcome" do
"home - welcome"
end
end
hash_branch("home") do |r|
r.is "welcome" do
"home - welcome"
end
r.hash_routes("roda_blog")
end
route do |r|
r.root do
self.class.opts
end
r.hash_routes
end
end
我们访问 http://0.0.0.0:9292/home/roda_home/welcome
可以获得期待的返回。
Roda执行路由十分简单,就是在运行Ruby代码。这里我们来分析一下:
Roda 的 route 源码定义是解析 路由的入口,所以从这里开始。正确解析 r.root 的时候,这是主页。
r.hash_routes
操作其实很简单,它内部会 先执行 r.hash_paths
然后再 执行 r.hash_branches
, 这两者的区别是 hash_paths
其实就是 Roda
的 r.is
思想走的是精确匹配。不过 hash_paths
的匹配思想非常简单粗暴,直接把 路由里面的 path,作为 key 在 hash_paths
注册的字典里查找,找到就返回。这就是精确查找。
r.hash_branches
其实就是 r.on
的感觉是模糊/*
式 匹配路由。他就是以 '/'
分割第一个元素作为 branch 进行匹配,匹配下去的继续执行。
可以简单理解为,执行路由的感觉,他不过是在执行匹配代码。这里 hash_path 如果是直接匹配中就是 O(1) 复杂度。
我们把注意力收回来,继续分析:
route 中的 r.hash_routes
其实是 r.hash_routes("")
的简写,所以它挂载的是 hash_branch("home")
。而hash_branch("home")
里面写了一句 r.hash_routes("roda_blog")
其实这里就是在通过 namespace
的方式去挂载 "roda_blog"
。
这里要额外注意,作为 namespace,symbol和 string 作为 key 完全不同效果。要区分开,他们不一样。
解析路由方面,namespace
完全不参加过程。解析过程,完全依赖与书写和挂载的顺序。这里 假设我们解析了 "/home"
就会到 hash_branch("home")
中,如果我们继续有参数 "/home/roda_home"
其实这里就会进入 hash_branch("roda_blog", "roda_home")
,因为第一个参数是 namespace
不参与计算,只是用来互相关联的作用,第二个参数是 path
会参与 @remaining_path
的计算中去。这样匹配中,还可以继续往下匹配,精准匹配到 welcome
结束。
所以这里定义成什么样子,就可以匹配。而内部如果想要继续分开,就继续使用 hash_route 划分为更小的文件。
如果不想分开,就可以继续使用 r.on、r.is。他们无非是 Ruby 的代码而已。
5. DSL hash_routes
5.1 hash_routes DSL 介绍
RodaRequest
提供了一个方法 r.hash_routes
用来自动请求 hash_paths\hash_branches
,在定义路由的时候,也设计了一个 hash_routes
这是一个 DSL,用来在上下文中快速的声明 hash_paths\hash_branches
。
5.2 hash_routes DSL 的 两种写法
DSL部分的 hash_routes 核心也非常简单。
可以用两种使用方法,其实差不多。
如果 block.arity=1 参考 proc#method-i-arity 如果你的块需要1个参数,那么会把 dsl实例作为参数返回在块中。如果没有参数,这个块会在 dsl的instance上下文中使用。
# Invoke the DSL for configuring hash routes, see DSL for methods inside the
# block. If the block accepts an argument, yield the DSL instance. If the
# block does not accept an argument, instance_exec the block in the context
# of the DSL instance.
def hash_routes(namespace='', &block)
dsl = DSL.new(self, namespace)
if block
if block.arity == 1
yield dsl
else
dsl.instance_exec(&block)
end
end
dsl
end
所以可以这样, 可以省略 |r|
或者 不省略
require "roda"
class App < Roda
plugin :hash_routes
plugin :json
hash_routes("roda_home") do |r|
r.is "welcome" do
"home - welcome"
end
r.on "login" do
"home - login"
end
end
hash_routes("plugin_home") do
is "welcome" do
"home - welcome"
end
on "bye" do
"home - welcome"
end
end
#....
end
5.3 hash_routes DSL 没有 path 了, 只有名字空间。
简单直接第一个参数是 namespace 所以 hash_routes DSL 永远是以 声明 被挂载路由而存在的。
而 hash_routes 中的 is\on 的实际上原理,是 把 hash_routes 的 namespace 和 自己声明的 path 组合在一起
is 就是使用 hash_paths, on 就是使用 hash_branches
只是一个快速声明的语法糖。
def on(segment, &block)
@roda.hash_branch(@namespace, segment, &block)
end
# Use the segment to setup a path in the current namespace.
# If path is given as a string, it is prefixed with a slash.
# If path is +true+, the empty string is used as the path.
def is(path, &block)
path = path == true ? "" : "/#{path}"
@roda.hash_path(@namespace, path, &block)
end
没有 path 了, 只有名字空间。这意味着 在 根 App 上,一定是通过原始的 r.on r.is 来分发到 r.hash_routes, 这里给出一个例子:
r.on 'orders' do
r.is do
r.hash_routes('orders/index')
end
end
另一个文件中
class App
hash_routes('orders/index') do
is true do |r|
#....
view 'orders/index'
end
end
end
路由之间的层次关系,必须通过嵌套的调用关系,并且是 path 部分的嵌套关系决定的。
5.4 坑 dsl instance 和 roda_request 傻傻分不清楚
当我们想在 hash_routes DSL 里面进行嵌套访问的时候,别忘了
hash_routes do |hr|
这里 hr 具有 on、is 方法,但是不能与 RodaReques 的 @_request 混淆,我们已经自源码里看清楚,这个不是 request对象。
要想在 hash_routes 里继续调用 hash_routes 进行嵌套,需要使用 is\on 方法的 block 参数 —— 这才是 @_request ,RodaRequest 实例才有 r.hash_routes 方法。
require "roda"
class App < Roda
plugin :hash_routes
plugin :json
hash_routes "list" do |hr| # 这里 hr 是 hash_routes DSL 实例 dsl
#/shop/list
hr.is true do
'lists'
end
end
hash_routes "shop" do
# /shop
is true do
'shops'
end
on "list" do |r| # !!! on 方法的 block 参数,才是 RodaRequest 实例 @_request 具有 hash_routes 方法
# 这里的 r 不是 @_request 而是 dsl instance
# 所以这里的方法是空
r.hash_routes("list")
end
end
route do |r|
r.root do
self.class.opts
end
r.on "shop" do
r.hash_routes("shop")
end
end
end
5.5 dsl 是返回实例意味着你可以这样
hash_routes 返回的是一个 dsl 实例 ,意味着你也可以通过命令式去声明。
这一切都在意料之中。
def hash_routes(namespace='', &block)
dsl = DSL.new(self, namespace)
if block
if block.arity == 1
yield dsl
else
dsl.instance_exec(&block)
end
end
dsl
end
# =======
require "roda"
class App < Roda
plugin :hash_routes
plugin :json
bhr = hash_routes(:b)
bhr.is "test" do
"b:test"
end
route do |r|
r.root do
self.class.opts
end
r.hash_routes(:b)
end
end
5.6 自由挂载
这里有一个例子,我可以在 route 里面直接挂载三个路由。
也可以在 hash_routes 里面嵌套 namespace 挂载。比较自由。
require "roda"
class App < Roda
plugin :hash_routes
hash_routes "a" do
on "a-path" do |r|
r.hash_routes("b")
"a---path"
end
end
hash_routes "b" do
on "b-path" do
"b---path"
end
end
hash_routes "c" do
on "c-path" do
"c----path"
end
end
route do |r|
r.hash_routes
r.hash_routes("a")
r.hash_routes("b")
r.hash_routes("c")
end
end
6.dispatch_from
dispatch_from 的作用是会在 hash_routes 的DSL中发挥作用,它主要会把当前的hash_routes用 hash_branch 包一层,再这层的下面调用 r.hash_routes 以他自己的namespace作为参数。
其实等于给改变了 hash_routes 的 namespance和branch。
比如:
require "roda"
class App < Roda
plugin :hash_routes
hash_routes "a" do
dispatch_from("abc")
on "a-path" do |r|
"a---path"
end
end
route do |r|
r.hash_routes
end
end
这里 route
中 r.hash_routes
实际上是 r.hash_routes("")
不过 route 这边是运行解析时候关注的。
还得切换下视角,hash_routes
声明语句其实是注册路由。到底注册了什么路由呢?
hash_routes "a"
是在 "a"
的 namespace
里注册路由。
普通情况下需要调用 r.hash_routes("a")
挂载生效。
这里比较特别 dispatch_from("abc")
实际上是 dispatch_from(namespace="", "abc")
等于要
r.hash_branch("abc")
包裹,并且依然是注册在""
所以这里
http://127.0.0.1:9292/abc/a-path
可以访问到
如果我们把名字空间加在 dispatch_from
里,比如 dispatch_from("ns", "abc-prefix")
看看下面的会如何反映
require "roda"
class App < Roda
plugin :hash_routes
# 以下代码等价于如下情况
#
# hash_routes "a" do
# on "a-path" do |r|
# "a---path"
# end
# end
# hash_branch("ns", "abc-prefix") do
# r.hash_routes("a")
# end
hash_routes "a" do
dispatch_from("ns", "abc-prefix")
on "a-path" do |r|
"a---path"
end
end
route do |r|
r.hash_routes("ns")
end
end
在运行的时候,我们挂载 "ns"
,这里实际上 dispatch_from("ns", "abc-prefix")
就相当于定义了 "ns"
了。
总之 dispatch_from
这就是一个包装重写 namespace
和 branch
的一个方法。
7. ??获取参数
TODO
总结:
1.Roda原生的 r.is r.on 只能把路由定义在一个文件中
因为本质上他们只是一段嵌套的block代码,重要的价值在于他们嵌套产生的闭包,可以帮助声明程序。 而执行的时候,递进调用,也会非常方便。这里无法很好地拆成多个文件。
2.hash_routes解决拆分的方法
实际上这里也没有什么魔法可言,hash_routes 所谓的拆分,就是做了一些事情,他把我们其他文件声明 hash_* 的方法在 App < Roda 中执行一遍。这其实是在执行类方法,而这个函数执行的意义实际上在不断地给 Roda.class.opts 里面的一个 hashmap 里面不断地注册方法名,同时也把这些方法定义在 App 上。
这就像是把 App 作为一个全局变量,然后把所有方法都挂在上面了,保持方法名没有冲突即可。
3.hash_routes 的解析
等到路由进入 Roda 的route开始执行,它就像执行Ruby代码一样一段一段的执行,r.is、r.on 他们会递进块式的执行。如果遇到 r.hash_routes 他们会通过传入的名字空间,去App上找到 存储方法的 hashmap,然后从中取出 相应 namespace 作为key的 route hash_map, 然后再通过 @remaining_path 根据是 path还是branch匹配key来获得方法名,然后调用这个方法名。
@remaining_path 作为每一段执行路由里面,都会访问的一个全局变量。不过这个变量是在顺序执行的代码中被访问的。匹配就编辑它,去除匹配部分继续递进顺着 tree-route 的树杈进行匹配。 如果不匹配就把修改的还原回去,跳过自己,传递给下一段代码。直至无法匹配返回 false,Roda遇到 false、nil 处理为 404。
3.小结
hash_routes 是注册方法
r.hash_routes 是挂载方法,默认挂载 ‘’
挂载在同一级别,就是同一级别的路由。
嵌套的 r.hash_routes 才是下一级