如果你熟悉 Rails 的話,你知道最主要的三個組件是 models,controllers 和 views。
沒什么好吃驚的,有對應的 Ruby 模塊,它們是: ActiveRecord , ActionController 和 ActionView。
Rails 提供了鉤子來訪問這些類庫在初始化的時候。通過添加或修改這些類庫的代碼,我們可以對 Rails 棧做任何事。在本章中,我們會看看最常見的方式去添加 gem 的代碼到一個 Rails 應用和 ActiveSupport 是如何幫助的。
Railties
Railties 是對于配置和加載 Rails 框架的類庫的鉤子。它們允許我們很方便的添加或修改在 Rails 中已經存在的功能。
使用一個 Railtie 讓我們有能力去添加 view 幫助方法,controller 方法,模型方法,生成器和一些列其他有用的特性。Railtie 的文檔提供了這些任務的示例代碼。文檔很少考慮到 Rails 在表面之下做的事情。然后,事實上我們可以鉤子初始化進程,這意味著如果我們知道方法處于哪個類中,我們可以改變或者擴展這個目標類。
看看第一個例子:
class MyRailtie < Rails::Railtie
initializer "my_railtie.configure_rails_initialization" do
# some initialization behavior
end
end
在 Railtie 中,Initializers 并不是在 config/initializers 目錄下的那類東西。
它們是特殊的代碼塊來鉤子 Rails 初始化進程的。
為了實現這個功能,一個 initializer 需要一個專門的名字(就像上面那個)并且相關的代碼被放在接踵而至的塊中。
在我們深入創建一個 Railtie 之前,讓我們探索一些擴展已有 Ruyb 類的方法。
Ruby Extensions
假設我們有如下的類:
module MegaLotto
class HolidayDrawing
def draw
puts "This drawing is holiday-worthy!"
end
end
end
再想象一下我們想要添加 #jackpot 方法,但是類已經被定義了。Ruby 給了我們打猴子補丁的能力,正如我們在核心擴展中看到的那樣。
最簡單的實現方法就是在一個新的單獨的文件中重新打開 MegaLotto::HolidayDrawing 類并且添加新方法:
module MegaLotto
class HolidayDrawing
def jackpot
puts "You've won the big one!"
end
end
end
現在我們既可以訪問原先已經定義的 #draw 方法,也可以訪問新的 #jackpo 方法。
MegaLotto::HolidayDrawing.new.draw # => This drawing is holiday-worthy!
MegaLotto::HolidayDrawing.new.jackpot # => You've won the big one!
另一個給 Ruby 類添加方法的選項是使用 extend 或者 include 方法。雖然這篇博文有點老了,但是這模式在今天依然和適合 Ruby。
通常當使用 extend 或者 include 時,對于要包含目標方法(在我們的例子用是 #jackpot)放在一個單獨的 Ruby 模塊中:
module MegaLotto
module Jackpot
def jackpot
puts "You've won the big one!"
end
end
end
為了添加 #jackpo 方法到 MegaLotto::HolidayDrawing,我們再一次重構耐心打開 MegaLotto::HolidayDrawing 類并且包含那個模塊:
module MegaLotto
class HolidayDrawing
include MegaLotto::Jackpot
end
end
這產生了和之前一樣的結果,但是具體實現被抽象到了一個模塊:
MegaLotto::HolidayDrawing.new.jackpot # => You've won the big one!
再進一步,包含一個類方法到一個標準的 Ruby 類中,我們可以 5 行代碼縮減為:
MegaLotto::HolidayDrawing.include(MegaLotto::Jackpot)
不過,當我們嘗試時,我們得到了下面的錯誤:
NoMethodError: private method `include` called for
MegaLotto::HolidayDrawing:Class from (irb):36
from /Users/bhilkert/.rbenv/versions/2.0.0-p247/bin/irb:12:in `<main>`
這說明包含了一個私有方法,所以用 Ruby 的 send 方法避開 Ruby 的可見性限制,這樣就能訪問到私有方法了。send 方法接收一個方法名(符號或字符串)以及可選的參數。
看上去就像這樣:
MegaLotto::HolidayDrawing.send(:include, MegaLotto::Jackpot)
調用 #jackpot 的結果和預期一樣:
MegaLotto::HolidayDrawing.new.jackpot # => You've won the big one!
注意:extend 的用法類似,除了我們不需要使用 send 因為方法是 public 的。使用 extend 對源對象添加了類方法。所以它不適合我們這個例子。這些加載的方式是需要一個 Rails 初始化鉤子的。如果我們使用上面的例子,一個例子 Railtie 可以像這樣:
class MyRailtie < Rails::Railtie
initializer "my_railtie.configure_rails_initialization" do
MegaLotto::HolidayDrawing.send(:include, MegaLotto::Jackpot)
end
end
現在,MegaLotto::HolidayDrawing 命名空間不被 Rails 啟動時包含,所以這個例子沒有幫助。然而,如果我們把它應用到 ActionView::Base 中,你可以看到它是如何立刻變的有用的:
class MyRailtie < Rails::Railtie
initializer "my_railtie.configure_rails_initialization" do
ActionView::Base.send(:include, MegaLotto::Jackpot)
end
end
這是一個常見模式對于 gems 包含 view helpers。然后,我們可以通過改變 ActiveSupport 的 on_load 方法來做的更好。
Active Support Load Hooks
一個擴展原生 Rails 的方法是通過 Active Support gem。
正如前面所提到的,Active Support 包含了一些列幫助和擴展方法。
Active Support 有一個 .on_load 方法來保持加載鉤子的跟蹤。
當一個特定的 Rails 類被加載時,它的相關的加載鉤子會被執行。
.on_load 的源碼只是一個標準的 Ruby Hash,包含一個 key 對于目標 Rails 類。
讓我們看看和 Railtie 等價的 Active Support .on_load 方法。
對于我們的 initializer 它看起來像這樣:
class MyRailtie < Rails::Railtie
initializer "my_railtie.configure_rails_initialization" do
# some initialization behavior
end
end
使用 initializer,不需要使用 include 或者 extend 來添加方法到一個已經存在的類(就像我們上面看到的),我們會觸發包含的方法當一個特定的 Rails 類庫加載時(這里的例子是 action_view):
class MyRailtie < Rails::Railtie
initializer "my_railtie.configure_rails_initialization" do
ActiveSupport.on_load(:action_view) do
# some initialization behavior, but in the
# context of the base `ActionView` class
end
end
end
這種方法的好處是它看傻姑娘去更像原生的 Ruby 并且避開了不得不使用 send 來讓類包含額外的模塊。
我們將會對將要到來的 Rails 集成使用這種方式。
真實世界的例子
WillPaginate gem 提供了一個很好的例子關于 ActiveSupport.on_load 鉤子的價值。
這是我見過的很復雜的 Railtie 之一。它讓分頁影響了 views,controllers 和 models 當查詢和展示數據給用戶看時。注意 .on_load 鉤子對于 ActionView,ActionController 和 ActiveModel 。
它說明了 ActionController 它自己使用 .on_load 方法來加載它的內部。即使 Rails 內部使用 Railtie 鉤子提供給了 gem 的作者!
總結
在 Rails 初始化時訪問 Rails 類庫給了我們很大的靈活性,讓 gem 的創造者可以集成 Rails 棧的任何組件。
很明顯 Rails 核心團隊讓 gem 集成和訪問 Rails 內部有一個優先權。
這允許我們,作為 gem 開發者,通過分離開發類庫來構建有價值的功能。
希望這個章節讓我們知道了集成和 Rails 初始化進程加鉤子是多么簡單。
接著 Rails 集成的話題,下面我們會看看我們如何給我們的 gem 加上 Rails 的 view 幫助方法。