rails sinatra 等一些 ruby 的 web 框架,都是属于 rack app,建立在 rack 基础上的。 rack 是什么?官网上的解释是 Rack provides a minimal interface between webservers supporting Ruby and Ruby frameworks.我的理解就是链接服务器和 ruby 应用框架的,它解析 http,rack 里有两个比较重要的 class, Rack::Request, Rack::Response,Rack::Request 负责解析 request, Rack::Response 负责 http 的 response,想了解具体 rack 的应用查看http://www.rubydoc.info/github/rack/rack/Rack/Mime。 rack 可以 use 多个 middleware 和 run 一个 app, middleware 一般如下格式:
class MyMiddleware
def initialize(app)
@app = app
end
def call(env)
puts 'mymiddleware'
puts env.inspect
if env['PATH_INFO'] == '/'
@app.call(env)
else
[404, {'Content-Type' => 'text/plain'}, ['not ok']]
end
end
end
run 的那个 app 要响应 call,可以如下:
my_app = proc do |env|
[200, {'Content-Type' => 'text/plain'}, ["ok"]]
end
那么就可以用 rack 在 config.ru 中这样写服务,如下:
use MyOther
run my_app
以上只是展示了 rack 的感念,以及 rack 写服务的基础。因为 sinatra 是建立在 rack 基础之上的,所有这些应该算是读 sinatra 源码的前提准备。 1.sinatra 的整体结构 那么,我们来看一下,sinatra 是怎样来应对 rack 的要求的,sinatra 大体结构是这样的,代码如下:
module Sinatra
class Base
# 用于作为middleware用
def initialize(app=nil)
super
@app = app
yield self if block_given?
end
def call(env)
dup.call!(env)
end
def call!(env)
end
class << self
# 用于作为app用
def call(env)
prototype.call(env)
end
def prototype
@prototype ||= new
end
end
end
end
以上的代码结构就是 sinatra 应对 rack 的要求而组织的,使得 sinatra 既可以作为 middleware 又可以作为 app,当然,sinatra 要多一些 magic,后面再说,如果只是这样的结构,sinatra 程序就只能写成 modular-style structure,不能写成 classic style 的 structure,对于 modular-style structure 我写了一 seed https://github.com/wpzero/sinatra_seed.gitclone下来看一下。,可以 下面我们来看,sinatra 路径 magic get/post/put/delete 2.sinatra 路径 class macro sinatra 应用的代码如下:
require "sinatra/base"
class UserController < Sinatra::Base
get '/hello' do
'hello'
end
end
这个 get 方法就是 sinatra 的一些 magic,用 class macro 实现的。 class macro,涉及到一些 meta programming 的概念,我这里稍微介绍一下 ruby 为什么有 singleton method? 例如:
class C < D
end
c1 = C.new
c2 = C.new
def c1.hello
puts 'hello'
end
c1.hello
c2.hello
#=> 'hello' #=> undefined method `hello' for # 这是为什么?c1 和 c2 不是属于一同一个 class 吗?c1 look up method 一定也是向右一步找到它的 class(C),然后一路向上找 class C 的 ancestors, 找是否有 hello 这个 instance_method 没,有就调用,可是 c2 为什么不能找到 hello 这个 method,原因是 ruby,在 instance 和它的 class 之间还有一层 class(也就是大家所说的 eigenclass),每一个 instance 和它的 eigenclass 是唯一对应的。这就是 hello 这个方法的悉身之所,而 instance c2 的 eigenclass 与 c1 的 eigenclass 不同,所以 c2 不可找到 hello。 有了 eigenclass 这个概念,我们就可以利用往 eigenclass 中加入 instance method,那么相应的 instance 就可以调用了。而 class 也是一个 instance 只不过它的 class 是 Class 而已。 具体关系如图: 那么我们就来看一个最简单的 class macro。
class C
class << self
def macro
puts 'macro'
end
end
macro
end
#=> macro
在 class C 的 eigenclass 中定义一个方法,那么 class C 就可以调用。 那么 sinatra 是怎么实现 get/put 这些 routes 的方法的? 大体结构如下:
class Base
class << self
def get
#具体内容
end
def post
#具体内容
end
end
end
这样我们就可以调用 get 等方法在 Base 的 subclass 中。 下面我们具体分析它的实现的代码。 3.sinatra get 的具体实现(unboundmethod) 先介绍一下 unboudmethod。 一般 instance method 都是属于某个 class,而调用它的是该 class 的 instance,那么该在 instance method 中就可以 access 该 instance 的 instance variable。那么我们可不可以把一个 instance method 指定给一个 instance?ruby 是支持这个功能的,那就是 bind。 先看一个例子:
class Square
def area
@side * @side
end
def initialize(side)
@side = side
end
end
area_un = Square.instance_method(:area)
s = Square.new(12)
area = area_un.bind(s)
area.call #=> 144
instance_method Returns an UnboundMethod representing the given instance method in mod. 而这有什么用呢?我们就可以把一些 method 存起来,在某一时刻 bind 给一个 instance 来执行。 联想一下 sinatra 的 magic get 方法,其实,它就是实现了在 url 的 path_info 符合某个路径时,执行传给 get 的 block,而且这个 block 是 bind 到一个 Application instance 上的(也就是前边的举的例子的 UserController 的 instance)。 那么来看一下 sinatra 的源码,如下:
class Base
class << self
def get(path, opts={}, &block)
route('GET', path, opts, &block)
end
def route(verb, path, options={}, &block)
signature = compile!(verb, path, block, options)
(@routes[verb] ||= []) << signature
signature
end
def compile!(verb, path, block, options = {})
method_name = "#{verb} #{path}"
unbound_method = generate_method(method_name, &block)
# string to regular expression, and get the route keys
pattern, keys = compile path
conditions, @conditions = @conditions, []
[ pattern, keys, conditions, block.arity != 0 ?
proc { |a,p| unbound_method.bind(a).call(*p) } :
proc { |a,p| unbound_method.bind(a).call } ]
end
# path to regular expression and keys
def compile(path)
keys = []
if path.respond_to? :to_str
pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
pattern.gsub!(/((:\w+)|\*)/) do |match|
if match == "*"
keys << 'splat'
"(.*?)"
else
keys << $2[1..-1]
"([^/?#]+)"
end
end
[/^#{pattern}$/, keys]
elsif path.respond_to?(:keys) && path.respond_to?(:match)
[path, path.keys]
elsif path.respond_to?(:names) && path.respond_to?(:match)
[path, path.names]
elsif path.respond_to? :match
[path, keys]
else
raise TypeError, path
end
end
# 产生Base 的unboundmethod
def generate_method(method_name, &block)
# the current class is Base, self is Base
define_method(method_name, &block)
method = instance_method method_name
remove_method method_name
method
end
end
end
这里 generate_method 用于把一个 block 变为 Base 的 unboundmethod 用的就是 define_method 加上 instance_method。 -------------今天周五,哎!!带工资写一些吧。
依次类推,其他的 sinatra routes DSL,也都是相同的原理。 看下 sinatra 的源码,如下:
class Base
class << self
attr_reader :routes, :filters
# routes 是个hash, :get => [], :post => [] ...
# filters 是 hash, :before => [], :after => [].....
def get(path, opts={}, &block)
# get
conditions = @conditions.dup
route('GET', path, opts, &block)
# head
@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
end
end
现在,实现了 get /post/put/delete/ 等 sinatra 的 DSL。 但是 sinatra 程序执行的时候是怎么用到着 routes, filters 的呢?其实想一想也简单,就是 app instance 的 class 或者 superclass ...的 instance 属性 routes,filters 中存在呢,下面我们来研究一下 sinatra 是怎么实现的。 4.sinatra 程序如何实现 routes dispatch(根据 path_info,来执行相应的程序逻辑) 其实这主要的程序执行过程就是在 Base 的 instance method call! 中,回顾一下,前边我说的 sinatra 的主要框架。 来看一下 sinatra 代码的实现,如下:
class Base
# 这是rack app的写法
attr_accessor :request, :response, :params, :env
def initialize(app=nil)
super()
@app = app
yield self if block_given?
end
def call(env)
dup.call!(env)
end
def call!(env)
@env = env
# app instance 的instance variable, 用于分析request,
# 这里我用了Rack::Request, sinatra其实是自己写了一个Request继承Rack::Request, 其实就是加了一些instance method helper。
# 为了简单,我直接用Rack::Request
@request = Rack::Request.new(env)
# app instance 的instance variable, 用于完成reponse
# 这里同样,sinatra自己写了一个subclass继承了Rack::Response
# 这里为了简单明了,我用Rack::Response
@response = Rack::Response.new
# request.params 是Rack帮我们解析的一个http请求的参数,这里包括url data(url中的参数,如: \users?available=1中的:available => 1) 和
# form data(请求body中的信息,一般是put, post中的)
@params = indifferent_params(@request.params)
@response['Content-Type'] = nil
# 这个是程序的主要执行过程(dispatch)。这里用sinatra这个框架很特别的用法
invoke { dispatch! }
# 后面专门分析sinatra的错误处理
invoke { error_block!(response.status) }
# 这里可以先忽略content_type,status, body这些method,其实是很简单的,就是设置response,后面我们再介绍。
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
@response.finish
end
end
这里前面不过是对 env 的解析,用了 Rack::Request,至于 reponse 用了 Rack::Response。 最重要就是 invoke { dispatch! }这行代码,我们要好好分析学习一下。 下面我们看一下:invoke 和 dispatch! 这两个 method 的代码:
class Base
def invoke
# catch和throw sinatra完美诠释和应用了,应该学习。这里sinatra用catch throw来实现 1.随时跳出routes的处理(跳回到catch),2.同时返回结果。有些像c的goto。但是也是和goto一样是一把双刃剑
res = catch(:halt) { yield }
# 根据catch的结果设置status, body, headers
res = [res] if Fixnum === res or String === res
if Array === res and Fixnum === res.first
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
#
def dispatch!
invoke do
# 如果是静态文件,文件服务器
static! if settings.static? && (request.get? || request.head?)
# before filter执行
filter! :before
# route 执行
route!
end
rescue ::Exception => boom
# 错误处理后面在来分析
invoke { handle_exception!(boom) }
ensure
# after filter 执行
filter! :after unless env['sinatra.static_file']
end
end
我读 invoke 这里的代码,其实是很惊讶的,本来我只是知道有 catch 和 throw 这东西,因为没有机会用,我一直认为它们和 begin rescue ensure 相似,没想到,他们竟然是 ruby 的 goto,用于跳出一个程序 logic , 而且是可以带返回值,这里返回值付给 res,然后,更新 response。 这里发一个小小的 throw catch 的例子:
res = catch(:halt) {tt}
def tt
throw :halt, [1,2]
end
这里 res 的值为 [1,2],希望帮助大家理解 sinatra 的 invoke 的原理。 下面我来分析 dispatch! 方法。 1.先是判断是否是静态文件,用 static! 方法,static! 用了 send_file 这个 method,send_file 这个 method 用到了 Rack::File,这个有兴趣的,可以读一下。 2.执行 filter! before,来执行符合条件的 before。代码如下:
class Base
def self.settings
self
end
# 访问 the class Base 得到 这些属性
# Access settings defined with Base.set.
def settings
self.class.settings
end
def filter!(type, base = settings)
# one right step to the class and along the ancestors to find the methods which respond_to? :filters
# 其实就是Base
# filters 是 Application class的filters数据
filter! type, base.superclass if base.superclass.respond_to?(:filters)
base.filters[type].each { |args| process_route(*args) }
end
end
这里,我们可以看到,settings 其实就是 Application(继承了 Base 的 controller)。filter! 默认的 settings 参数就是 Application。filter! type, base.superclass if base.superclass.respond_to?(:filters) 这就 Application 的 superclass 一层一层的爬,都判断一遍,是否有符合的。base.filters 就是我们前边说的,Base instance variable @filters, 然后 each 执行 process_route,这里我先不讨论这个方法,我们和 route! 一起来看。 3.执行 route!, 代码如下:
def route!(base = settings, pass_block=nil)
if routes = base.routes[@request.request_method]
routes.each do |pattern, keys, conditions, block|
pass_block = process_route(pattern, keys, conditions) do |*args|
route_eval { block[*args] }
end
end
end
# 如果 当前application中,即controller中没有匹配的routes, 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
# 一个request 只对应一个路径
def route_eval
throw :halt, yield
end
大体逻辑和 filter! 相似,但是注意它的 process_route 在 route_eval 中调用,那么 route_eval 作用是什么? route_eval 确保 routes 中只要有一个路径符合要求就执行返回结果,不再继续查找。 4.用 ensure 确保 flter! after 执行 就算是 throw:halt ensure 的代码还是执行的,小例子:
def tt
puts 'execute'
throw :halt
ensure
puts 'ensure'
end
catch(:halt){ tt }
# => execure
# => 'ensure'
最后 filter! after 一定是执行的。 其实,我感觉 dispathc! 中的 invoke 完全没有必要,反而多执行一边 对 reponse 的设置,这个我有做测试,没有必要多执行一边 invoke 因为去掉,dispathc! 中的 invoke,ensure 还是执行的,这是 ensure 的属性,无非就是为了如果是静态文件时,不执行 filter! after,这个我感觉完全可以弄一个 instance_varibale 记录一下,没必要多执行,一遍 invoke!。也许是为了,代码的一致性。 我有把 sinatra 代码中的 dispathc! 中 invoke 去掉和没去掉之前做过对比,没有影响结果的正确定。反而少之行一遍 invoke。 没去掉之前 ----------------invoke----------------------- ----------------invoke----------------------- -------------second------------------- -------------ensure------------------- -------------first------------------- ----------------invoke----------------------- -------------third-------------------
去掉之后 ----------------invoke----------------------- -------------ensure------------------- -------------first------------------- ----------------invoke----------------------- -------------third------------------- 我感觉 dispatch! 这里的 invoke 完全可以去掉,还能提高性能。 5.process_route method 分析 代码如下:
def process_route(pattern, keys, conditions, block = nil, values = [])
route = @request.path_info
route = '/' if route.empty?
# 判断是否匹配
return unless match = pattern.match(route)
values += match.captures.to_a.map { |v| force_encoding URI.decode_www_form_component(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,先忽略掉
conditions.each { |c| throw :pass if c.bind(self).call == false }
# 执行block
block ? block[self, values] : yield(self, values)
end
ensure
@params = original if original
end
这个方法也不复杂 1.match = pattern.match(route) 判断 request.path_info 是否符合条件,不符合条件 return