Rack 應用程式
什麼是 Rack?
根據 Rack 網站上的介紹:
Rack provides a minimal interface between webservers that support Ruby and Ruby frameworks.
Rack 提供了一個介面,讓你的 Ruby 程式可以與 Web Server 之間進行溝通,從最簡單的 Ruby 程式,到輕量級的 Ruby 框架(例如 Sinatra),甚至是本書一直在介紹的 Rails,說到底它們都是一種 Rack 的應用程式。
而 Rack 期望的規格很簡單:
To use Rack, provide an “app”: an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements:
簡單的說,就是提供一個能夠回應 call
方法的物件,並且回傳一個包含以下三個元素的陣列:
- HTTP 狀態(數字型態,例如正常回應是 200、找不到頁面是 404、伺服器錯誤是 500)。
- HTTP Header(Hash 型態)。
- Body(陣列型態,或是只要是個可以回應
each
方法的物件也可)。
用文字敘述看起來有點複雜也不太容易解釋,就直接動手來寫寫看吧。
最簡單的 Rack 應用程式
為了讓環境單純一點,先找個你喜歡的目錄,建立一個 rack_demo
的目錄,然後在這個目錄裡執行 Rack 程式 rackup
:
$ mkdir rack_demo
$ cd rack_demo
$ rackup
configuration /private/tmp/rack_demo/config.ru not found
當你執行 rackup
之後,會發現它出現一個訊息,表示它需要一個名為 config.ru
的設定檔,這個 ru
就是 rackup
的縮寫。接著就手動建立這個檔案,並在裡面寫一小段程式:
# 檔案:config.ru
run Proc.new {
[200, { "Content-Type" => "text/html" }, ["Hello, Rack"]]
}
說明:
- Rack 期望要有一個可以
call
的物件,所以這裡做了一個Proc
物件給它執行。 - 執行後要回傳一個陣列,所以我直接在
Proc
物件裡放了一個陣列,當它被call
之後會回傳。
接著再次執行 rackup
:
$ rackup
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.1-p111), codename: Sassy Salamander
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:9292
Use Ctrl-C to stop
沒打錯字的話,Rack 就會在本機的 Port 9292 開啟服務,開啟瀏覽器連到 http://localhost:9292/
,應該可以看到這個畫面:
就這樣,你就完成你的第一個 Rack 應用程式了。不要以為這樣沒什麼,即使像 Rails 這麼複雜的框架說穿了其實也就是在 Rack 上面往上疊而已。
如果想要改變預設的 Port 9292,可使用參數
rackup -p 3000
,就會在 Port 3000 啟動服務。
只要有支援 call
方法就行了吧?
既然 Rack 只期待可以回應 call
的物件就可以了,其實也不一定要用 Proc
來寫,也可直接寫個一般的 Ruby 類別,然後加個 call
的實體方法給它:
class App
def call(env)
[200, { "Content-Type" => "text/html" }, ["Hello, Rack from Class"]]
end
end
run App.new
要注意的是,Rack 在呼叫 call
方法的時候會把環境參數也傳進來,所以在上面這個例子中,call
方法需要加上個參數,否則會發生錯誤。
重新啟動 rackup
指令,應該會在畫面上看到「Hello, Rack from Class」字樣了。
其實這樣也行…
除了 Proc
跟上述寫一個類別再加 call
方法之外,也可以直接寫一個方法,然後使用 Ruby 內建的 method
方法把它包成 Method
物件,這個方法剛好也可以 call
:
def main(env)
[200, { "Content-Type" => "text/html" }, ["Hello, Rack from Method"]]
end
run method(:main)
method
方法會把 main
方法包成一個 Method
類別的物件。重新啟動 rackup
指令,這次應該會在畫面上看到「Hello, Rack from Method」。
參考資料: Ruby 的 Method 類別 https://ruby-doc.org/core/Method.html
中間其實還可以動一些手腳…
在 Rack 執行的過程中,還可以在中間安插一些流程,我們稱之中介軟體(Middleware)。Middleware 的規格也很單純,就跟一般的 Rack 要求一樣,要可以回應 call
方法:
class GameChanger
def initialize(app, who = "5xRuby")
@app = app
@who = who
end
def call(env)
status, headers, body = @app.call(env)
body << "<br />Powered by #{@who}!"
[status, headers, body]
end
end
use GameChanger, "5xRuby"
run Proc.new {
[200, { "Content-Type" => "text/html" }, ["Hello, Rack"]]
}
說明:
- 在
GameChanger
這個類別裡一樣要有call
方法,它最後也是要回傳跟一般 Rack 程式一樣的三個元素。 - 要掛上這個 Middleware,是使用
use
這個方法。 - 在
use
的時候,它會自動把整個 Rack 程式當做第一個參數傳入,所以你可以看到在GameChanger
類別裡的initialize
方法額外接收了一個參數。 - 如果希望可以再傳入其它的參數,可在類別的
initialize
方法中再加上額外的參數,在使用的時候就會變use GameChanger, "5xRuby"
。 - Middleware 之所以可以一個接一個的執行下去,主要靠的就是在
call
方法裡再繼續執行傳進來的那個物件的call
方法。
上面這段範例執行後,應該可以在畫面上看到在最後一行加上了「Powered by 5xRuby!」的字樣:
Middleware 除了可以自己寫,Rack 本身也有帶一些方便的 Middleware 可以用,例如 Rack::Auth::Basic
,可以讓你簡單的就做到權限控管:
use Rack::Auth::Basic do |username, password|
username == "5xRuby" && password == "my_password"
end
run Proc.new {
[200, { "Content-Type" => "text/html" }, ["Hello, Rack"]]
}
重新啟動 rackup
,應該就會看到要你輸入帳號密碼的畫面:
帳號密碼必須正確才能通關喔。
Middleware 的順序?
另外,如果需要的話,Middleware 也可以掛上好幾個,像這樣:
use GameChanger, "5xRuby"
use GameChanger, "Taiwan"
use GameChanger, "KaoChienLong"
依照 use
的順序,掛在越前面的 Middleware 會越先被執行。為了能更清楚執行的順序,我在 call
方法加上了一行程式碼:
class GameChanger
# ..[略]..
def call(env)
puts "called by #{@who}"
status, headers, body = @app.call(env)
body << "<br />Powered by #{@who}!"
[status, headers, body]
end
end
執行之後會發現結果是:
called by 5xRuby
called by Taiwan
called by KaoChienLong
不過,你如果看瀏覽器會發現畫面是這樣:
咦? 怎麼順序跟剛剛印出來的結果不同? 整個剛好是反過來的呢?
掛了上面這三個 Middleware,在執行 Rack 程式的時候,流程上大概是這樣:
- 當使用
use
方法掛載第一個帶有5xRuby
參數的 Middleware,它會呼叫下一個 Middleware,也就是帶有Taiwan
參數的 Middleware。 - 當執行帶有
Taiwan
參數的 Middleware,它會呼叫帶有KaoChienLong
參數的 Middleware,這也是之所以你會看到這印出來的結果是照這個順序的原因。 - 然後 Rack 應用程式開始執行(run),在這個當下,Body 的內容是
Hello, Rack
。 - 回到帶有
KaoChienLong
參數的 Middleware,會在 Body 後面加上Powered by KaoChienLong!
字樣。 - 接著回到帶有
Taiwan
參數的 Middleware,會在 Body 後面加上Powered by Taiwan!
字樣。 - 再來回到帶有
5xRuby
參數的 Middleware,會在 Body 後加上Powered by 5xRuby!
字樣。 - 最後,結果就呈現在畫面上,也就是上面看到的結果。
有點像在疊盤子,越早放下來的盤子會在越下面,但拿的時候會從最上面開始拿。所以,雖然在越前面的 Middleware 是越早被執行,但實際呼叫的順序是反過來的喔。
Rails 的話呢?
如果仔細觀察你的 Rails 應用程式,在根目錄底下有個 config.ru
,內容如下
require_relative 'config/environment'
run Rails.application
Rails 的複雜度就比我們上面的範例複雜得多,但拆解開來看,也就是從 run
開始的 Rack 應用程式而已。
另外,Rails 也是掛了一大串的 Middleware,你可以使用 rails middleware
指令看一下目前的 Rails 用了哪些 Middleware:
$ rails middleware
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::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run HelloRails::Application.routes
從名字大概可以猜得出來這些 Middleware 做了哪些事,舉例來說,你有看到中間那個 use ActiveRecord::Migration::CheckPending
嗎? 當你有 Migration 還沒處理的時候,就是它跳出來提醒你的。
了解 Rack 應用程式是怎麼運作的,雖然對開發 Rails 網站不見得有直接的幫助,但至少可以對 Rails 的運作以及啟動流程有更清楚的認識。
Comments