深入淺出 rack
rack是一個協定(protocol),是一個接口(interface),來傳遞 HTTP request/response。透過rack能夠連結到我們的Ruby web app。
一個簡單的rack app
現在我們先來寫個簡單的rack app,如果你的還沒裝rack,可以下指令gem install rack
來安裝。
# first_rack_app.rb
require "rack"
class FirstRackApp
def call(env)
status = 200
headers = {"Content-Type" => "text/html"}
body = ["This is my first web app"]
return [status, headers, body]
end
end
Rack::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.rb
require "rack"
class FirstRackApp
def call(env)
status = 200
headers = {"Content-Type" => "text/html"}
body = ["This is my first web app"]
[status, headers, body]
end
end
Rack::Handler::WEBrick.run FirstRackApp.new, Port:3001
這段code很簡單,我寫了一個rack app,裡面定義了一個call
method,它回傳一個 array 。依序是
- Status Code:200
- Header:一個hash,我請求的Content-Type是text,於是我可以在body寫一串text
- Body:一個array,這array裡面有著一串string
若是不懂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"]]
end
endRack::Handler::WEBrick.run FirstRackApp.new, Port:3001
rack app的結構很簡單:
1. 一個
call
method2. 回傳一個Array,依序是:HTTP status, header, body
env是什麼?
我們來改一下code,看看這個env
是什麼吧
# first_rack_app.rb
require "rack"
class FirstRackApp
def call(env)
[200, {}, [env.inspect]]
end
end
Rack::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。
為何rack app要定義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與path
Rack app裡頭只有一個call
method,env
是一大串hash,當我們需要hash裏頭的一些特定資訊時,我的call
method要怎麼寫呢?
在此,以env
中的"REQUEST_METHOD"
與"PATH_INFO"
為例,我現在定義一個Rack app
# some_rack_app.rb
class 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}"]]
end
end
這段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的private
method其實不只Class自己內部可以存取,它的SubClass也可以存取。如果對private的意義不熟,可以參考這篇文章:
Rack::Handler
回到最前面最簡單的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:3001
Rack::Builder
與config.ru
原本要啟動rack app,必須寫一個Ruby檔,然後require "rack"
,啟動時也要寫一大串Rack::Handler::WEBrick.run
。
若想要用更簡潔地寫法啟動rack app,則我們可以透過Rack::Builder
。首先新增一個config.ru
# config.ru
class FirstRackApp
def call(env)
[200, {"Content-Type" => "text/html"}, ["This is my first web app"]]
end
end
run 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::Builder
is a pattern that allows us to more succinctly define our application and the middleware stack surrounding it.ref:What is Rack? | Ruby on Rails Tutorial by thoughtbot
推薦這篇文章
Rack::Builder
這節,說明了不用config.ru
是怎麼寫Rack::Builder
ref:Advanced Rack — Gabe Berke-Williams
in
config.ru
,use
adds middleware to the stack,run
dispatches to an application. You can usemap
to construct a Rack::URLMap in a convenient way.ref:Class: Rack::Builder — Documentation for rack/rack (master)
這樣寫,並不只是為了更簡潔,而是要帶入middleware的概念
Middleware的概念
Middleware本身就是一個rack app。
在config.ru
裡可以用use
調用先前編寫的一支支Middlewares,例如:
# config.ru
require './app.rb'
require './middleware1.rb'
require './middleware2.rb'
require './middleware3.rb'
use Middleware1
use Middleware2
use Middleware3
run 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,推薦閱讀下面兩篇,尤其是維基百科的那張圖,看了會很有感覺
1. 指令管線化(Instruction pipeline) — 維基百科
2. 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.
Middleware與config.ru
在config.ru
裡,可以用run
、map
、use
這三個method
run
:執行一個proc或lambda的instanceuse
:告訴我們的Rack app要使用什麼middlewaremap
:告訴Rack app什麼路徑(path
),要用什麼middleware與rack app來處理
在這邊給一個map
很具體的例子
# config.ru
require './main_rack_app.rb'
require './admin_rack_app.rb'
require './first_middleware.rb'
require './second_middleware.rb'
map '/' do
use FirstMiddleware
run MainRackApp.new
end
map '/admin' do
use SecondMiddleware
run AdminRackApp.new
end
當送過來的requets,它的"REQUEST_PATH"=>"/"
就給FirstMiddleware
與MainRackApp
處理。
當送過來的requets,它的"REQUEST_PATH"=>"/admin"
就給SecondMiddleware
與AdminRackApp
處理。
如此一來,我們就能依照不同路由,來給我們的request通過不同的middleware與rack app進行處理。
rack也有提供不少包好的middleware讓人使用。
Middleware的具體細節
這邊就直接給一個簡單的例子,延續先前的config.ru
範例
原本我們知道的config.ru
大概會這樣寫
# 原始config.ru
class MyApp
def call(env)
[200, {"Content-Type" => "text/html"}, ["This is my app"]]
end
end
run FirstRackApp.new
現在我把rack app的部分獨立成my_app.rb
# my_app.rb
class MyApp
def call(env)
[200, {"Content-Type" => "text/html"}, ["This is my app."]]
end
end
這個時候,config.ru
應該改寫成
# config.ru
require "./my_app.rb"
app = MyApp.new
run app
接著我寫一個簡單的middleware,my_app_middleware.rb
Middleware會用class包起來,先定義initialize
method,再定義call
method。
# my_app_middleware.rb
class 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]
end
end
如上面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.new
use MyAppMiddleware
run 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.rb
class HelloNickMiddleware
def initialize(app)
@app = app
end
def call(env)
code, headers, body = @app.call(env)
body << "Hello, Nick"
[code, headers, body]
end
end
然後再修改一下config.ru
# config.ru
require "./my_app.rb"
require "./my_app_middleware.rb"
require "./hello_nick_middleware.rb"
require "./sao.rb"
app = MyApp.new
sao = SAO.new
map '/' do
use MyAppMiddleware
run app
end
map '/nick' do
use HelloNickMiddleware
run sao
end
rackup
之後,你可以去http://localhost:3001/nick
看到我們第二個middleware所插入進去的字串
另外要注意一下,config.ru
的top-level context只能有一個run
,像是
# config.ru
... #上面略
map '/admin' do
use MyAppMiddleware
run app
end
map '/nick' do
use HelloNickMiddleware
run sao
end
run myapp
top-level所
run
的rack app,會預設是/
路徑的request,依此邏輯在top level裡run
兩個不同的rack app就沒有意義了。若不清楚什麼是top level可以參考下面兩篇文章
2. What is the Ruby Top-Level? | Like Dream of Banister Fiend
一些常用的東西
在前面的「更細膩地操作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
Rack:Request的例子
建一個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']]
end
endRack::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=3001
This 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,可以參考下面兩篇1. The Will Will Web | 介紹 Favicon.ico 重點觀念
2. [架站] 為什麼網站的根目錄最好有 favicon.ico 和 robots.txt 存在? | jjdai Blog
Rack::Response的例子
建一個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']]
end
endRack::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
- Class: Rack::Request — Documentation for rack (2.0.1)
- Class: Rack::Response — Documentation for rack/rack (master)
ref
- Advanced Rack — Gabe Berke-Williams
- What is Rack? | Ruby on Rails Tutorial by thoughtbot
- Your first Rack app | Webapps for Beginners
- The Rack Env | Webapps for Beginners
- Method and Path | Webapps for Beginners
- Web Development in Ruby : Environment Variables in Rack App
- Ruby Rack及其应用(上)
- Ruby进阶之Rack入门 — 简书
- Ruby进阶之Rack深入 — 简书
- JoelQ/intro-rack-notes: Intro to Rack
- Understanding Rack Apps and Middleware