Post

Sinatra

Sinatra

Sinatra

Sinatra 是ruby中最为简单的server框架,提供了一系列的dsl,来供构建server使用

目录结构概览

从目录结构看起, base.rb 中最为重要代码行数最多, 其中涵盖了所有的Sinatra重要代码, Response, Request, CommonLogger, NotFound, Helpers, Templates, Base

从调用逻辑看起

  1. 调用代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
       require 'sinatra'
    
       get '/' do
         'Hello world!1'
       end
    
       get '/car' do
       end
    
       get '/car/info/:id' do |id|
         p '/car/info/:id ----'
         body '/car/info'
         status 200
       end
    
       get '/car/info' do
         body '/car/info'
         status 200
       end
    
       # main.rb
       extend Sinatra::Delegator
    
       call
    
  2. 代码调用栈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
       |- extend Sinatra::Delegator
         |- delegate [:get, :put ...] :register
           |- Delegator.delegate(Application)
             |- define_method :get do
             |-  Application.send(get, *args, &block)
               |- Application < Base
               |- Base.get(path, opts, &block)
                 |- route('GET', path, opts, &block)
                   |- (@routes[verb] || []) << compile!('GET', path, block, options)
    
    
       |- call
         |- call!(env)
           |- @request  = Request.new(env)
           |- @response = Response.new
           |- invoke { dispatch! }
           |- invoke { error_block!(response.status) }
             |- res = catch(:halt) { yield }
             |- body res
               |- route!("GET", '/car/info', options, &block)
                 |- 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
                   |- return unless match = pattern.match(route)
                   |- block ? block[self, values] : yield(self, values)
    

    第一个步骤, 将 :get, :put 等方法, 委托给Application, Application 继承Base,拿get举例, get 的调用在@routes中添加 将proc 取消binding的 可执行proc(不知道为什么需要这样,详细见 generate_method, 而且method_name 还可以是 “{ver} ${path}”), Regrex 对象等

    第二个执行, rack 中中间件调用call, 构建了response, Request.new(env), invoke 为一个接住halt异常,并且记录返回数据到response 的函数, dispatch!则更为重要, 在其中执行了route!, route!的目的为执行路由,寻找匹配的路由,然后执行其中的可执行proc, process_route, 使用存在routes中的正则匹配路径是否命中,命中则执行对应的proc,

  3. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    
       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
    
       class Application < Base; end
    
       class Base
         def get(path, opts = {}, &block)
           conditions = @conditions.dup
           route('GET', path, opts, &block)
    
           @conditions = conditions
           route('HEAD', path, opts, &block)
         end
    
         def route(verb, path, options = {}, &block)
           # Because of self.options.host
           signature = compile!(verb, path, block, options)
           (@routes[verb] ||= []) << signature
         end
    
         def compile!(verb, path, block, options = {})
           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 }
           [ pattern, keys, conditions, wrapper ]
         end
    
         def generate_method(method_name, &block)
           method_name = method_name.to_sym
           define_method(method_name, &block)
           method = instance_method method_name
           remove_method method_name
           method
         end
       end
    
    
       def call!(env) # :nodoc:
         @env      = env
         @request  = Request.new(env)
         @response = Response.new
         @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
    
       def invoke
         res = catch(:halt) { yield }
         res = [res] if Integer === res or String === res
         body(res)
       end
    
    
       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
    
       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
    
       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
    
  4. 总结 1. DSL中通过将用户的block, 解除bind,执行bind来达到更换执行环境的问题,用这种方法实现的有 :get, :error 等, filter 2. sinatra提供的一下halt, pass 等,是采用throw :halt, 等来实现的, throw异常 然后中断正常处理,从而由sinatra接管 3. sinatra在路由匹配上每次都会将所有的现有的路由,首先通过verb(get, post, put) 等,过滤, 然后 循环匹配, 4. 关于渲染模板中如何将实例变量(@name=’xx’) 带入到模板中去, sinatra使用的为tilt, 在render的时候 将Sinatra::Application 实例传递进去, 使tilt在Application的实例变量中渲染模板(get 获得的代码块在Application实例中执行, 所以实例变量等都会副职在 其中, 而渲染模板的环境也在Application中,保证能够正常执行) 5. 这里在halt的实现方式上就有对比了, Rails vs Sinatra, rails中的callback实现的方式为每次检查Environment的变量, sinatra中为抛出异常, 传统认为抛出异常的代价是很高的。 6. sinatra中的invoke抽象的非常好, invoke是执行block然后将结果保存到body, status,response中 7. error 中的管理方法同路由中的一样,保存在类变量中, error => @error, :get, :put => @route, 8. 其中的Delegator的设定, 可以避免代码污染,污染全局空间,将委托方法放在一个module中

This post is licensed under CC BY 4.0 by the author.