#277 Mountable Engines
- Download:
- source codeProject Files in Zip (56.6 KB)
- mp4Full Size H.264 Video (19.6 MB)
- m4vSmaller H.264 Video (12.7 MB)
- webmFull Size VP8 Video (18.7 MB)
- ogvFull Size Theora Video (23.3 MB)
先週末Rails 3.1 HackFestが開催され、参加者の努力によってRails 3.1のリリース候補第5版が公開されました。このリリースには、マウント可能なエンジン(mountable engine)に関する重要な修正が含まれています。マウント可能なエンジンによって、任意のRailsアプリケーションを別のアプリケーションにマウントできるようになることについて、今回のエピソードで紹介します。
エピソード104で紹介したException Notificationプラグインを覚えている方もいるでしょう。このプラグインをアプリケーションに追加することで、アプリケーションが発生させた例外をデータベースに蓄積することができました。ユーザインタフェースも提供され、例外の情報を表示することができました。今回のエピソードでは、このプラグインをマウント可能なエンジンとして作り直します。
はじめに
エンジンを書き始める前に、Rails 3.1 RC5以降を使用していることを確認してください。次のコマンドを実行することで、最新版をインストールできます。
$ gem install rails --pre
Railsの正しいバージョンがインストールされたら、マウント可能なエンジンの作成に取りかかりましょう。すでにあるRailsアプリケーションの中から作業する必要はありません。エンジンの作成は、新規のRailsアプリケーションの作成と同じで、rails new
コマンドを使用します。唯一の違いは、rails plugin new
を実行するという点です。今回作成するアプリケーションは例外を扱うため、uhoh
という名前にします。マウント可能なエンジンとして作成するために--mountable
オプションを指定します。
$ rails plugin new uhoh --mountable
エンジンのディレクトリ構造は、通常のRailsアプリケーションの構造にとても似ていて、別のアプリケーションの中にマウントされるように設計されているという点を除けば基本的には同じであるといえます。しかし実際にはいくつかの違いがあります。アプリケーション全体で、名前空間を設定されたディレクトリがいくつかあります。例えば、application_controller
ファイルが/app/controllers/uhoh
の下にあり、同じく assets
、helpers
、views
の各ディレクトリの下にそれぞれファイルが置かれます。これによって、組み込まれる先のアプリケーションからエンジンのコードをきれいに切り離すことができます。assets
ディレクトリを持つということは、エンジンがアプリケーションにマウントされるときにpublic
ディレクトリにいちいちアセットをコピーしなくてもいいということです。asset pipelineによってこれらはすべて自動処理されます。
アセットもディレクトリに名前空間を設定されるので、それらにリンクする場合はそのディレクトリを介する必要があります。これはlayoutsディレクトリにも当てはまりますが、RC5にはバグがあるようで、application.html.erb
ファイルが2つ存在します。ここでは、uhoh
ディレクトリの外にある方を削除しましょう。もう一つの方を見てみると、その中のアセットへの参照はすべてuhoh
ディレクトリが追加で含まれています。画像やその他のアセットにリンクする場合は、その名前空間を含む必要があります。
<!DOCTYPE html> <html> <head> <title>Uhoh</title> <%= stylesheet_link_tag "uhoh/application" %> <%= javascript_include_tag "uhoh/application" %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
次にエンジンのキーとなるファイルのひとつである、/lib/uhoh
ディレクトリ内のengine.rb
を見てみましょう。
module Uhoh class Engine < Rails::Engine isolate_namespace Uhoh end end
これはRails::Engine
から継承されたクラスで、設定をカスタマイズする場合の中心的な場所になります。このクラスには最初からisolate_namespace
の呼び出しがあり、つまりこれはエンジンが独立した単位として扱われ、マウント先のアプリケーションを気にしなくてもいいということを意味します。
エンジンについてのこの簡単な概要で紹介する最後のパーツは、/test
ディレクトリです。/test/dummy
の下にはRailsアプリケーションがあり、それを見ると、アプリケーションにマウントされたエンジンがどういう仕組みで動くかを理解することができます。アプリケーションのconfig
ディレクトリにはroutes.rb
ファイルが含まれています。ここにはmount
の呼び出しが含まれ、 パスに割り当てられているエンジンのメインクラスが渡されます。
Rails.application.routes.draw do mount Uhoh::Engine => "/uhoh" end
この、適当なパスを決めてマウントする作業は、エンジンをアプリケーションにインストールした場合には誰かが行う必要があります。これはRackアプリケーションなので、/uhohに対してリクエストが来た場合はEngine
クラスに渡されます。この行を、エンジンのREADMEファイルのインストール手順の中に含むようにすれば、アプリケーションのユーザがルートファイルの中でどのようにマウントすればいいかがわかって便利でしょう。
このダミーのアプリケーションは/test
ディレクトリ内にありますが、手作業でテストを行うときにも役に立つでしょう。エンジンのディレクトリからrails s
を実行すると、ダミーのアプリケーションが起動します。http://localhost:3000/uhoh/
にアクセスすると、その場所にマウントされているエンジンの画面に導かれます。しかしそのページにアクセスすると、対応するコントローラを記述していないため、エラーが表示されます。
そこで、エンジン内にfailures
コントローラを作成します。通常のRailsアプリケーションの場合と同じように、Railsのジェネレータを使用できます。エンジン内にいても、コントローラを名前空間で指定する必要はなく、すべて自動処理されます。
$ rails g controller failures index
このコマンドによって、通常のコントローラの場合と同じファイルが作成されます。ただ違うのは、すべてが正しいディレクトリに名前空間を指定されます。次にエンジンのルートを修正して、コントローラのindexアクションにroot
のルート(route)を設定します。この修正は、エンジンの/config/routes.rb
ファイルに対して行われます。
Uhoh::Engine.routes.draw do root :to => "failures#index" end
http://localhost:3000/uhoh/
にアクセスすると、アクションのビューが表示されます。
このページでは、発生した例外のリストを表示します。この情報を保存するためのモデルが必要なので、メッセージフィールドを持った簡単なFailure
モデルを作成します。
$ rails g model failure message:text
モデルを作成しましたが、どうやってマイグレーションを実行すればいいのでしょうか。エンジン内では通常通りrake db:migrate
を実行でき、すべては期待通りに動作します。しかし、誰かがエンジンをアプリケーション内にマウントしようとするとうまく行きません。これはrake
コマンドが、マウントされたエンジン内のmigrationを認識できないからです。エンジンのユーザに対して、代わりにrake uhoh:install:migrations
を実行するように指示する必要があります。このコマンドによって、エンジンのmigrationをアプリケーションにコピーした上でrake db:migrate
を通常のように実行できるようになります。この情報をエンジンのインストール手順書に入れておくのがいいでしょう。
Railsコンソールも、エンジン内で期待通りに機能します。これを使ってFailure
の例を作成してみます。
Uhoh::Failure.create!(:message => "hello world!")
クラスを参照する場合はつねに名前空間を含まなくてはいけない点に留意してください。Failureのレコードができたので、それをFailuresController
のindex
アクションで表示します。
module Uhoh class FailuresController < ApplicationController def index @failures = Failure.all end end end
コンソールにいる場合と違い、ここではすでにUhoh
モジュールの中にいるので、名前空間を指定する必要はありません。ビューで、すべてのfailureをループするコードを書き、リスト表示させます。
<h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul>
ページを再度読み込むと、追加したFailure
が表示されます。
例外をとらえる
これでfailureを記録するメソッドができたので、エンジンが組み込まれたアプリケーションが例外を発生させたときにfailureを生成するメソッドを作成します。これをテストするためにダミーアプリケーションで例外をシミュレートする方法が必要です。このためにエンジンのdummyアプリケーションのディレクトリに移動し、simulate
というコントローラとその中にfailure
アクションを作成します。
$ rails g controller simulate failure
アクション内で例外を発生させます。
class SimulateController < ApplicationController def failure raise "Simulating an exception" end end
ブラウザでそのアクションにアクセスすると、期待通りに例外が表示されます。
ここでエンジンを修正して、例外の発生を待機するようにして、発生したら新規のFailure
作成させるようにします。この解決策は効率的ではないかもしれませんが、シンプルで今回の場合には十分役に立ちます。ではまずエンジン内に初期化ファイル(initializer)を作成します。エンジンのconfigディレクトリにはinitializers
ディレクトリはありませんが、自分で作成してそこに初期化ファイルを置くと認識されます。このディレクトリにexception_handler.rb
というファイルを作成します。
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, start, finish, id, payload| if payload[:exception] name, message = *payload[:exception] Uhoh::Failure.create!(:message => message) end end
このファイルで、notificationを購読設定します(notificationについてはエピソード249[動画を見る, 読む]で詳しく解説しています)。対象は、アクションが処理されたときに通知してくれるnotificationです。アクションが処理されたら、payload
に例外が含まれるかどうかをチェックします。もし含まれていたら例外が発生したということで、そのメッセージを新規のFailure
に保存します。
これをテストする前にサーバを再起動します。そして再度http://localhost:3000/simulate/failure
にアクセスして例外を発生させます。例外が表示されたら、http://localhost:3000/uhoh
にアクセスしてそれを見ることができます。
エンジンでURLを扱う
エンジン内で使用するURLヘルパーは、そのエンジン向けのURLを生成します。例えばfailuresのindex
ページからroot URLへのリンクを追加すると、このリンクはエンジンのroot URLを指定していて、組み込まれた先のアプリケーションのrootではありません。
<p><%= link_to "Failures", root_url %></p>
このリンクはhttp://localhost:3000/uhoh
を指しますが、これはエンジンのroot URLです。これは、リンクがあるのと同じページです。というのもroot URLはルート設定でFailuresController
のindex
アクションを指すように定義されているからです。アプリケーション自体へのリンクを作成するには、次のようにmain_app
のURLヘルパーを呼び出します。
<p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", main_app.simulate_failure_path %></p>
これによってアプリケーションのSimulate Failureページへのリンクがhttp://localhost:3000/simulate/failure
に作成されます。
これの反対で、アプリケーションからエンジンにリンクを設定したい場合はどうすればいいでしょうか? まず最初にすることは、アプリケーションのルートファイルを修正して、エンジンをマウントしてそれに:as
オプションで名前を与えます。
Rails.application.routes.draw do get "simulate/failure" mount Uhoh::Engine => "/uhoh", :as => "uhoh_engine" end
これで、uhoh_engine
のメソッドとして呼び出すことで、エンジンのURLヘルパーにアクセスできます。これを実際に見てみるために、failureアクションを一時的に修正して、例外を発生させる代わりにエンジンのroot URLにリダイレクトします。
class SimulateController < ApplicationController def failure redirect_to uhoh_engine.root_url end end
http://localhost:3000/simulate/failure
にアクセスすると、engineヘルパーを使ってエンジンのURLにリダイレクトするように設定しているため、http://localhost:3000/uhoh
にリダイレクトされます。これも、エンジンのREADMEファイルで言及しておくべき機能でしょう。
これでエンジンの機能はほぼ出来上がりましたが、failureを表示するリストが質素なので、アセットを使って少し味付けします。まず画像を追加します。あらかじめ適当な画像を見つけて/app/assets/images/uhoh
ディレクトリに置いてあります。それをページに追加するために、他の画像と同じようにimage_tag
を使うことができます。
<%= image_tag "uhoh/alert.png" %> <h1>Failures</h1> <ul> <% for failure in @failures %> <li><%= failure.message %></li> <% end %> </ul> <p><%= link_to "Failures", root_url %></p> <p><%= link_to "Simulate Failure", ↵ main_app.simulate_failure_path %></p>
CSSも少し設定します。SASSとCoffeeScriptは、デフォルトではエンジンでは利用できませんが、依存関係として追加することができます。failures.css
ファイルにCSSを追加すると、自動的にincludeされます。
html, body { background-color: #DDD; font-family: Verdana; } body { padding: 20px 200px; } img { display: block; margin: 0 auto; } a { color: #000; } ul { list-style: none; margin: 0; padding: 0; } li { background-color: #FFF; margin-bottom: 10px; padding: 5px 10px; }
JavaScriptも同じです。failures.js
ファイルに記述されたコードは自動的にincludeされます。
$(function() { $("li").click(function() { $(this).slideUp(); }); });
ここでページを再度読み込むと、見た目はずっとよくなってクリックして例外を隠すことができるようになり、JavaScriptが有効になったことがわかります。
今回のエピソードは以上です。マウント可能なエンジンはRails 3.1の優れた新機能なので、一度見てみることをお勧めします。