前段时间写了篇文章<为什么我们需要 Rack?>, 再过段时间,对 rack 一直都是一个知其然,不知道其所以然的状态。最近从源码读起,这里与大家分享一下. 注意:本帖前半段极其无聊,请大家注意。
首先,创建config.ru
. 这里直接引用前文的例子。
#config.ru
# 将 body 标签的内容转换为全大写.
class ToUpper
def initialize(app)
@app = app
end
def call(env)
status, head, body = @app.call(env)
upcased_body = body.map{|chunk| chunk.upcase }
[status, head, upcased_body]
end
end
# 将 body 内容置于标签, 设置字体颜色为红色, 并指明返回的内容为 text/html.
class WrapWithRedP
def initialize(app)
@app = app
end
def call(env)
status, head, body = @app.call(env)
red_body = body.map{|chunk| "<p style='color:red;'>#{chunk}</p>" }
head['Content-type'] = 'text/html'
[status, head, red_body]
end
end
# 将 body 内容放置到 HTML 文档中.
class WrapWithHtml
def initialize(app)
@app = app
end
def call(env)
status, head, body = @app.call(env)
wrap_html = <<-EOF
<!DOCTYPE html>
<html>
<head>
<title>hello</title>
<body>
#{body[0]}
</body>
</html>
EOF
[status, head, [wrap_html]]
end
end
# 起始点, 只返回一行字符的 rack app.
class Hello
def initialize
super
end
def call(env)
[200, {'Content-Type' => 'text/plain'}, ["hello, this is a test."]]
end
end
use WrapWithHtml
use WrapWithRedP
use ToUpper
run Hello.new
在 termial 中输入rackup
, 而后,一个 web 服务就运行了.
关于这段例子,前文的解释是:
use 与 run 本质上没有太大的差别,只是 run 是最先调用的。
它们生成一个 statck(谢谢 @est 指出错误) 本质上是先调用 Hello.new#call, 而后返回 ternary-array, 而后再将之交给另一个 ToUpper, ToUpper 干完自己的活,再交给 WrapWithRedP, 如此一直到 stack 调用完成.use ToUpper; run Hello.new
本质上是完成如下调用:ToUpper.new(Hello.new.call(env)).call(env)
看到这里,个人有许多疑问:
rackup
命令来自于哪里?rackup
命令如何找到config.ru
这个文件的,又是如何加载它运行它的,或者说它是如何加载代码创建服务的?use
后面跟着的是 middleware 这个知道,但是,它是如何生成加载的 stack 的?run
?call
方法就可以了,它们如何工作?那么,从源码一行一行看过去吧,首先是rackup
, 放在rack
项目的bin
文件夹下,只有几行:
#!/usr/bin/env ruby
require "rack"
Rack::Server.start
好吧,个人表示不理解,于是跳到 lib\server.rb
# lib\server.rb
def self.start(options = nil)
new(options).start
end
-_-!
# lib\server.rb
def initialize(options = nil)
@options = options
@app = options[:app] if options && options[:app]
end
-_-! 好吧,Rack::Server.start
时一个参数也没有。因此 new
=> initialize
时也一个参数也没有。也即是说最后从类方法start
又跳回了实例方法start
上:
# lib\server.rb
# 这该死的start实例方法相当之长, 这里只引用关键点:
def start &blk
# ...
server.run wrapped_app, options, &blk
# ...
end
好吧,server 来自于方法,wrapped_app 也是个方法:
# lib\server.rb
def server
@_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
end
def wrapped_app
@wrapped_app ||= build_app app
end
def build_app(app)
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
klass, *args = middleware
app = klass.new(app, *args)
end
app
end
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
这方法间引来引去的,实在让人抓狂,这里直接给出关键点吧。以下内容也不帖出这该死的方法链了,只帖关键点。这样写下去不如直接帖源码自己看不是吗?(我承认我只是表达一下我抓狂的心情)
关键点在于:server 方法的后半段Rack::Handler.default(options)
以及 wrapped_app
方法.
这里不卖关子,前者直接决定了调用哪个服务器来运行项目,是thin
还是mongrel
, WEBrick
之类的.
wrapped_app
与app
两个方法则决定了会加载config.ru
的内容作为运行项目.
options[:builder] ? build_app_from_string : build_app_and_options_from_config
这一行可知,因为 option 根本没指定,所以会调用 build_app_from_string
.
# lib\server.rb
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
于是我们华丽地滚到了lib\builder.rb
文件:
# lib\builder.rb
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
好吧,历经千动百难我们终于找到了正主。这个builder_script
就是config.ru
文件的内容了。eval
方法在TOPLEVEL_BINDING
环境下,也即我们运行rackup
命令的环境下运行代码.
....但是,妈蛋,这builder_script
来自于哪里?self.options[:builder]
作为参数传进来的,但是泥煤的这个 options 不是一直都是空的吗!!?
好吧,这个不是options
, 这个是options
类方法。属于Rack::Server
类.
经研究,顺序是这样的:options 方法里引用parse_options(ARGV)
也即是分析用户输入内容,但是明显我们没有输入任何内容。parse_options
方法开篇第一句:options = default_options
, 而后在后面的代码分析 ARGV, 死命地覆盖它。
default_options
是这个方法,里面的一行指定了,options[:config]
, 是config.ru
文件。
# lib\server.rb
def options
@options ||= parse_options(ARGV)
end
def parse_options(args)
options = default_options
# Don't evaluate CGI ISINDEX parameters.
# http://www.meb.uni-bonn.de/docs/cgi/cl.html
args.clear if ENV.include?(REQUEST_METHOD)
options.merge! opt_parser.parse!(args)
options[:config] = ::File.expand_path(options[:config])
ENV["RACK_ENV"] = options[:environment]
options
end
def default_options
environment = ENV['RACK_ENV'] || 'development'
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
{
:environment => environment,
:pid => nil,
:Port => 9292,
:Host => default_host,
:AccessLog => [],
:config => "config.ru"
}
end
思维回溯,config.ru
终于传入eval
之中了,我们的项目找到根脚了,个人不禁泪流满面。而 Rack::Builder.initialize
最后会将其传入的 block 通过instance_eval
方法运行其实例变量环境之中。也即是config.ru
的所有文本内容,在Rack::Builder
实例化后,在其环境下调用了。于是 use 生成 stack, 于是 use 方法将 middlewares 加入到@use array 中去,run 方法将其参数设置为起始项目并存入@app, 而后再通过 to_app 将之转化为 stack 待用,最后用户发起请求,server 调用 call 方法,并传入 env. env 在主项目@app, 和 @use 中的 middlewares 中薪火相传,每经手一次,就可能被做些修改,最后,返回值 .
再回到builder.rb
文件,也即eval
所在的内容:
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
好吧,我们知道它调用了类Rack::Builder
类的initialize
方法。
def initialize(default_app = nil,&block)
# middlewares 默认为空[]; @map默认不存在. @run 默认等于创建时的项目, @warmup默认不存在.
@use, @map, @run, @warmup = [], nil, default_app, nil
# 在Builder实例中运行block, 也即是在builder运行环境中再执行block中的代码.
instance_eval(&block) if block_given?
end
太过细节的东西不要再讲了,我们再简化一下吧:Rack::Builder
类是核心所在。它包含了生成 middlewares stack 的核心代码,决定了 middlewares 的调用顺序。也决定了它们如何被调用。它提供的use
, map
, run
, to_app
等方法,决定了一切:
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) }
@use
end
def run(app)
@run = app
end
def map(path, &block)
@map ||= {}
@map[path] = block
end
def to_app
app = @map ? generate_map(@run, @map) : @run
fail "missing run or map statement" unless app
app = @use.reverse.inject(app) { |a,e| e[a] }
@warmup.call(app) if @warmup
app
end
def call(env)
to_app.call(env)
end
use
方法将 middleware 放到一个 proc 之中,再将此 proc 放到@use这个 array 之中。run
方法将起始项目放到@app实例变量之中。map
方法用于生成简单的 rack 项目内路由。暂时不放入讨论之中。to_app
方法生成 stack, 并等待最后从 server 处传传进来。用户打开浏览器地址,server(如 thin) 就会调用这个 call 方法,并传入 env 作为其值。问题的关键点在于to_app
方法最后返回的那个app
, 它本质上已经将项目以及 middlewares 全部串在了一起,并且保证一次调用 call, 就会按规则走完 middlewares 全流程。_ (是否对前面那么啰嗦,后面语言简洁的转折表示不适应?)
这里不再去解释源码的调用链了,我们来写一段小程序来模拟这个创建 stack,并调用它们的全过程:
# t5.rb
@use = []
class Mdw
def initialize(app)
@app = app
end
def call(env)
puts env = "mdw1:" + env
@app.call(env)
end
end
class Mdw2
def initialize(app)
@app = app
end
def call(env)
puts env = "mdw2:" + env
@app.call(env)
end
end
startApp = proc {|app| puts tmp = 'startApp:' + app }
def use(md)
@use << proc {|app| md.new(app) }
end
def call(s)
@use.reverse.inject(@app){|a, w| w[a] }.call(s)
end
def run(a)
@app = a
end
use Mdw
use Mdw2
run startApp
call('env')
运行ruby t5.rb
, 可以得到如下内容:
mdw1:env
mdw2:mdw1:env
startApp:mdw2:mdw1:env
startApp
与middlewares
中用的puts
方法输出了值的全变化过程.
这里没有模拟rack
返回[statusCode, Headers, Body]
模式的部分。只模拟了链式调用的生成,以及调用的过程。
一切的关键在于@use.reverse.inject(@app){|a, w| w[a] }.call(s)
这行代码,在 @use 变量上调用 inject 的过程,以及 block 调用的特殊方法。
有多少人知道,proc/lambda是可以通过[ ]
来调用的?
关于inject
:
Combines all elements of enum by applying a binary operation, specified by a block or a symbol that names a method or operator. If you specify a block, then for each element in enum the block is passed an accumulator value (memo) and the element. If you specify a symbol instead, then each element in the collection will be passed to the named method of memo. In either case, the result becomes the new value for memo. At the end of the iteration, the final value of memo is the return value for the method. If you do not explicitly specify an initial value for memo, then the first element of collection is used as the initial value of memo.
关于block
:
prc[params,...] Invokes the block, setting the block’s parameters to the values in params using something close to method calling semantics. Generates a warning if multiple values are passed to a proc that expects just one (previously this silently converted the parameters to an array). Note that prc.() invokes prc.call() with the parameters given. It’s a syntax sugar to hide “call”.
array.inject(i){|a, b| b[a]}
的运行过程中,前一轮的 b[a] 被存到内存中,并加载了下一轮的 a 变量中。因为 a 和 b 都是 proc, 所以 procA[procB] 本质上是调用这个代码块。在这个过程中,初始点 app, 会被放在最底层,而 middleware 会一层一层套在上面。@use.reverse.inject(@app){|a, w| w[a] }
返回的是最外层的那个 middleware 的实例。模拟的代码,只是将 call() 方法手动引用了而已.(本来,它是等着用户行为,再由 server 来调用,传值的)
下面附上示例代码t5.rb
运行过程的推衍过程:
注意:以下内容极其无聊,请精神正常的人不要较真:
最后数行代码的推衍过程
# 前面定义middleware类的过程请无视.
# step 1, `use Mdw;useMdw2;run startApp`
@app = proc {|app| puts tmp = 'startApp:' + app }
@use = [proc {|app| Mdw.new(t) }, proc {|app| Mdw2.new(t) }]
# step 2: `@use.reverse.inject(@a){|a, w| w[a] }`
# step 2.1, 第一轮
a = proc {|app| puts tmp = 'startApp:' + app }
e = proc {|app| Mdw2.new(t)
# step 2.2, 第二轮
a = proc {|app| Mdw2.new(t)[proc {|app| puts tmp = 'startApp:' + app }]
# 实际值: a = Md2.new(proc {|app| puts tmp = 'startApp:' + app })
e = proc {|app| Mdw.new(t) }
# step 2.3, 第三轮
a = proc {|app| Mdw.new(t) }[proc {|app| Mdw2.new(t)[proc {|app| puts tmp = 'startApp:' + app }]]
# 实际值: a = proc {|app| Mdw.new(t) }[Mdw2.new(proc {|app| puts tmp = 'startApp:' + app })]
# 实际值: a = Mdw.new(Mdw2.new(proc {|app| puts tmp = 'startApp:' + app }))
# step 3, @use.reverse.inject(@a){|a, w| w[a] }.call('s')
# step 3.1
Mdw.new(Md2.new(proc {|app| puts tmp = 'startApp:' + app })).call('s')
# step 3.2
Mdw2.new(proc {|app| puts tmp = 'startApp:' + app }).call('Mdw1:' + 's')
# step 3.3
proc {|app| puts tmp = 'startApp:' + app }.call('Mdw2:'+'Mdw1:' + 's')
'startApp:' + 'Mdw2:'+'Mdw1:' + 's'
# 如果想实现链式调用, 一个作为另一个的参数, 形成套娃机制. 需要彼此为参数的设定.
看代码,何其苦!虽说真正的猛士敢于面对惨淡的人生,敢于正视淋漓的鲜血,但是且容我们叫一声苦。