新的 Rails3 Controller 重構後,變成 ActionController::Base < ActionController::Metal < AbstractController 的繼承體系。除了 ActionMailer 繼承自 AbstractController,讓 API 與 Controller 一致之外,新的 ActionController::Metal 更是充滿玩味。

故事可以從上個禮拜,Yehuda 把 Rails 2 的 Metal 移除了(commit),根據 commit 的說明,Rails 2 的 Metal 在 Rails 3 裡面,可以用 1. 放在 Route 之前的 Rack middleware 或 2. 放在 Route 之後的一個 Rack endpoint,而這個 Rack endpoint,除了自己實作 Rack app,我們也可以用 ActionController::Metal 來實作出自己的客製化 Controller。

Rails2 發明 Metal 原因是,有些事情不需要完整的 Controller 功能,希望能夠越快越好,例如 XML/JSON API 等。而 Rails2 的 Metal 雖然非常快,但是沒什麼彈性,完全不能使用 Controller 的功能,例如用 Layout, Template, Sessions 等,基本上就跟單純的 Rack middleware 沒什麼兩樣。但是在 Rails3 中,可以透過自繼承 ActionController::Metal 做出白紙般的客製 Controller,可以有非常快的速度,如果有需要用到 Controller 的功能,也可以任意選擇組合加入,十分彈性。

例如,我們來實作一個超級精簡的 Static Controller:


# lib/static_controller.rb
class StaticController < ActionController::Metal
  include ActionController::Rendering

  append_view_path "#{Rails.root}/app/views"

  def about
    render "about"
  end
end

# config/route.rb
match '/about', :to => "static#about", :as => :about

這個範例有接近於 Metal 的速度,並加入了 Controller 的 Template 功能 (你可以參考 ActionController::Base 這是擁有全部功能的版本)。其中 “static#about” 是 StaticController 的 about action 縮寫,而在 Rails3 中,controller action 也都是一個 Rack app。

除了我的這個簡單範例,在 The Powerful New Rails RouterUpgrading a Rails 2 App to Rails 3 的影片中,也有分別舉例。

總而言之,如果你在 Rails3 中不需要全部的 Controller 的功能,想要盡量拉高效能,有幾種推薦作法:

* 寫成 Rack Middleware,然後在 config/application.rb 中插入 config.middleware.use YourMiddleWare
* 寫成 Rack App,在 config.route.rb 中將某個對應的網址指到這個 Rack App
* 繼承自 ActionController::Metal 實作一個 Controller,其中的 action 也是一個 Rack App

其中的差異就在於後兩者會在 Rails Route 之後(好處是統一由 route.rb 管理 URL 路徑),如果繼承自 ActionController::Metal 可以有彈性獲得更多 Controller 功能。原則上,我想我會推薦 ActionController::Metal,寫起來最為簡單,一致性跟維護性較高。

另外,還有個小玩意, ActionController::Middleware 是 Controller 層級的 Rack Middleware,讓你可以在放入到某個特定 Controller 之中(也就是只有該 Controller 使用這個 Middleware)。不過呢,這個功能我到現在還沒看到任何實用的例子就是了。

最後,Yehuda 提供了一個 參考數據


fast: through middleware inserted at 0
slwr: through middleware inserted via @use@
rotr: through endpoint sent via the router
bare: through ActionController::Metal with self.response_body
cntr: through ActionController::Base with self.response_body
text: through ActionController::Base with render :text
tmpl: through ActionController::Base with simple render :template
layt: through ActionController::Base with render :template with layout

         real     rps
fast   0.004271   2900 Rack 極限
slwr   0.067029   2200 使用 config.middleware.use YourMiddleware
rotr   0.088085   2000 經過 Rails Route 之後
bare   0.103868   1900 使用 ActionController::Metal 的最快速度
cntr   0.355898   1070 使用 ActionController::Base 的最快速度
text   0.557127    825 使用 ActionController::Base 加上 render :text
tmpl   0.639581    765 使用 ActionController::Base 加上 render :template
layt   1.678789    375 使用 ActionController::Base 加上 Template 跟 Layout

