FrontPage  Index  Search  Changes  RSS  Login

[Ruby] Rails Engines基礎

概要

Rails Engines は、Ruby on Rails (以降 Rails) 製アプリケーションを、アプリケーションエンジン(以降 エンジン)として扱うための仕組みを提供してくれます。

エンジンが保有する資産(ルーティングやコントローラ、モデルなど)を、エンジンを利用したアプリケーション側から自然で透過的に扱う事が可能です。

Rails Engines は、Rails プラグインとして提供されます。

"The engines plugin enhances Rails plugins ― allowing sharing of code, views and other aspects of your application in a clear and managed way.

エンジン

アプリケーション開発において、いくつかの実装で使い回せる様、コア機能を切り出して開発するという事は一般的です。このコア機能を「エンジン」と呼ぶ事にします。

例えば、SafariとWebKitの関係や、FirefoxとGeckoのような関係において、それぞれWebKitやGeckoをエンジンと呼ぶ事ができるでしょう。

例えばSDK

例えば、あるプラットホーム上での動作を想定したアプリケーションを開発する場合、開発をサポートするSDKの存在は見逃せません。

Rails Engines は、この SDK 開発にも役立ちます。類似製品を開発する様な案件を多数抱える場合、それぞれの共通点を上手く吸収する様な SDK があると、開発を楽に進める事ができるでしょう。

例えばブログ

例えば、ブログエンジンとして標準構成のブログを Rails Engines に対応させておけば、要件ごとにカスタマイズしたブログを簡単に作る事ができます。

チュートリアル

注) 以降、手元の環境が Rails 2.1.0 であるため、これを前提に話を進めます。必要に応じて読み替えてください。

エンジン作成

ここでは、Rails 製アプリケーションをエンジンにするための手順を説明します。

Rails 製アプリケーションを用意する

まずは、エンジンとして利用したい Rails 製アプリケーションが必要です。

適当なアプリケーションの心当たりが無い場合は、簡単なアプリケーションを作りましょう。

$ rails --version
Rails 2.1.0
$ rails the_engine
$ cd the_engine
$ sqlite3 -batch db/development.sqlite3 --
$ script/generate scaffold Geek wise_remark:string
$ rake db:migrate
$ script/runner "Geek.create(:wise_remark => 'Shut the fuck up and write some code.')"
$ script/server
$ curl http://localhost:3000/geeks.xml
<?xml version="1.0" encoding="UTF-8"?>
<geeks type="array">
  <geek>
    <created-at type="datetime">2008-09-13T13:49:51Z</created-at>
    <id type="integer">1</id>
    <updated-at type="datetime">2008-09-13T13:49:51Z</updated-at>
    <wise-remark>Shut the fuck up and write some code.</wise-remark>
  </geek>
</geeks>

init.rb

エンジンはプラグインとして動作するので、初期化スクリプトとしてinit.rbを RAILS_ROOT 直下に作成します。

$ touch init.rb

エンジンのconfig/environment.rbは読み込まれないため、config/environment.rbにエンジン固有の初期化処理を書いている場合は、init.rbに移動してconfig/environment.rbからinit.rbを読み込むなどの作業が必要です。

routes.rb

ルーティング規則を共有する場合、エンジンの config/routes.rb は RAILS_ROOT 直下に存在している必要があります。

$ ln -s config/routes.rb routes.rb

アプリケーションからエンジンのルーティング規則を利用したければ、以下のコードをアプリケーションの config/routes.rb に追記しましょう。

map.from_plugin :plugin_name

エンジンを利用する

ここでは、先ほど作成したエンジンを利用するための手順を説明します。

空のプロジェクトを作成する

$ rails --version
Rails 2.1.0
$ rails on_the_engine
$ cd on_the_engine
$ sqlite3 -batch db/development.sqlite3 --

Rails Engines をインストール

エンジンを利用するために、Rails Enginesをインストールする必要があります。

$ script/plugin discover -n
$ script/plugin install engines

最新版の Rails Engines を利用したい場合は、GitHub のリポジトリを利用してください。

$ script/plugin install git://github.com/lazyatom/engines.git

エンジンをインストール

$ cp -R the_engine on_the_engine/vendor/plugins/

普段は、通常のプラグインインストール手順で行うはずですが、今回は簡略化のため、コピーで済ませました。

ブートの設定

エンジンを有効にするために、config/environment.rbを編集します。

