homeASCIIcasts

277: マウント可能なエンジン 

(view original Railscast)

Other translations: En De Fr Es

Other formats:

Written by Naomi Fujimoto

先週末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の下にあり、同じく assetshelpersviewsの各ディレクトリの下にそれぞれファイルが置かれます。これによって、組み込まれる先のアプリケーションからエンジンのコードをきれいに切り離すことができます。assetsディレクトリを持つということは、エンジンがアプリケーションにマウントされるときにpublicディレクトリにいちいちアセットをコピーしなくてもいいということです。asset pipelineによってこれらはすべて自動処理されます。

エンジンのディレクトリ配置

アセットもディレクトリに名前空間を設定されるので、それらにリンクする場合はそのディレクトリを介する必要があります。これはlayoutsディレクトリにも当てはまりますが、RC5にはバグがあるようで、application.html.erbファイルが2つ存在します。ここでは、uhohディレクトリの外にある方を削除しましょう。もう一つの方を見てみると、その中のアセットへの参照はすべてuhohディレクトリが追加で含まれています。画像やその他のアセットにリンクする場合は、その名前空間を含む必要があります。

/app/views/layouts/uhoh/application.html.erb

<!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を見てみましょう。

/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の呼び出しが含まれ、 パスに割り当てられているエンジンのメインクラスが渡されます。

/test/dummy/config/routes.rb

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ファイルに対して行われます。

/config/routes.rb

Uhoh::Engine.routes.draw do
  root :to => "failures#index"
end

http://localhost:3000/uhoh/にアクセスすると、アクションのビューが表示されます。

indexビューのデフォルトのテキスト

このページでは、発生した例外のリストを表示します。この情報を保存するためのモデルが必要なので、メッセージフィールドを持った簡単な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のレコードができたので、それをFailuresControllerindexアクションで表示します。

/app/controllers/uhoh/failures_controller.rb

module Uhoh
  class FailuresController < ApplicationController
    def index
      @failures = Failure.all
    end
  end
end

コンソールにいる場合と違い、ここではすでにUhohモジュールの中にいるので、名前空間を指定する必要はありません。ビューで、すべてのfailureをループするコードを書き、リスト表示させます。

/app/views/uhoh/failures/index.html.erb

<h1>Failures</h1>
<ul>
  <% for failure in @failures %>
  <li><%= failure.message %></li>
  <% end %>
</ul>

ページを再度読み込むと、追加したFailureが表示されます。

failureがリスト表示される

例外をとらえる

これでfailureを記録するメソッドができたので、エンジンが組み込まれたアプリケーションが例外を発生させたときにfailureを生成するメソッドを作成します。これをテストするためにダミーアプリケーションで例外をシミュレートする方法が必要です。このためにエンジンのdummyアプリケーションのディレクトリに移動し、simulateというコントローラとその中にfailureアクションを作成します。

$ rails g controller simulate failure

アクション内で例外を発生させます。

/test/dummy/app/controllers/simulate_controller.rb

class SimulateController < ApplicationController
  def failure
    raise "Simulating an exception"
  end
end

ブラウザでそのアクションにアクセスすると、期待通りに例外が表示されます。

ブラウザに例外が表示される

ここでエンジンを修正して、例外の発生を待機するようにして、発生したら新規のFailure作成させるようにします。この解決策は効率的ではないかもしれませんが、シンプルで今回の場合には十分役に立ちます。ではまずエンジン内に初期化ファイル(initializer)を作成します。エンジンのconfigディレクトリにはinitializersディレクトリはありませんが、自分で作成してそこに初期化ファイルを置くと認識されます。このディレクトリにexception_handler.rbというファイルを作成します。

/app/config/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ではありません。

/app/views/uhoh/failures/index.html.erb

<p><%= link_to "Failures", root_url %></p>

このリンクはhttp://localhost:3000/uhohを指しますが、これはエンジンのroot URLです。これは、リンクがあるのと同じページです。というのもroot URLはルート設定でFailuresControllerindexアクションを指すように定義されているからです。アプリケーション自体へのリンクを作成するには、次のようにmain_appのURLヘルパーを呼び出します。

/app/views/uhoh/failures/index.html.erb

<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オプションで名前を与えます。

/test/dummy/config/routes.rb

Rails.application.routes.draw do

  get "simulate/failure"

  mount Uhoh::Engine => "/uhoh", :as => "uhoh_engine"
end

これで、uhoh_engineのメソッドとして呼び出すことで、エンジンのURLヘルパーにアクセスできます。これを実際に見てみるために、failureアクションを一時的に修正して、例外を発生させる代わりにエンジンのroot URLにリダイレクトします。

/test/dummy/app/controllers/simulate_controller.rb

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を使うことができます。

/app/views/uhoh/failures/index.html.erb

<%= 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されます。

/app/assets/stylesheets/uhoh/failures.css

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されます。

/app/assets/javascripts/uhoh/failures.js

$(function() {
  $("li").click(function() {
    $(this).slideUp();
  });
});

ここでページを再度読み込むと、見た目はずっとよくなってクリックして例外を隠すことができるようになり、JavaScriptが有効になったことがわかります。

CSSとJavaScriptが適用されたfailureページ

今回のエピソードは以上です。マウント可能なエンジンはRails 3.1の優れた新機能なので、一度見てみることをお勧めします。