當我們在撰寫 Rails 3 的 Plugin/Engine 時,會被要求養成好的習慣,如你要覆寫 ActiveRecord 等底層模組/類別時,請先 require 它和你的覆寫模組(注意順序),再把覆寫模組給 include 進去:
## in lib/my_gem.rb
require "active_record"
require "my_gem/active_record_sti_fix"
ActiveRecord::Base.class_eval { include MyGem::ActiveRecordStiFix }
對於常寫 Rails plugin 的人,會想直接更動 Rails 3 原本的 Life Cycle 來滿足需求,這時只要引入一個 Rails::Railtie 子類別便可變更啟動程序,這是因為 ActionMailer, ActionController, ActionDispatch, ActionView, ActiveModel, ActiveRecord, ActiveResource 和 ActiveSupport 等都擁有一個繼承 Rails::Railtie 的類別,Rails 3 啟動程序主要工作就是正確誘發這些 railtie(請注意 require 的順序):
## in lib/my_gem.rb
module MyGem
# Make sure your Gem loads the railtie.rb file if Rails is loaded first.
require 'my_gem/railtie' if defined?(Rails::Railtie)
end
## in lib/my_gem/railtie.rb
require 'my_gem'
require 'rails'
module MyGem
class Railtie < ::Rails::Railtie
# This creates a config.my_gem in the user's Application
config.my_gem = MyGem
# configuration shared by all railties and the application
config.generators.test_framework :rspec
config.middlewares.use MyGem::MyMiddleware
config.to_prepare do
MyGem.config_setup!
end
# rails 3 notification system
require "my_gem/railties/log_subscriber"
log_subscriber MyGem::Railties::LogSubscriber.new
rake_tasks do
load "my_gem/setup.rake"
end
generators do
require "../../generators/another_generator.rb"
end
initializer :setup_my_gem, :before => :load_environment_config do |app|
app.config.locale = 'zh-TW'
app.config.time_zone = 'taipei'
end
end
end
關於以上程式內容,設定 config.my_gem 作為這個 rubygem 特有的設定值辨識名,config.* 可以設定所有 railtie 都讀得到的設定值,可定義 Rails::LogSubscriber 來處理 log 記錄,可用 rake_tasks 方法載入 rake tasks,用 generators 方法指定 generators 的位置,以及用 initializer 方法定義一些常量、變數等等(可以不需要在 config/initializers 下建設定檔),最後這些方法都會在執行 YourApp::Application < Rails::Application 時被誘發。
而平常寫的 Rails 3 Engine(小型嵌入式 rails 應用程式,會自動載入 app, config, lib 等目錄)分為兩種。一種是 Engines in gems,需要額外載入 Rails::Engine 類別(在 Rails.root/config/environment.rb 一開始就 require 進來或在 Gemfile 定義 gem ‘my_engine’),才會把這個 rubygem 啟動成為 rails engine,這是因為 Rails::Engine 本身也是 Rails::Railtie 的子類別(多了預設的 initializers 和特定的 methods),因此擁有類似的載入和設定方式:
## lib/my_engine.rb
module MyEngine
# Make sure your Gem loads the railtie.rb file if Rails is loaded first.
require 'my_engine/engine' if defined?(Rails::Railtie)
end
## lib/my_engine/engine.rb
require 'my_engine'
require 'rails'
module MyEngine
class Engine < ::Rails::Engine
config.my_engine = MyEngine
# Add a load path for this specific Engine
config.load_paths << File.expand_path("../../a_path", __FILE__)
config.eager_load_paths << File.expand_path("../../b_path", __FILE__)
config.load_once_paths << File.expand_path("../../c_path", __FILE__)
# Add/specify the load path for this specific Engine
paths.app.controllers << "lib/controllers"
paths.app.metals = "lib/metal"
end
end
另一種是 Engines in plugins,不需額外載入 Rails::Plugin 類別(這是 Rails::Engine 的子類別),必須依循預設規則,只要把 engine 程式放在 vender/plugins 下,Rails 3 就自動幫你處裡好,如同以往也會自動載入 PLUGIN_ROOT/init.rb,唯一要注意的是 Rails::Plugin 是最終類別,且同一個 engine 中不能同時載入另一個 Rails::Engine。
最後來看 Rails::Application,它本身就是個巨大的 Rails::Engine,以 Singleton 方式存在,主要工作是協調各 Railtie 的啟動程序,如 initialization, configuration, routes, middleware 和 metal(定義在 Rails::Application 目錄下 ),讓彼此和睦相處。
註:以上主要編修自 josevalim 的 gist 文件,並以 rails-3.0.0.beta2 這個版本為主,詳細的 method 用法請參考 Rails 原始碼。