禾川資訊 Grass Brook

Text

研究 Rails 3 的 Rails::Engine 和 Rails::Railtie

當我們在撰寫 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 原始碼。

Posted on Monday, March 15 2010. Tagged with: applicationenginepluginrailtierails 3
禾川資訊 Grass Brook We are a studio focused on Ruby, Rails and Agile Development.
Ask me anything Submit
Previous Next