$ cp config/environment.rb config/environment.rb.orig
$ vi config/environment.rb
$ diff config/environment.rb.orig config/environment.rb
--- config/environment.rb.orig  2008-09-13 23:17:15.000000000 +0900
+++ config/environment.rb       2008-09-13 23:17:50.000000000 +0900
@@ -9,6 +9,7 @@
 
 # Bootstrap the Rails environment, frameworks, and default configuration
 require File.join(File.dirname(__FILE__), 'boot')
+require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
 
 Rails::Initializer.run do |config|
   # Settings in config/environments/* take precedence over those specified here.

ルーティング規則の取り込み

先述の通り、エンジン側で定義されているルーティング規則を取り込むためには、アプリケーション側のconfig/routes.rbに設定を記述する必要があります。

$ cp config/routes.rb config/routes.rb.orig
$ vi config/routes.rb
$ diff config/routes.rb.orig config/routes.rb
--- config/routes.rb.orig       2008-09-14 00:48:26.000000000 +0900
+++ config/routes.rb    2008-09-14 00:49:45.000000000 +0900
@@ -1,4 +1,5 @@
 ActionController::Routing::Routes.draw do |map|
+  map.from_plugin :the_engine
   # The priority is based upon order of creation: first created -> highest priority.
 
   # Sample of regular route:

DBのセットアップ

エンジン側のマイグレーションコードを利用して、DBのセットアップを行います。

$ rake db:migrate:plugins

$ rake db:migrate
$ rake db:migrate:plugin\[the_engine\]

あわせて、データを入れておきます。

$ script/runner "Geek.create(:wise_remark => 'Shut the fuck up and write some code.')"

アセットのコピー

起動時に、自動的にエンジンの公開ディレクトリ以下のファイル群が、アプリケーションの'public/plugin_assets/<plugin_name>'にコピーされます。

アプリケーションのビューテンプレート内で、スタイルシート等のパスに不都合が出ないよう、エンジン側で配慮する必要があります。必要に応じて、engine_stylesheet や engine_javascript を利用する様にします。

動作確認

$ curl http://localhost:3000/geeks.xml <?xml version="1.0" encoding="UTF-8"?> <geeks type="array">

 <geek>
   <created-at type="datetime">2008-09-13T16:00:20Z</created-at>
   <id type="integer">1</id>
   <updated-at type="datetime">2008-09-13T16:00:20Z</updated-at>
   <wise-remark>Shut the fuck up and write some code.</wise-remark>
 </geek>

</geeks>

起動プロセスについて

  1. Rails のバージョンチェック
  2. Rails Engines のロード (lib/engines.rb)
  3. Rails::Configuration のメソッドを再定義
    1. default_plugin_locators (return [Engines::Plugin::FileSystemLocator])
    2. default_plugin_loader (return Engines::Plugin::Loader)
    3. default_plugins (return [:engines, :all])

Rails Engines のロード (lib/engines.rb)

必要なファイルをロードし、Engines モジュールを定義しています。

lib/engines/plugin.rb
  • Rails::Plugin を継承した Engines::Plugin クラスを定義
lib/engines/plugin/list.rb
  • Engines::Plugin::List クラスを定義
lib/engines/plugin/loader.rb
  • Rails::Plugin::Loader を継承した Engines::Plugin::Loader クラスを定義
lib/engines/plugin/locator.rb
  • Rails::Plugin::FileSystemLocator を継承した FileSystemLocator クラスを定義
lib/engines/assets.rb
  • Engines::Assets モジュールを定義

Files in this directory are automatically generated from your plugins. They are copied from the 'assets' directories of each plugin into this directory each time Rails starts (script/server, script/console... and so on). Any edits you make will NOT persist across the next server restart; instead you should edit the files within the <plugin_name>/assets/ directory itself.

lib/engines/rails_extensions/rails.rb
  • Rails.plugins を Engines.plugins に置き換える
    • Rails.plugins を再定義し、その内部で Engines.plugins を実行

Rails の起動プロセスを拡張

Rails は、ご存知の通りconfig/environment.rbで以下のコードによって初期化処理を行っています。

Rails::Initializer.run do |config|
...
end

この実装は、以下(rails-2.1.0/lib/initializer.rb)になります。

def self.run(command = :process, configuration = Configuration.new)
  yield configuration if block_given?
  initializer = new configuration
  initializer.send(command)
  initializer
end

また、Rails::Configuration.initialize の実装は以下の通りです。