Update(2010/7/27): Bundle 1.0.0 之後不需要 bundle lock 了,只要 bundle install 就會自動 lock。預設都裝到 system gem 的位置。

Bundler 是一套為了 Rails3 所打造的全新 Gem dependencies 管理工具:一套基於 Rubygems 的更高階套件管理工具,適合讓 Application 管理多套 Gems 依存關係的複雜情境。而你在 Rails3 中 (Bundler 不只用在 Rails3,其他例如 Sinatra 或是 Rails2 也都可以使用) 要使用的 Gems,也都必須宣告在它的 Gemfile 裡,沒寫在裡面的話,就算手動 require 也找不到。這跟已往你可以直接 require 任意 rubygems 不同,在使用 Bundler 的環境中,要 require 什麼 rubygems 必須透過 Gemfile 管理。

Gemfile 的寫法大致如下:


  # 第二個參數可以指定版本
  gem "rails", "3.0.0.beta3" 

  # 如果 require 的檔名不同,可以加上 :require
  gem "sqlite3-ruby", :require => "sqlite3"

  # 可以用 Git 當做來源,甚至可以指定 branch, tag 或 ref。
  gem 'authlogic', :git => 'git://github.com/odorcicd/authlogic.git',
                            :branch => 'rails3'

  # 可以直接用電腦裡的其他目錄
  gem "rails", :path => '/Users/ihower/github/rails'

  # Group 功能可以讓特定環境才會載入
  group :test do
    gem "rspec-rails", ">= 2.0.0.beta.8"
    gem "webrat"
  end


設定好 Gemfile 之後,我們有一些指令可以用:

  • bundle check 可以檢查目前缺少哪些 rubygem,然後你可以手動透過 sudo gem install 安裝到系統裡。
  • bundle install 安裝所有需要的套件。如果系統已經有裝了,就用系統的,不然會裝到 $BUNDLE_PATH 下,預設是你家目錄 ~/.bundle (因此請不要用 sudo 執行 bundle install)。如果來源是 git (例如上述的 authlogic),每次執行 bundle install 就會自動 git pull 更新,十分方便。
  • bundle lock 和 bundle unlock 會做 snapshotting 記錄下目前所有套件的版本在 Gemfile.lock,建議這個檔案也一起 commit 出去。適合要佈署或多人開發時,可以確保大家的版本都一致。
  • bundle package 如果你的 Server 沒聯外網路,或是怕 rubygems.org 連不上,可以用這個指令把所有套件都打包到 vendor/cache 下。基本上,跟以往 Rails 1.X 2.X 時代佈署時會建議你盡量打包依存套件並 commit 出去,在使用 Bundler 後已經大大地不需要了,因為透過 bundle lock 我們就可以確保每台機器上執行的套件版本一致。
  • bundle exec 因為 Bundle 可以說是獨立出一個套件環境,所以如果有非 Rails 的指令需要執行,而且你的系統 Gems 又沒有安裝,那就會需要透過 bundle exec XXX 來執行。例如 bundle exec cucumber。
  • bundle show gem_name 可以查看這個 gem 的目錄位置
  • bundle open gem_name 可以用編輯器打開這個 gem 的目錄

開發 Rails3 實際用一陣子之後,發現很偏好將套件裝成 Gem 了(如果有提供 Gem 版的話),之前 Rails 1.X 2.X 時代會比較喜歡裝成 Plugin,因為想說別人要裝 Gem 可能會有問題,以及佈署也怕出包。但是有了 Bundler 之後,只要 Bundle install 就可以裝好並確保大家的版本一致會動。不像已往的 rake gems:install 超不可靠。可以透過 Bundle 裝這些依存套件也減少了需要 commit 出去的 vendor/plugin 檔案,讓你的專案 repository 變乾淨了。另外,我也超喜歡的 Bundler 可以支援 Git 來源,只要 bundle install 就會更新,不需要額外的管理工具去煩惱更新 plugins。

其他推薦閱讀: