こんにちは、hachi8833です。BigBinaryシリーズ、今回はRails 5で導入されたActionController::Rendererを使って、コントローラの制約を受けずに任意のビューテンプレートをレンダリングする方法をご紹介します。
元記事
確認に使った環境
- Rails 5バージョン: 5.0.2(5-0-stable)
- Rubyバージョン: 2.4.0p0
この環境で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を使い、@order
でOrder.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::Renderer
とrequest.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
参考
- Using Rails 5 new renderer with Clearance and Devise
- New feature in Rails 5: Render views outside of actions
- Rails 5で追加されたinitializerのまとめ