sinatra的路由
来自1.4.7
requrie 'sinatra'将导致top扩展Sinatra::Delegator
extend Sinatra::Delegator
而Delegator是具备get、post、set等方法的,这些方法最终委派到Sinatra::Application
module Delegator #:nodoc:
def self.delegate(*methods)
methods.each do |method_name|
define_method(method_name) do |*args, &block|
return super(*args, &block) if respond_to? method_name
Delegator.target.send(method_name, *args, &block)
end
private method_name
end
end
delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
:template, :layout, :before, :after, :error, :not_found, :configure,
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
:production?, :helpers, :settings, :register
class << self
attr_accessor :target
end
self.target = Application
end
Sinatra::Application的get、post继承自Sinatra::Base,它们实际上是调用route来设置各种verb路由,且都是class method
def get(path, opts = {}, &block)
conditions = @conditions.dup
route('GET', path, opts, &block)
@conditions = conditions
route('HEAD', path, opts, &block)
end
def put(path, opts = {}, &bk) route 'PUT', path, opts, &bk end
def post(path, opts = {}, &bk) route 'POST', path, opts, &bk end
def delete(path, opts = {}, &bk) route 'DELETE', path, opts, &bk end
def head(path, opts = {}, &bk) route 'HEAD', path, opts, &bk end
def options(path, opts = {}, &bk) route 'OPTIONS', path, opts, &bk end
def patch(path, opts = {}, &bk) route 'PATCH', path, opts, &bk end
def link(path, opts = {}, &bk) route 'LINK', path, opts, &bk end
def unlink(path, opts = {}, &bk) route 'UNLINK', path, opts, &bk end
route方法主要是将path编译成“用于匹配url的正则表达式、匹配后与参数键值对应、选项、用于响应url的方法”的signature,然后将signature保存在@routes[verb]
def route(verb, path, options = {}, &block)
# Because of self.options.host
host_name(options.delete(:host)) if options.key?(:host)
enable :empty_path_info if path == "" and empty_path_info.nil?
signature = compile!(verb, path, block, options)
(@routes[verb] ||= []) << signature
invoke_hook(:route_added, verb, path, block)
signature
end
将url转换成正则表达式的compile比较复杂,可以去看compile_test,看它到底支持哪些模式(现在的compile好像是用musterman来做了)
def compile!(verb, path, block, options = {})
options.each_pair { |option, args| send(option, *args) }
method_name = "#{verb} #{path}"
unbound_method = generate_method(method_name, &block)
pattern, keys = compile path
conditions, @conditions = @conditions, []
wrapper = block.arity != 0 ?
proc { |a,p| unbound_method.bind(a).call(*p) } :
proc { |a,p| unbound_method.bind(a).call }
wrapper.instance_variable_set(:@route_name, method_name)
[ pattern, keys, conditions, wrapper ]
end
(unbound_method这种做法解释如下:In fact, earlier versions of Sinatra do use instance_eval . However, there is an alternative: dynamically create a method from that block, get the unbound method object for that method, and remove the method immediately. When you want to run the code, bind the method object to the current instance and call it. This has a few advantages over instance_eval : it results in significantly better performance since the scope change only occurs once as opposed to every request. It also allows the passing of arguments to the block. Moreover, since you can name the method yourself, it results in more readable stack traces. All of this logic is wrapped in Sinatra’s generate_method)
搜索下源码,可发现routes是在route!里使用的。为得知请求时怎样到达那里的,在响应方法里打印调用栈,如
get '/' do
puts caller
"Bye, world!"
end
得到
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1611:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1611:in `block in compile!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:975:in `[]'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:975:in `block (3 levels) in route!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:994:in `route_eval'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:975:in `block (2 levels) in route!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1015:in `block in process_route'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1013:in `catch'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1013:in `process_route'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:973:in `block in route!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:972:in `each'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:972:in `route!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1085:in `block in dispatch!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `block in invoke'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `catch'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `invoke'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1082:in `dispatch!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:907:in `block in call!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `block in invoke'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `catch'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:1067:in `invoke'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:907:in `call!'
/home/ken/.rvm/gems/ruby-2.2.2/gems/sinatra-1.4.7/lib/sinatra/base.rb:895:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-protection-1.5.3/lib/rack/protection/xss_header.rb:18:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-protection-1.5.3/lib/rack/protection/path_traversal.rb:16:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-protection-1.5.3/lib/rack/protection/json_csrf.rb:18:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-protection-1.5.3/lib/rack/protection/base.rb:49:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-protection-1.5.3/lib/rack/protection/base.rb:49:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-protection-1.5.3/lib/rack/protection/frame_options.rb:31:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-1.6.4/lib/rack/logger.rb:15:in `call'
/home/ken/.rvm/gems/ruby-2.2.2/gems/rack-1.6.4/lib/rack/commonlogger.rb:33:in `call'
......
往上查找,可发现从call开始(确实也符合rack标准)。因处理流程切割成多个方法,需要以实例变量来保持状态,所以dup
def call(env)
dup.call!(env)
end
def call!(env) # :nodoc:
@env = env
@request = Request.new(env)
@response = Response.new
@params = indifferent_params(@request.params)
template_cache.clear if settings.reload_templates
force_encoding(@params)
@response['Content-Type'] = nil
invoke { dispatch! }
invoke { error_block!(response.status) } unless @env['sinatra.error']
unless @response['Content-Type']
if Array === body and body[0].respond_to? :content_type
content_type body[0].content_type
else
content_type :html
end
end
@response.finish
end
一路到invoke
def invoke
res = catch(:halt) { yield }
res = [res] if Fixnum === res or String === res
if Array === res and Fixnum === res.first
res = res.dup
status(res.shift)
body(res.pop)
headers(*res)
elsif res.respond_to? :each
body res
end
nil # avoid double setting the same response tuple twice
end
invoke执行包裹成block的dispatch!,而dispatch!又invoke了route!
def dispatch!
invoke do
static! if settings.static? && (request.get? || request.head?)
filter! :before
route!
end
rescue ::Exception => boom
invoke { handle_exception!(boom) }
ensure
begin
filter! :after unless env['sinatra.static_file']
rescue ::Exception => boom
invoke { handle_exception!(boom) } unless @env['sinatra.error']
end
end
终于到了route!
def route!(base = settings, pass_block = nil)
if routes = base.routes[@request.request_method]
routes.each do |pattern, keys, conditions, block|
returned_pass_block = process_route(pattern, keys, conditions) do |*args|
env['sinatra.route'] = block.instance_variable_get(:@route_name)
route_eval { block[*args] }
end
# don't wipe out pass_block in superclass
pass_block = returned_pass_block if returned_pass_block
end
end
# Run routes defined in superclass.
if base.superclass.respond_to?(:routes)
return route!(base.superclass, pass_block)
end
route_eval(&pass_block) if pass_block
route_missing
end
先找出verb所对应的响应方法集,再循环地process_route(注意,routes的排序按脚本中定义的顺序,The First Sufficient Match Wins)
def process_route(pattern, keys, conditions, block = nil, values = [])
route = @request.path_info
route = '/' if route.empty? and not settings.empty_path_info?
return unless match = pattern.match(route)
values += match.captures.map! { |v| force_encoding URI_INSTANCE.unescape(v) if v }
if values.any?
original, @params = params, params.merge('splat' => [], 'captures' => values)
keys.zip(values) { |k,v| Array === @params[k] ? @params[k] << v : @params[k] = v if v }
end
catch(:pass) do
conditions.each { |c| throw :pass if c.bind(self).call == false }
block ? block[self, values] : yield(self, values)
end
ensure
@params = original if original
end
如果match,则运行刚才调用process_route 时所带的block,内含route_eval。因之前已将响应方法包装成{ |a,p| unbound_method.bind(a).call },所以yield(self, values)使响应方法能以params访问到dup出来的Base的@params
而route_eval是执行响应方法块并以throw :halt来返回响应方法块的返回值的
def route_eval
throw :halt, yield
end
于是,便跳出route!,让dipatch!的invoke能拦截:halt返回值并设置response
再回到call!,最终,@response.finish,以符合rack规范的方式返回[status, header, body],完成响应。