def initialize
  set_root_path!
  self.frameworks                   = default_frameworks
  self.load_paths                   = default_load_paths
  self.load_once_paths              = default_load_once_paths
  self.log_path                     = default_log_path
  self.log_level                    = default_log_level
  self.view_path                    = default_view_path
  self.controller_paths             = default_controller_paths
  self.cache_classes                = default_cache_classes
  self.whiny_nils                   = default_whiny_nils
  self.plugins                      = default_plugins
  self.plugin_paths                 = default_plugin_paths
  self.plugin_locators              = default_plugin_locators
  self.plugin_loader                = default_plugin_loader
  self.database_configuration_file  = default_database_configuration_file
  self.routes_configuration_file    = default_routes_configuration_file
  self.gems                         = default_gems
  for framework in default_frameworks
    self.send("#{framework}=", Rails::OrderedOptions.new)
  end
  self.active_support = Rails::OrderedOptions.new
end

Rails Engines のブートで登場した、Rails::Configuration のメソッド再定義がここで関係してきます。つまり、以下の実体を変更する事で、Rails の起動プロセスを拡張している事が分かります。

  • Rails::Configuration#default_plugin_locators
  • Rails::Configuration#default_plugin_loader
  • Rails::Configuration#default_plugins

そして、initializer.send(command) によって Rails::Initializer#process が実行されます。この中で、上記のプラグイン関連クラスが利用されています。

  • add_plugin_load_paths
  • load_plugins

add_plugin_load_paths の実装は以下の通りで、plugin_loader に処理を委譲しています。

def add_plugin_load_paths
  plugin_loader.add_plugin_load_paths
end

load_plugins の実装は以下の通りで、plugin_loader に処理を委譲しています。

def load_plugins
  plugin_loader.load_plugins
end
Rails
:Initializer#plugin_loader:
def plugin_loader
  @plugin_loader ||= configuration.plugin_loader.new(self)
end

plugin_loader は上記通り。

Rails Engines によるプラグインのロード処理

先述の通り、plugin_loader にプラグイン関連の処理を委譲している事が分かっています。

plugin_loader は、Engines::Plugin::Loader です。このクラスの該当メソッドを見ていけば、アプリケーションのブート時に Rails Engines が行っている事が分かるはずです。

lib/engines/plugin/loader.rb
module Engines
  class Plugin
    class Loader < Rails::Plugin::Loader
      protected
        def register_plugin_as_loaded(plugin)
          super plugin
          Engines.plugins << plugin
          register_to_routing(plugin)
        end

        # Registers the plugin's controller_paths for the routing system. 
        def register_to_routing(plugin)
          initializer.configuration.controller_paths += plugin.select_existing_paths(:controller_paths)
          initializer.configuration.controller_paths.uniq!
        end
    end
  end
end

Rails::Plugin::Loader の register_plugin_as_loaded と register_to_routing をオーバーライドして挙動を変更しようとしている事が予測できます。

Rails
:Plugin::Loader#add_plugin_load_paths (rails-2.1.0/lib/rails/plugin/loader.rb):
def add_plugin_load_paths
  plugins.each do |plugin|
    plugin.load_paths.each do |path|
      $LOAD_PATH.insert(application_lib_index + 1, path)
      Dependencies.load_paths      << path
      unless Rails.configuration.reload_plugins?
        Dependencies.load_once_paths << path
      end
    end
  end
  $LOAD_PATH.uniq!
end

add_plugin_load_paths メソッドに関しては、標準のままだと判断して次に進みます。

Rails
:Plugin::Loader#load_plugins (rails-2.1.0/lib/rails/plugin/loader.rb):
def load_plugins
  plugins.each do |plugin| 
    plugin.load(initializer)
    register_plugin_as_loaded(plugin)
  end
  ensure_all_registered_plugins_are_loaded!
end

利用予定のプラグインについて、ロードとロード済みである事の登録処理を行っています。このループ内が、Rails Engines による拡張対象だという事が分かりました。

plugin.load では、以下の様な処理が行われています。

Rails
:Plugin#load(initializer):
def load(initializer)
  return if loaded?
  report_nonexistant_or_empty_plugin! unless valid?
  evaluate_init_rb(initializer)
  @loaded = true
end

evaluate_init_rbメソッドは、プラグインのルート直下にinit.rbが存在していれば、そのファイルを読み込んで評価する、といった事を行います。つまり、プラグインをロードした際に行われる初期プロセスと考える事ができます。

また、Rails::Configuration#default_plugins は Rails Engines によって再定義されており、先述の通り[:engines, :all]が返される様になっています。

