为什么这么说呢?从没有见过运用如此广泛,几乎是所有Ruby Web Framework基础的开源项目中,这个文件里竟然有如此之多令人费解的部分(完全可以认为是Bug)
简单的举个例子吧,首先,对于Rack来说,.ru文件和.rb文件的区别在哪里?
def self.parse_file(config, opts = Server::Options.new)
options = {}
if config =~ /\.ru$/
cfgfile = ::File.read(config)
if cfgfile[/^#\\(.*)/] && opts
options = opts.parse! $1.split(/\s+/)
end
cfgfile.sub!(/^__END__\n.*\Z/m, '')
app = new_from_string cfgfile, config
else
require config
app = Object.const_get(::File.basename(config, '.rb').capitalize)
end
return app, options
end
代码中告诉了我们答案,如果是.ru文件,Rack::Builder不仅仅会将其读出,而且还会从中解析出以#\开头的部分,作为Rack::Server启动的选项,举个例子:
#\--port 3000
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "Hello Rack!"]}
比如这个config.ru文件,rackup在执行这个文件是将启动出3000端口的服务器而非9292。
这本来是个不错的设计啦,但是仔细看代码就会发现,只支持一个选项,第二个选项用方括号运算符是匹配不到的,额。。。你妹。。
不得不承认Rack::Builder支持map包含map,也就是形成树结构,这点不错。更重要的是Rack仅仅执行那些不得不执行的map的代码块,如果路由没匹配到就不予执行这些都很好。但是当你发现Rack::Builder没接收到一个请求就会创建一个新的URLMap对象然后把路由计算成正则表达式,并且计算出的正则表达式不予缓存,每次都重新计算,就会感觉之前争取到的性能在这里又浪费了的惋惜啊。。好吧,这一定是考虑到开发模式下的需求。
接着就是最坑的一个Bug了
require 'rack'
infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, 'root']}
builder = Rack::Builder.new do
map '/' do
run infinity
end
use Rack::CommonLogger
map '/version' do
map '/' do
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
end
map '/last' do
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] }
end
end
end
Rack::Handler::Thin.run builder, :Port => 9292
curl 0.0.0.0:9292/version/last
虽然看上去结果应该是infinity beta 0.0,但实际上结果是root,额。。
这个Bug的关键在于use Rack::CommonLogger这句语句的位置,可以看到,它写在两个map的中间。而use方法是这么实现的:
def use(middleware, *args, &block)
if @map
mapping, @map = @map, nil
@use << proc { |app| generate_map app, mapping }
end
@use << proc { |app| middleware.new(app, *args, &block) }
end
为了让middleware处于middleware stack的外面,代码先判断当前有没有执行过map方法,如果执行过的话,先将它放进middleware stack,外面放middleware对象。这样执行的时候就能先执行到middleware了(大概作者是这么想的,但是这么做完全是错的,实际情况是先放进middleware stack的先被执行到)。如果你没有执行过map方法,那么虽然现在middleware stack上看上去只有middleware对象,但是等会执行的时候会把map包进来,这样就解决问题了。但是它显然没有想到,如果我把use写两个map中间的情况。此时,middleware stack对象会先把第一个map写进去,然后跟着middleware。然后在下一个map执行的时候,重新赋值map对象,并在最后被middleware stack的两个元素包住。这样请求一进来的时候就会发现,竟然是第一个map直接接受请求,连middleware都不是(之前讲过,实际情况是先写进middleware stack的先被执行到)。
算起来好像只有把use语句写在所有map之前的时候才是能真正正常工作的。大概确实很少人不这么做吧,否则这么明显的问题为何不被解决呢。
虽然我们可以看到,Rack::Builder实现了自己的路由和middleware调用方法,但是无论是Rails还是Sinatra,都无一基于Rack::Builder,不仅仅是因为Rack::Builder没有更多的功能,已有功能也不容易扩展,而且,里面坑太多了,还不如自己从头写呢。