Tech Racho エンジニアの「?」を「!」に。
  • 開発

[Rails 5] コントローラの制約を受けずに任意のビューテンプレートをレンダリングする

こんにちは、hachi8833です。BigBinaryシリーズ、今回はRails 5で導入されたActionController::Rendererを使って、コントローラの制約を受けずに任意のビューテンプレートをレンダリングする方法をご紹介します。

元記事

確認に使った環境

この環境でbundle exec rails generate scaffold Order ordername:string mail:stringでOrdersコントローラを作成し、Ordersに適当にデータを入力しました。

元記事では「outside of controller」といった表現が多用されており、訳文では「コントローラの外」などと表記していますが、これらは厳密には「ルーティングからのApplicationController呼び出しなどの呼び出しロジックの制約を受けずに、任意のテンプレートを(独立したコントローラクラスから)直接レンダリングできる」ことが含意されています。

Rails 5ではコントローラの外でもビューをレンダリングできる

Railsでは、リクエスト-レスポンスのサイクルに乗っていれば、コンテンツの種類に応じてビューでHTMLやJSONを簡単にレンダリングできます。
たとえば、レポート画面でPDFを生成してユーザーがダウンロードできるようにすることもできます。

ところで、そのときとまったく同じPDFをリクエスト-レスポンスの流れの外でも再度生成したいということはよくあります。しかしバックグラウンドジョブによるメール送信は既に完了してしまい、生成時点のリクエスト-レスポンスサイクルも終了してしまいました。Rails 4までは、こういう場合のために作り込みが必要になるのが普通でした。

Rails 5ではActionController::Rendererでこの問題に対応できます。

1. コントローラでビューをレンダリングする

まずは通常のコントローラでのレンダリングから試してみましょう。bundle exec rails consoleを実行してRailsコンソールを開いて実行できます。

OrdersController.render :show, assigns: { order: Order.last }

上のコードではapp/views/orders/show.html.erbを使い、@orderOrder.lastを指定しています。assigns:でのインスタンス変数の記法はコントローラのアクションを書く場合と同じです。

2. コントローラでpartialをレンダリングする

ビューのpartialも同様にレンダリングできます。

OrdersController.render :_form, locals: { order: Order.last }

app/views/orders/_form.html.erbを使ってorderをローカル変数として渡しています。

3. コントローラでJSONやテキストでレンダリングする

以下のコードではOrderの全データをJSONでレンダリングします。

OrdersController.render json: Order.all

4. コントローラでテキストをレンダリングする

もちろんテキストを直接指定してレンダリングすることもできます。

OrdersController.render plain: 'this is awesome!'

ここまでが通常の方法です。

ActionController::Rendererrequest.envを使う

Railsアプリが受け取ったリクエストは、指定の環境に基いて処理されます。コントローラでこの環境を扱うには、request.envを使うのが常套手段です。Deviseなどのgemは、wardenトークンなどについてenvのハッシュに依存します。

つまり、コントローラの外でレンダリングする場合は適切な環境を指定する必要があります。

Railsではレンダリングに使えるデフォルトのRack環境が提供されており、renderer.defaultsで環境にアクセスできます。デフォルト環境にはexample.orgというダミーのドメイン名まで用意されています。

OrdersController.renderer.defaults

実際には、Rails内部でこのオプションに基いて新しいRack環境がビルドされます。

rendererでRack環境をカスタマイズする

ここからが本題です。Rack環境はrendererメソッドで簡単にカスタマイズできます。

renderer = ApplicationController.renderer.new(method: 'post', https: true)

あとはカスタマイズした環境を使って、コントローラを指定せずにレンダリングできます。

: 以下も元記事のコードからの引用ですが、これを実行するにはapp/views/の直下にダミーのshow.html.erbファイルを置く必要があります。show.html.erbの中身は空でかまいません。

renderer.render template: 'show', locals: { order: Order.last }

追伸: Deviseなどで認証する場合

同記事のコメント欄で引用されている記事「Using Rails 5 new renderer with Clearance and Devise」によると、Deviseなどによる認証を導入している場合はActionController::Renderer::RACK_KEY_TRANSLATIONにキーを追加して環境として認識させる必要があるとのことです。

実際のRailsアプリでも認証が併用されることが多いので、この点に注意が必要です。

New feature in Rails 5: Render views outside of actions」にはDeviseでの注意点についてさらに詳しく説明されています。

追伸: Rails 4.2でレンダリングを自由に行いたい場合

試していませんが、上述の機能をRails 4.2向けに移植したbrainopia/backport_new_rendererというgemがUsing Rails 5 new renderer with Clearance and Deviseで紹介されています。

利用法が少し違うので注意が必要です。

  # www.stefanwienert.de ブログより
  def self.render_with_signed_in_user(user, *args)
    renderer = ApplicationController.renderer.new
    renderer.env[:clearance] = Clearance::Session.new({}).tap{|i| i.sign_in(user) }
    renderer.render(*args)
  end

参考

関連記事(BigBinaryシリーズ)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。