Engines
:Plugin#load:
def load(initializer)
  return if loaded?
  super initializer
  add_plugin_view_paths
  Assets.mirror_files_for(self)
end

標準の処理に加えて、ビューのパスとアセットの(アプリケーション側への)ミラーリングが行われています。

Rails
:Plugin::Loader#register_plugin_as_loaded (rails-2.1.0/lib/rails/plugin/loader.rb):
def register_plugin_as_loaded(plugin)
  initializer.loaded_plugins << plugin
end

register_plugin_as_loaded では、標準のメソッドを実行後に、Engines.plugins にプラグインを追加しています。さらに、register_to_routing によってルーティングのために、コントローラのパスを追加してユニークをとっています。

init.rb

Rails Engines がロードされた際に、init.rbが評価されます。

init.rb
config.after_initialize do
  Engines.init if defined? :Engines
end

Rails の起動プロセスの後処理に、Engines.init を登録しています。

Engines.init
def init
  load_extensions
  Engines::Assets.initialize_base_public_directory
end
Engines.load_extensions
def load_extensions
  rails_extensions.each { |name| require "engines/rails_extensions/#{name}" }
  # load the testing extensions, if we are in the test environment.
  require "engines/testing" if RAILS_ENV == "test"
end

Engines.load_extensions によって、Rails Engines による Rails の拡張が行われます。

self.rails_extensions = %w(action_mailer asset_helpers routing migrations dependencies)
Engines
:Assets.initialize_base_public_directory:
def initialize_base_public_directory
  dir = Engines.public_directory
  unless File.exist?(dir)
    Engines.logger.debug "Creating public engine files directory '#{dir}'"
    FileUtils.mkdir_p(dir)
  end
  readme = File.join(dir, "README")
  File.open(readme, 'w') { |f| f.puts @@readme } unless File.exist?(readme)
end

Engines::Assets.initialize_base_public_directory では、Rails Engines 用の公開ディレクトリが作成されます。

self.public_directory = File.join(RAILS_ROOT, 'public', 'plugin_assets')

Rails の拡張

Rails Engines は、engines/rails_extensions/以下に置かれている、以下に一致するファイルをロードする事で、Rails を拡張します。。

%w(action_mailer asset_helpers routing migrations dependencies)

エンジン側の Rails 機能(資産)を使いたい場合は、これらに関するドキュメントを読むとよいでしょう。

以降、それぞれの概要を簡単に説明しておきます。

ActionMailer

エンジン側のテンプレートを利用できる様に、テンプレートパス関連の拡張を行っています。

Assets

エンジン側のアセット(JS, CSS, 画像)をアプリケーション側で利用できる様に、ヘルパの拡張を行っています。

  • stylesheet_link_tag
  • javascript_include_tag
  • image_path
  • image_tag

:plugin_name を指定する事で、利用するエンジンを指定する事ができます。

Routing

エンジン側のルーティング規則を利用できる様に、ActionController::Routing::RouteSet::Mapper を拡張しています。

from_plugin メソッドを利用すれば、エンジン側のルーティング規則をそのまま利用できます。

Migrations

エンジン側のマイグレーションコードを利用できる様に、Rails のマイグレーション機構を拡張しています。

エンジン側マイグレーションでは、スキーマ管理のテーブル名が標準で、plugin_schema_infoとなります。

Dependencies

エンジンのコントローラやヘルパを継承しつつ、アプリケーション側でのオーバーライドを有効にできる様に、Dependencies を拡張します。

モデルに関しては、このような仕組みを提供していません。

追加Rakeタスク

tasks/engines.rakeによって、以下のタスクが追加されます。

rake db:fixtures:plugins:load
rake db:migrate:all
rake db:migrate:plugin[name,version]
rake db:migrate:plugins
rake doc:plugins:enginesenvironment
rake doc:plugins:the_engineenvironment
rake test:plugins:all
rake test:plugins:functionals
rake test:plugins:integration
rake test:plugins:setup_plugin_fixtures
rake test:plugins:units

まとめ

Rails Engines を利用する事で、以下の様な事が行われます。

  • ロードパスの追加(調整)
  • ルーティング規則の取り込み
  • マイグレーションの利用
  • アセットの利用
  • Rakeタスク追加
  • ジェネレータ追加
  • コントローラとヘルパのコードミキシング補助
Last modified:2008/09/17 13:05:48
Keyword(s):[ruby]
References: