rack 是一个协定(protocol),是一个接口(interface),来传递 HTTP request/response。透过 rack 能够连结到我们的 Ruby web app。

现在我们先来写个简单的 rack app,如果你的还没装 rack,可以下指令 gem install rack 来安装。
# first_rack_app.rbrequire "rack"class FirstRackApp def call(env) status = 200 headers = {"Content-Type" => "text/html"} body = ["This is my first web app"] return [status, headers, body] endendRack::Handler::WEBrick.run(FirstRackApp.new, Port:3001)执行 ruby first_rack_app.rb 后,去 http://localhost:3001/ 就能看到 This is my first web app 这行字了,若想要让它中断下指令 ctrl c 即可。
由于 Ruby 可以省略
return以及()的特性,所以这段 code 可以更简洁写成
# first_rack_app.rbrequire "rack"class FirstRackApp def call(env) status = 200 headers = {"Content-Type" => "text/html"} body = ["This is my first web app"] [status, headers, body] endendRack::Handler::WEBrick.run FirstRackApp.new, Port:3001这段code很简单,我写了一个rack app,裡面定义了一个 call method,它回传一个 array 。依序是
若是不懂 Status code、Header 是什麽,可以参考下面两篇
于是,你可以在一些 rack 文章上面看到更简洁的写法
# first_rack_app.rbrequire "rack"class FirstRackApp def call(env) [200, {"Content-Type" => "text/html"}, ["This is my first web app"]] endendRack::Handler::WEBrick.run FirstRackApp.new, Port:3001rack app的结构很简单:
- 一个
callmethod- 回传一个array
- HTTP status
- header
- body
我们来改一下code,看看这个 env 是什麽吧
# first_rack_app.rbrequire "rack"class FirstRackApp def call(env) [200, {}, [env.inspect]] endendRack::Handler::WEBrick.run FirstRackApp.new, Port:3001接著,我们在Terminal下开一个分页,然后到 first_rack_app.rb 所在的路径位置,下指令 curl http://localhost:3001/ ,于是我们看到了一大串hash。
$ curl http://localhost:3001/{ "GATEWAY_INTERFACE"=>"CGI/1.1", "PATH_INFO"=>"/", "QUERY_STRING"=>"", "REMOTE_ADDR"=>"::1", "REMOTE_HOST"=>"::1", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://localhost:3001/", "SCRIPT_NAME"=>"", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"3001", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"WEBrick/1.3.1(Ruby/2.4.0/2016-12-24)", "HTTP_HOST"=>"localhost:3001", "HTTP_USER_AGENT"=>"curl/7.43.0", "HTTP_ACCEPT"=>"*/*", "rack.version"=>[1, 3], "rack.input"=>#<StringIO:0x007fd4a894fde8>, "rack.errors"=>#<IO:<STDERR>>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "rack.url_scheme"=>"http", "rack.hijack?"=>true, "rack.hijack"=>#<Proc:0x007fd4a894fb18@/Users/nicholas/.rvm/gems/ruby-2.4.0/gems/rack-2.0.1/lib/rack/handler/webrick.rb:74 (lambda)>, "rack.hijack_io"=>nil, "HTTP_VERSION"=>"HTTP/1.1", "REQUEST_PATH"=>"/"}env 也就是environment是一个hash。 env 被传到 call method裡去,于是Rack environment知道了这个request的很多资讯。我们可以看到,这个request是送出一个 GET 请求( "REQUEST_METHOD"=>"GET" )。,也可以看到我们在code裡定义的port 3001( "SERVER_PORT"=>"3001" ),欲了解这些Rack environment的定义可以看Rack doc。
call method上面的范例,我们的rack app裡面,只有定义一个 call method,为何rack要定义这个 call method呢?
在其他Rack的文章你可能会常看到,rack app接一个proc或lambda,像是:
app = proc do |env| ['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]end或
application = lambda do |env| [200, { "Content-Type" => "text/html" }, ["Yay, your first web application! <3"]]end这就是Rack会定义 call method的主要原因。
当我们的Ruby web app 被Rack app呼叫( call )时,Rack app 的实体(instance)其实是一个proc或lambda instance,然后它调用了proc与lambda的default method,也就是 call 。不得不说Rack的作者真的很聪明,想到这写法。
如果对proc或lambda不熟,可以参考我写的block、proc、yield、lambda之间的关係
call :语意化 method 与 pathRack app裡头只有一个 call method, env 是一大串hash,当我们需要hash里头的一些特定资讯时,我的 call method要怎麽写呢?
在此,以 env 中的 "REQUEST_METHOD" 与 "PATH_INFO" 为例,我现在定义一个Rack app
# some_rack_app.rbclass SomeRackApp def call handle_request(env['REQUEST_METHOD'], env['PATH_INFO']) end private def handle_request(method, path) if method == "GET" get(path) else method_not_allowed(method) end end def get(path) [200, {"Content-Type" => "text/html"}, ["You have requested the path #{path}, using GET"]] end def method_not_allowed(method) [405, {}, ["Method not allowed: #{method}"]] endend这段 code 很简单,当 rack app 调用 call 时,会把 env hash得到的 REQUEST_METHOD 与 PATH_INFO 传进我们自己定义的 handle_request method裡面。
接著用一个 if..else 做判断,如果 REQUEST_METHOD 是GET,就调用自定义的 get method来回传一组array,如果 REQUEST_METHOD 不是GET,就用自定义的 method_not_allowed ,回传另一组array
如果我们把所有的逻辑都写在 call 裡头,时间久了后再去读code就很难读,但我们把它语意化包成一个个method,就会非常直觉了。
private在Ruby裡面是一个method,在Ruby的privatemethod其实不只Class自己内部可以存取,它的SubClass也可以存取。如果对private的意义不熟,可以参考这篇文章
回到最前面最简单的rack app, first_rack_app.rb 最结尾有一行 Rack::Handler::WEBrick.run FirstRackApp.new, Port:3001 ,这个 Rack::Handler 是什麽? WEBrick 是什麽?
Rack用handler来跑( run ) Rack app,在此我们选的是 WEBrick 这个handler。
我们可以在env hash中看到 "SERVER_SOFTWARE"=>"WEBrick/1.3.1(Ruby/2.4.0/2016-12-24)" 。
WEBrick是ruby内建的HTTP server,其他还有些常见的还有:Puma、Unicorn, Thin, Apache via Passenger...。
假如我们要用Puma的话,就写
require "rack/handler/puma"...Rack::Handler::Puma.run FirstRackApp.new, Port:3001Rack::Builder 与 config.ru原本要启动rack app,必须写一个Ruby档,然后 require "rack" ,启动时也要写一大串 Rack::Handler::WEBrick.run 。
若想要用更简洁地写法启动rack app,则我们可以透过 Rack::Builder 。首先新增一个 config.ru
# config.ruclass FirstRackApp def call(env) [200, {"Content-Type" => "text/html"}, ["This is my first web app"]] endendrun FirstRackApp.new然后在 config.ru 所在的位置,下指令 rackup -p 3001 ,接著去浏览器 http://localhost:3001/ 就能看到 "This is my first web app"
config.ru中的ru就是指rackup,所以才会在Rebuilding rails上看到用rackup -p 3001这指令来启动我们的Ruby web app。
Rack::Builderis a pattern that allows us to more succinctly define our application and the middleware stack surrounding it.ref
推荐这篇文章
Rack::Builder这节,说明了不用config.ru是怎麽写Rack::Builderin
config.ru,useadds middleware to the stack,rundispatches to an application. You can usemapto construct a Rack::URLMap in a convenient way.ref
这样写,并不只是为了更简洁,而是要带入middleware的概念
Middleware本身就是一个rack app。
在 config.ru 裡可以用 use 调用先前编写的一支支Middlewares,例如:
# config.rurequire './app.rb'require './middleware1.rb'require './middleware2.rb'require './middleware3.rb'use Middleware1use Middleware2use Middleware3run App.new
如上面的code所示,Rack::Builder依序使用了三个middleware,最后才执行App。用这样的方式产生出一个Rack app
这段code的效果等同如下
rack_app = Middleware1.new(Middleware2.new(Middleware3.new(App)))文章开头时,我们看到下面这张图,帮助我们理解rack与user以及我们的Ruby web app的关係。

我们可以看到user送出的request先通过rack server然后才送到Ruby web app。
也就是说,user送出的request先经过rack app的处理后,才送进我们的ruby web app,透过下面这张图,能直观地了解rack app做了什麽事

一层又一层的Middleware拦截了我们的request并对他做处理与修改,如此一来Ruby web app所收到的request,就只会有这app所需要的资讯,而不会有其他多馀的资讯。
这样的模式称之为pipeline design pattern,推荐阅读下面两篇,尤其是维基百科的那张图,看了会很有感觉
- 指令管线化(Instruction pipeline) - 维基百科
- comment: ruby on rails - What is Rack middleware? - Stack Overflow
Rack middleware is more than "a way to filter a request and response" - it's an implementation of the pipeline design pattern for web servers using Rack.
It very cleanly separates out the different stages of processing a request - separation of concerns being a key goal of all well designed software products.
config.ru在 config.ru 裡,可以用 run 、 map 、 use 这三个method
run :执行一个proc或lambda的instanceuse :告诉我们的Rack app要使用什麽middlewaremap :告诉Rack app什麽路径( path ),要用什麽middleware与rack app来处理在这边给一个 map 很具体的例子
# config.rurequire './main_rack_app.rb'require './admin_rack_app.rb'require './first_middleware.rb'require './second_middleware.rb'map '/' do use FirstMiddleware run MainRackApp.newendmap '/admin' do use SecondMiddleware run AdminRackApp.newend
当送过来的requets,它的 "REQUEST_PATH"=>"/" 就给 FirstMiddleware 与 MainRackApp 处理。
当送过来的requets,它的 "REQUEST_PATH"=>"/admin" 就给 SecondMiddleware 与 AdminRackApp 处理。
如此一来,我们就能依照不同路由,来给我们的request通过不同的middleware与rack app进行处理。
rack也有提供不少包好的middleware让人使用
这边就直接给一个简单的例子,延续先前的 config.ru 范例
原本我们知道的 config.ru 大概会这样写
# 原始config.ruclass MyApp def call(env) [200, {"Content-Type" => "text/html"}, ["This is my app"]] endendrun FirstRackApp.new现在我把rack app的部分独立成 my_app.rb
# my_app.rbclass MyApp def call(env) [200, {"Content-Type" => "text/html"}, ["This is my app."]] endend这个时候, config.ru 应该改写成
# config.rurequire "./my_app.rb"app = MyApp.newrun app
接著我写一个简单的middleware, my_app_middleware.rb
Middleware会用class包起来,先定义 initialize method,再定义 call method。
# my_app_middleware.rbclass MyAppMiddleware def initialize(app) @app = app end def call(env) code, headers, body = @app.call(env) message = "This is my app middleware." body << message [code, headers, body] endend
如上面code所示,我先透过 initialize method去捞出rack app,接著再定义 call method。
在middleware的 call method裡,一开始先用 @app.call 去捞出我原本在 my_app.rb 裡定义call食捞出的array,然后存成 code 、 headers 、 body
接著我塞一串字串进到body裡去,最后再回传一个array。
我回到 config.ru 来使用我刚刚写好的middleware
require "./my_app.rb"require "./my_app_middleware.rb"app = MyApp.newuse MyAppMiddlewarerun app
然后一样下指令 rackup -p 3001 ,接著你就能在 http://localhost:3001/ 看到 " This is my app.This is my app middleware. "
再来个 map 的具体实例吧。
现在我创造另一个middleware hello_nick_middleware.rb
# hello_nick_middleware.rbclass HelloNickMiddleware def initialize(app) @app = app end def call(env) code, headers, body = @app.call(env) body << "Hello, Nick" [code, headers, body] endend
然后再修改一下 config.ru
# config.rurequire "./my_app.rb"require "./my_app_middleware.rb"require "./hello_nick_middleware.rb"require "./sao.rb"app = MyApp.newsao = SAO.newmap '/' do use MyAppMiddleware run appendmap '/nick' do use HelloNickMiddleware run saoend
rackup 之后,你可以去 http://localhost:3001/nick 看到我们第二个middleware所插入进去的字串
另外要注意一下, config.ru 的top-level context只能有一个 run ,像是
# config.ru... #上面略map '/admin' do use MyAppMiddleware run appendmap '/nick' do use HelloNickMiddleware run saoendrun myapp
top-level所
run的rack app,会预设是/路径的request,依此逻辑在top level裡run两个不同的rack app就没有意义了。若不清楚什麽是top level可以参考下面两篇文章
在前面的“更细腻地操作 call :语意化method与path”这节,曾看到用 env["REQUEST_METHOD"] 来查看用什麽request method,rack提供更便利的方式来操作 env 这hash裡的 key-value pairs。
request = Rack::Request.new(env)Rack::Request 提供了一些method让我们不用直接操作 env 的hash
# 等同于 env["REQUEST_METHOD"]request.request_method# 等同于 env["rack.request.query_hash"] + env["rack.input"]request.params# 等同于 env["REQUEST_METHOD"] == "GET"request.get?最后简单两个范例常见的 Rack:Request 与 Rack:Response
建一个 my_app1.rb
require 'rack'class MyApp1 def call(env) # 创建一个request对象,就能享受Rack::Request的便捷操作了 @request = Rack::Request.new env method = @request.request_method info = @request.path_info puts "This website request method is `#{method}` " puts "This website path info is `#{info}`" [200, {}, ['hello world']] endendRack::Handler::WEBrick.run MyApp1.new, Port:3001执行 ruby my_app1.rb 然后去 http://localhost:3001/ ,接著你可以在Terminal看到
$ ruby my_app1.rb[2017-03-13 16:14:58] INFO WEBrick 1.3.1[2017-03-13 16:14:58] INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin14][2017-03-13 16:14:58] INFO WEBrick::HTTPServer#start: pid=61940 port=3001This website request method is `GET`This website path info is `/`localhost - - [13/Mar/2017:16:15:01 CST] "GET / HTTP/1.1" 200 11- -> /This website request method is `GET`This website path info is `/favicon.ico`我们确实可以看到这个request用的method与path information
This website request method is `GET`This website path info is `/`PS:会看到
/favicon.ico是因为浏览器会去抓网站的logo,可以参考下面两篇
建一个 my_app2.rb
require 'rack'class MyApp2 def call(env) # 创建一个request对象,就能享受Rack::Request的便捷操作了 @response = Rack::Response.new env @response.set_cookie('token', 'xxxxxxx123') @response.headers['Content-Type'] = 'text/plain' puts @response.inspect [200, {}, ['hello world']] endendRack::Handler::WEBrick.run MyApp2.new, Port:3001执行 ruby my_app2.rb 然后去 http://localhost:3001/ ,接著你可以在Terminal看到很长一大串
$ ruby my_app2.rb[2017-03-13 16:33:03] INFO WEBrick 1.3.1[2017-03-13 16:33:03] INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin14][2017-03-13 16:33:03] INFO WEBrick::HTTPServer#start: pid=62319 port=3001#<Rack::Response:0x007f957b99b520 @status=200, @header={"Content-Length"=>"7805", "Set-Cookie"=>"token=xxxxxxx123", "Content-Type"=>"text/plain"}, @writer=#<Proc:0x007f957b99aad0@/Users/nicholas/.rvm/gems/ruby-2.4.0/gems/rack-2.0.1/lib/rack/response.rb:32 (lambda)>,......(后面省略)不过你可以看到,我们在程式裡写的cookie与header都成功写到response裡去了
更多 method 可以参考 Rack doc

