鐵人賽已經接近尾聲,今天要來聊聊 Rails 很重要的一個部分,也是初學者比較少了解到的東西,就是Rack Middleware
,你可以在手邊的 Rails 專案裡面下這個指令
rake middleware
會發現類似像這樣的東西
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
不知道大家有沒有做過千層蛋糕?要先用平底鍋做出一片一片的餅皮,然後在一層一層的堆疊上去,Middleware
其實就有點像是千層蛋糕,透過一層層的服務所加疊起來的 Rack Application,換句話說利用這個概念,可以選擇去增加或減少某些服務,為了讓大家更了解 Rack Middleware
,接下來會做一些小範例
首先我們開啟一個新的資料夾,並且新增一個 config.ru
,加上以下的程式碼
# demo/config.ru
run proc {
[200, {'Content-Type' => 'text/html'},
["Hello, world!"]]
}
我們建立一個簡單的 Rack Application,並且回應 200
,這裡相信大家都已經不陌生
或是你也可以這樣寫
# demo/config.ru
obj = Object.new
def obj.call(*args)
[200, {'Content-Type' => 'text/html'},
["Hello, world!"]]
end
run obj
只要 run
一個有 call
method 的物件,一樣可以運作
現在我們來加上另一個服務看看
# demo/config.ru
SERVICE_B = proc {
"I am Service B!"
}
SERVICE_A = proc {
service_b_string = SERVICE_B.call
[200, {'Content-Type' => 'text/html'},
["I am Service A and " + service_b_string]]
}
run SERVICE_A
# 執行結果 I am Service A and I am Service B!
這裡我們建立了有兩個服務的 Rack Middleware
,在最底下我們用 run
先執行 SERVICE_A
,並且在 SERVICE_A
裡面去呼叫 SERVICE_B
以上範例只是基本的概念,但實際應用上並不會在 SERVICE_A
裡面直接呼叫 SERVICE_B
,這樣如果那天使用者把 B
拿掉,那 A
就會跟著壞掉,我們希望這些服務只專注在自己的事情上面,服務與服務之間並不知道對方的存在,唯一聯繫的方式就是透過參數傳遞訊息,請看下面例子
# demo/config.ru
use Rack::Auth::Basic, "apa" do |_, pass|
'it_30' == pass
end
run proc {
[200, {'Content-Type' => 'text/html'},
["Hello, world!"]]
}
這裡我們使用了 Rack::Auth::Basic
來做簡單的驗證,開啟伺服器打開瀏覽器觀看以後,會跳出輸入帳號密碼的提示框,輸入 「apa」 和 「it_30」 之後才會做 200
回應,會發現我們這裡用了 use
這個關鍵字來執行另一個服務,而兩個服務之間並不會知道對方的存在,這就是實際上 Rack Middleware
在做的事情,再來看看更複雜的例子
# demo/config.ru
class ServiceA
def initialize(app, arg = "")
@app = app
@arg = arg
end
def call(env)
status, headers, content = @app.call(env)
content[0] += ' I am ' + @arg
[ status, headers, content ]
end
end
use ServiceA, 'apa'
run proc {
[200, {'Content-Type' => 'text/html'},
["Hello, world !"]]
}
# 在瀏覽器上印出 Hello, world ! I am apa
這裡建立了一個 ServiceA
的 class,裡面一樣有一個 call
method,當最底下呼叫的 run 呼叫 proc 之後,因為我們有用 use
來「登記」ServiceA 這個服務,所以會傳入整包 「app」 給 ServiceA
做處理,最後再由 ServiceA 做回應
那如果有多個 use
做登記呢?,例如像下面例子
use ServiceA
use ServiceB
run App.new
上面那段程式碼,實際上就等於下面這段程式碼
run ServiceA.new(
ServiceB.new(
App.new
)
)
所以現在你看到 Rails 的 config.ru 這樣寫,再搭配本篇文章一開頭的 rake middleware
指令
require_relative 'config/environment'
run Rails.application
應該就會比較清楚知道,Rails 背後的服務是怎麼運作的
為了更加深讀者的印象,我們用剛剛所學的觀念,來建立一個簡單的 logger,將 User 使用瀏覽器的瀏覽紀錄印在終端機上
# demo/config.ru
app = lambda do |env|
[200, { 'Content-Type' => 'text/plain' }, ['hello from lambda!']]
end
class Logger
def initialize(app)
@app = app
end
def call(env)
method = env['REQUEST_METHOD']
path = env['PATH_INFO']
puts "#{method} #{path}"
@app.call(env)
end
end
use Logger
run app
這裡我們做了一個 Middleware
叫 Logger
,將接受到的資訊,也就是 env
,取出 REQUEST_METHOD
和 PATH_INFO
並且印出,這樣我們就知道使用者看了那些頁面
實際上我們做得 Logger,也可以用 Rack::CommonLogger
來取代
當然 Rack 其實還能做到更多事情,另外一個有趣的服務是 Rack::URLMap
,能做到類似 routing
的效果,讓我們來看看下面例子
# demo/config.ru
require "rack/lobster"
use Rack::ContentType
map '/lobster' do
use Rack::ShowExceptions
run Rack::Lobster.new
end
map '/lobster/is_disappear' do
run proc {
[200, {}, ['where is lobster?']]
}
end
run proc {
[200, {}, ['I want to eat lobster']]
}
我們在最上面登記了一個 Rack::ContentType
,用預設的方式來回傳 HTML content type
,這樣每個 block 就算沒有回傳 content type
也不受到影響,接著用瀏覽器瀏覽 http://127.0.0.1:3001/
,沒有意外上面會秀出 I want to eat lobster
的字樣,那如果瀏覽 http://127.0.0.1:3001/lobster
呢?
會發現,看到一隻龍蝦!
不要問我為什麼有龍蝦
利用 map
我們可以定義出,哪個 URL 要做哪些事情,這跟我們在 application.rb
裡面做得事情很類似,另外我們也在 /lobster
裡面登記了另一個服務 Rack::ShowExceptions
,當你按下龍蝦那頁的 crash!
時,就會秀出 Exceptions
的頁面
今天介紹了 Rack Middleware
龍蝦 的用法,明天開始就要來實作比較複雜的 routing
話說,又一個連假要繼續寫鐵人賽...