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 方法的物件,並且回傳一個包含以下三個元素的陣列:

  1. HTTP 狀態(數字型態,例如正常回應是 200、找不到頁面是 404、伺服器錯誤是 500)。
  2. HTTP Header(Hash 型態)。
  3. 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"]]
}

說明:

  1. Rack 期望要有一個可以 call 的物件,所以這裡做了一個 Proc 物件給它執行。
  2. 執行後要回傳一個陣列,所以我直接在 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/,應該可以看到這個畫面:

image

就這樣,你就完成你的第一個 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"]]
}

說明:

  1. GameChanger 這個類別裡一樣要有 call 方法,它最後也是要回傳跟一般 Rack 程式一樣的三個元素。
  2. 要掛上這個 Middleware,是使用 use 這個方法。
  3. use 的時候,它會自動把整個 Rack 程式當做第一個參數傳入,所以你可以看到在 GameChanger 類別裡的 initialize 方法額外接收了一個參數。
  4. 如果希望可以再傳入其它的參數,可在類別的 initialize 方法中再加上額外的參數,在使用的時候就會變 use GameChanger, "5xRuby"
  5. Middleware 之所以可以一個接一個的執行下去,主要靠的就是在 call 方法裡再繼續執行傳進來的那個物件的 call 方法。

上面這段範例執行後,應該可以在畫面上看到在最後一行加上了「Powered by 5xRuby!」的字樣:

image

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,應該就會看到要你輸入帳號密碼的畫面:

image

帳號密碼必須正確才能通關喔。

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

不過,你如果看瀏覽器會發現畫面是這樣:

image

咦? 怎麼順序跟剛剛印出來的結果不同? 整個剛好是反過來的呢?

掛了上面這三個 Middleware,在執行 Rack 程式的時候,流程上大概是這樣:

  1. 當使用 use 方法掛載第一個帶有 5xRuby 參數的 Middleware,它會呼叫下一個 Middleware,也就是帶有 Taiwan 參數的 Middleware。
  2. 當執行帶有 Taiwan 參數的 Middleware,它會呼叫帶有 KaoChienLong 參數的 Middleware,這也是之所以你會看到這印出來的結果是照這個順序的原因。
  3. 然後 Rack 應用程式開始執行(run),在這個當下,Body 的內容是 Hello, Rack
  4. 回到帶有 KaoChienLong 參數的 Middleware,會在 Body 後面加上 Powered by KaoChienLong! 字樣。
  5. 接著回到帶有 Taiwan 參數的 Middleware,會在 Body 後面加上 Powered by Taiwan! 字樣。
  6. 再來回到帶有 5xRuby 參數的 Middleware,會在 Body 後加上 Powered by 5xRuby! 字樣。
  7. 最後,結果就呈現在畫面上,也就是上面看到的結果。

有點像在疊盤子,越早放下來的盤子會在越下面,但拿的時候會從最上面開始拿。所以,雖然在越前面的 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