Rails: リクエストのライフサイクルとRackを理解する(翻訳)
本記事は、私達がRailsConf 2019で行ったスピーチのまとめです。スライドはこちらでご覧いただけます。
エディタでコントローラのファイルを開き、アクションメソッドにRubyコードを少々書いて、ブラウザでURLを叩けば、書いたコードは即動き出します。ほとんどのRails開発者は、こうしたワークフローについてそこそこ慣れ親しんでいるはずです。しかし、このしくみを深く考えてみたことはありますか?ブラウザのアドレスバーにURLを入力してからコントローラのメソッドが呼び出されるまでのしくみを説明できますか?そのメソッドを実際に呼び出しているのがどこかご存知でしょうか?
インターネッツの旅路
ランチに誰かを誘おうとしている状況を考えてみましょう。「Pastiniでどう?」というメッセージは同僚に送信するには十分ですが、その地域に不案内な知人には少々不親切です。普通なら図のようにレストランの住所も送ってあげるべきでしょう。これでタクシーに行き先を伝えるなりマップで調べるなりできるようになります。
コンピュータでもこれと似たようなことが言えます。ブラウザにドメイン名だけを入力した場合、ブラウザの最初の仕事はサーバーに接続することです。「skylight.io」のようなドメイン名は人間には覚えやすくても、コンピュータはそれだけではサーバーを見つけられません。サーバーへの到達方法を知るには、そうしたドメイン名をコンピュータネットワークのアドレス、すなわちIPアドレス(Internet Protocolアドレスの略です、念のため)に変換する必要があります。
うすうすお気づきかと思いますが、IPアドレスは34.194.84.73
のような感じになります。コンピュータはこうしたIPアドレスを用いることで、インターネット上で互いに接続されたネットワークをたどって目的のサーバーにたどり着けるようになります。
DNS(ドメインネームシステム)は、コンピュータがドメイン名をIPアドレスに変換して正しいサーバーを見つけるためのものです。皆さんのコンピュータでdig
というユーティリティですぐに試せます(なおオンライン版のdigもあります)
; <<>> DiG 9.10.6 <<>> skylight.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32689
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;skylight.io. IN A
;; ANSWER SECTION:
skylight.io. 59 IN A 34.194.84.73
;; Query time: 34 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Mon Apr 29 14:50:34 PDT 2019
;; MSG SIZE rcvd: 56
dig
の出力は何だか難しそうに見えますが、重要なのは「ANSWER SECTION」です。ここではskylight.io
が34.194.84.73
というIPアドレスに解決されています。
DNSは、IPアドレスに対応付けられたドメイン名を登録する場所です。ドメインを購入または所有した場合は、この対応付けを自分で行わなければなりません。そうしないと、顧客はあなたのサーバーを見つけられません。
サーバーのIPアドレスがわかれば、ブラウザからサーバーに接続できるようになります。ブラウザからサーバーへの接続方法はなかなか興味深いものです。両者の接続は、受話器を手に取って相手の電話番号に電話をかけているようなものと考えることが可能です。
実はこの部分についても、telnet
と呼ばれるツールで実際に「生の」接続を試すことができます。たとえば、telnet 34.194.84.73 80
と入力することで、先ほどのサーバーへの接続をオープンにできます。80はデフォルトのHTTPポート番号です。
訳注
telnetはmacOSやLinuxの多くのディストリビューションにはデフォルトでインストールされていません(telnetは通信を暗号化できません)。
接続に成功したら、何かメッセージを伝えなければなりませんが、どんなメッセージを伝えればよいのでしょうか?ブラウザとサーバーが相手のメッセージを理解するには、互いに「話す」言葉について合意が取れていなければなりません。その言葉こそがHTTP(Hyper Text Transfer Protocl)と呼ばれるものです。ブラウザとサーバーは、どちらもHTTPを理解できます。
ここで行える最もシンプルなリクエストとして「skylight.io/hello」を送るには、「リクエストの種類: GET」「対象ホスト: skylight.io」「対象パス: /hello」を指定します。
GET /hello HTTP/1.1
Host: skylight.io
上をtelnetセッションに正しく入力すれば(リクエストの終わりを示すために、最後に空行を入力するのが肝心です)、サーバーから以下のようなレスポンスを受け取ります。
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Thu, 25 Apr 2019 18:52:54 GMT
Hello World
上は、リクエストが成功したことを示しています。いくつかのヘッダー情報に続いて、コントローラでレンダリングした「Hello World」というテキストが最後に表示されています。
HTTPは(バイナリプロトコルではない)平文テキストでできたプロトコルなので、人間が読んで理解することもデバッグすることもできます。HTTPは、ブラウザがWebページやアセットをリクエストしたり、フォームを送信したり、キャッシュを操作したり、圧縮したりといったさまざまな操作を体系的に提供します。
このブラウザとサーバーの間の通信は、一般の電話と同様、暗号化なしの状態です。
このリクエストは、相手側に届くまでにさまざまな場所を通過します。このカンファレンスのWi-Fiルーター、会場のコンベンションセンターのルーター、インターネットプロバイダ、サーバーをホスティングしている企業など、リクエストが正しい場所に届くまでに多くの中間ネットワークを経由します。つまり、その途中ではブラウザとサーバーのやりとりを盗み聞きする機会はいくらでもあるということです。しかし皆さんはもちろん盗み聞きなどされたくありませんよね?
ご心配なく。やりとりの内容は暗号化(encrypt)できます。暗号化されたやりとりも暗号化されていないやりとりとすべて同じところを通過するので、「やりとりが行われている」こと自体は中間地点にいてもわかります。しかし途中にいる者がやりとりの内容を理解することはできません。メッセージを復号(decrypt)するキーは、やりとりしているサーバーとブラウザしか持っていないからです。
これがHTTPSと呼ばれるプロトコルです(SはセキュアのSです)。HTTPSは、HTTPの単なる別種ではありません。上述のように平文テキストでやりとりをする点については同じですが、ブラウザはメッセージを送信する前にメッセージを暗号化し、サーバーはそれを復号してからメッセージを解釈します。
暗号化と復号は、ブラウザとサーバーの両方が秘密鍵(secret key)を用いて行います。ブラウザとサーバーの両者が暗号化通信に合意すれば、他の誰も内容を読むことはできません。なお、通信経路のあらゆる中間地点で通信を見ることができる状況で、ブラウザとサーバーが暗号化と復号に使う鍵を、鍵を奪われずにどうやって受け渡すのでしょうか?これは高度なトピックなので、別の機会にお話したいと思います。
サーバー
さて、ブラウザが無事サーバーに接続して、特定のWebページをサーバーにリクエストするところまで成功しました。このリクエストに対するレスポンスはどのように生成されるのでしょうか?
そもそもこれは何のサーバーなのかといえば、「Webサーバー」です。Webサーバーは、上述のとおり「HTTPを話す」サーバーです。apache、nginx, passenger、lighthttpd、unicorn、puma、果てはwebrickなどがWebサーバーの例です。Rubyで書かれたWebサーバーもありますし、C言語などで書かれたWebサーバーもあります。
Webサーバーの役割は、リクエストを「理解する」ことと、そのリクエストに対してどんなサービスを提供するかを決定することです。静的アセットを求めるシンプルなリクエストであれば、Webサーバーをそれ用に構成するだけで簡単に扱えるようになります。
たとえば、ブラウザが/assets/
以下にあるものをリクエストしたら、常にアプリの/public/assets
フォルダ以下のものを返すようにWebサーバーを設定したいとします。リクエストされたアセットが存在すれば、サーバーはそれを圧縮して返し、存在しない場合は「404 not found」ページを返すべきです。
利用しているWebサーバーによっては、そのサーバー固有の設定言語や設定構文も使えたりします。たとえばNginxを使っていれば、nginx.confに以下のような記述があるでしょう。
location /assets {
alias /var/myapp/public/assets;
gzip_static on;
gzip on;
expires max;
add_header Cache-Control public;
}
しかしさらに凝ったことをやろうとすると、話は複雑になってきます。
たとえば、「ブラウザで/blog
にアクセスしたら、直近のブログ記事10件をデータベースから取得して見た目を整え、コメントも表示し、HTMLヘッダーやフッターやナビゲーションバーを追加し、JavaScriptやCSSも追加してからレスポンスを返せ」とWebサーバーに指示したいとしましょう。
ここでWebサーバーの設定言語について解説しようとすると話がややこしくなってしまいます。しかし私たちにはRailsという味方がいるのですから、実際にWebサーバーに指示したいのは「リクエストを受け取ったら、そこから先はRailsに渡してお任せしろ」ということなのです。ところでWebサーバーとRailsはどのようにやりとりしているのでしょうか?
Rubyでは、この種の情報をやりとりするための手法がいろいろ考えられます。RailsがブロックにWebサーバーを登録する方法もあれば、WebサーバーがRailsのメソッドを呼び出す方法もあります。Webサーバーはリクエストの情報をメソッドの引数という形で渡すことも、環境変数で渡すこともできます。グローバル変数で渡すことすら可能でしょう。さて、WebサーバーからRailsにリクエストの情報を渡したとき、そこにどのようなオブジェクトが存在するべきでしょうか?言い換えれば、RailsはどのようにしてWebサーバーに返信するのでしょうか?
最終的には上のどの方法も利用可能ではありますが、重要なのは、やりとりの両側で同じ規約に合意することです。Rackはそのために誕生しました。Rackは、WebサーバーがRuby製Webフレームワークとやりとりする(あるいはその逆)ための統一APIを提供するミドルウェアです。どんなRubyフレームワークであっても、Rackプロトコルを実装してRackの規約に沿っていれば、上述のさまざまなWebサーバーとシームレスにやりとりできるようになります。
Rackは、ごくわずかな機能を実現するためのシンプルなRubyプロトコルであり規約です。WebサーバーはWebフレームワークに「そちらの担当分のリクエストが1件来たぞ、そうそう、パスとかHTTP verbとかヘッダーもあるのでよろしく」と通知する必要があります。WebフレームワークはWebサーバーに「ほいきた、こちらの処理は終わったので結果(ステータスコード、ヘッダー、body)をお返しするぞ」と返信する必要があります。
Rackはこうしたやりとりを軽量かつフレームワーク非依存に扱うため、Rubyでできる最もシンプルな方法を選択します。RackはWebフレームワークへの通知をメソッド呼び出しで行い、やりとりの詳細はメソッドの引数を経由させ、戻り値をメソッド呼び出しから返すことでWebフレームワークに返信します。
以上をコードで表すと次のような感じになります。
env = {
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/hello',
'HTTP_HOST' => 'skylight.io',
# ...
}
status, headers, body = app.call(env)
status # => 200
headers # => { 'Content-Type' => 'text/plain' }
body # => ['Hello World']
最初に、Webサーバーはハッシュを1つ用意します。これは「envハッシュ」と呼ばれるのが通例です。envハッシュにはHTTPリクエストの情報がすべて含まれています。たとえばREQUEST_METHOD
にはHTTP verbが、PATH_INFO
にはリクエストのパスが、HTTP_*
には対応するヘッダーの値が含まれます。
一方、アプリやフレームワークは#call
メソッドを1つ実装しなければなりません。Webサーバーは、env
ハッシュを唯一の引数としてこのメソッドを呼び出します。ここでは、env
ハッシュ内の情報に基づいてリクエストが処理されることと、正確に3つの要素を含むarray(「3つのタプル」とも言います)を1つ返すことが期待されます。
この3つの要素とは何だかおわかりでしょうか?
第1の要素はHTTPステータスコードです。200ならリクエストの成功を、404ならnot foundを表すといった具合です。
第2の要素は、Content-Type
などのレスポンスヘッダーを含む1個のハッシュです。
最後に第3の要素は、レスポンスのbodyとなる1個の配列です。このbodyはきっと文字列だろうとお思いの方もいるかもしれませんが、実は違います!いくつかの技術的な理由によって、このbodyは「each
可能なオブジェクト」、つまり文字列をyield
するeach
を実装しているオブジェクトなのです。シンプルな場合は、その中に文字列が1切れ入ったarrayを1個だけ返します。
では実際に動くところをお目にかけましょう!Railsでやる前に、シンプルなRackアプリを1つ手作りしてみます。
# app.rb
class HelloWorld
def call(env)
if env['PATH_INFO'] == '/hello'
[200, {'Content-Type' => 'text/plain'}, ['Hello World']]
else
[404, {'Content-Type' => 'text/plain'}, ['Not Found']]
end
end
end
これは、おそらく最もシンプルに構築できるRackアプリでしょう。継承をまったく使っておらず、#call
を実装するシンプルなクラスが1つあるだけです。このコードはenv
ハッシュのリクエストパスを参照し、パスが正確に/hello
とマッチすれば"Hello World"という平文レスポンスをレンダリングし、マッチしない場合は404 "Not Found"エラーレスポンスを出力します。
さてアプリができましたが、このアプリをどう使えばよいのでしょうか?アプリに仕事をさせるにはどうしたらよいのでしょうか?Rackは、Webサーバーが実装できるプロトコルの1つに過ぎないことを思い出しましょう。つまりこのアプリを、Rackの言葉でやりとりできるWebサーバーと接続する必要があります。
ありがたいことに、rack
という便利なgemがあります。これはRackの仕様を実装するお便利ユーティリティ集で、rackup
と呼ばれるサンプルWebサーバーを備えています。rackup
はconfig.ruと呼ばれる形式の設定ファイルを認識します。
# config.ru
require_relative 'app'
run HelloWorld.new
config.ruは基本的にRubyファイルですが、若干の設定DSLを含んでいます。ここでは先ほどのappファイルをrequire_relative
し、Hello Worldアプリのインスタンスを構成して、run
というDSLメソッドでrackup
サーバーに渡しています。
これで、config.ruファイルのあるディレクトリでrackup
コマンドを実行して、サーバーをデフォルトの9292番ポートにアタッチできるようになりました。http://localhost:9292/helloを開けば「Hello World」と表示され、http://localhost:9292/watあたりを開けば「Not Found」エラーが表示されます。
今度は、http://localhost:9292/
というルートパスからhttp://localhost:9292/hello
へのリダイレクトを追加したいとしましょう。この場合は次のようにアプリを変更できます。
# app.rb
class HelloWorld
def call(env)
if env['PATH_INFO'] == '/'
[301, {'Location' => '/hello'}, []]
elsif env['PATH_INFO'] == '/hello'
[200, {'Content-Type' => 'text/plain'}, ['Hello World']]
else
[404, {'Content-Type' => 'text/plain'}, ['Not Found']]
end
end
end
これでも一応動きますが、このままでは今後の拡張が面倒になります。ここに継ぎ足していくとなると、if
やelsif
やelse
やend
が延々チェインしていくことになります。リダイレクトはよく行われる操作でもあるので、アプリの他の場所でも使い回しが効くようにしたいものです。リダイレクト機能の実装をモジュラーかつ再利用可能かつコンポジション可能にできるとよさそうですね。
もちろんできます!
# app.rb
class Redirect
def initialize(app, from:, to:)
@app = app
@from = from
@to = to
end
def call(env)
if env["PATH_INFO"] == @from
[301, {"Location" => @to}, []]
else
@app.call(env)
end
end
end
class HelloWorld
def call(env)
if env["PATH_INFO"] == '/hello'
[200, {"Content-Type" => "text/plain"}, ["Hello World!"]]
else
[404, {"Content-Type" => "text/plain"}, ["Not Found!"]]
end
end
end
これでHelloWorld
クラスの部分を一切変更せずに済むようになりました。その代りに、単一の責務をこなす役割を担うRedirect
というクラスを新たに追加しました。マッチするパスがあればリダイレクトレスポンスを発行して終了し、ない場合は、このクラスに渡しておいた次のアプリに委譲します。
config.ruを以下のように変更して、リダイレクトが効くようにします。
require_relative 'app'
run Redirect.new(
HelloWorld.new,
from: '/',
to: '/hello'
)
上はHelloWorld
アプリのインスタンスを構成し、それをRedirect
アプリに渡します。
これでRackミドルウェアを実装できました!ミドルウェアはRackの仕様の技術的な一部ではありません。Webサーバー目線では、ここにはRedirect
というアプリがひとつあるきりです。このアプリの#call
メソッドの中でさらに別のRackアプリが呼ばれますが、Webサーバーはそのことについて関知する必要はありません。
このミドルウェアのパターンはよく使われますので、config.ruにはそれ用のDSLキーワードがあります。
require_relative 'app'
use Redirect, from: '/', to: '/hello'
run HelloWorld.new
use
キーワードを使えばネストがきれいさっぱりなくなります。嬉しいですね!
このミドルウェアパターンはきわめて強力です。余分なコードを書かなくても、rack gemのミドルウェアを数個追加するだけで、圧縮機能やHTTPキャッシュやHEAD
リクエストのハンドリングをアプリに追加できます。
require_relative 'app'
use Rack::Deflater
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Redirect, from: '/', to: '/hello'
run HelloWorld.new
このようにしてとても機能的なアプリを構築できることがおわかりかと思います。
Rails 3より前のRack
ついにRailsまでたどり着きました!
言うまでもなく、RailsはRackを実装しています。自分のRailsアプリを覗いてみれば、以下の記述があるconfig.ruファイルが見つかるはずです。
require_relative 'config/environment'
run Rails.application
config.ruはrackup
由来ではありますが、その他のWebサーバーやHerokuなどのサービスでも認識されるので、config.ruがデフォルトでRailsに含まれているのは便利です。
run
キーワードには何らかのRackアプリを渡すことになっています。つまりRails.application
は#call
に応答するRackアプリでもあるはずです!Railsコンソールで試してみましょう。
仕様に沿ったenv
ハッシュをわざわざ手作りしなくても、rack gemのRack::MockRequest.env_for
ユーティリティメソッドでできます。このメソッドにURLをひとつ渡せば後は代わりにやってくれます。このRails.application.call
呼び出しにこのenv
ハッシュを渡せば、ステータスコードとヘッダーとbodyが期待どおりタプル(tuple)として生成されます。見慣れたリクエストログをコンソールに出力することもできます。素晴らしい!
ところで、このRailsアプリのconfig.ruにはどこにもuse
ステートメントがありません。Railsはミドルウェアを全然使っていないのでしょうか?そんなことはありません。実は、アプリに入っている全ミドルウェアを以下のコマンド一発でおなじみのconfig.ru構文の形で表示できます。
$ bin/rails middleware
use Rack::Sendfile
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Blorgh::Application.routes
これを見れば、Railsではcookieハンドリングなどの多くの機能を「ミドルウェア」の形で実装していることがわかります。これはうまい設計です。なにしろAPIサーバーを実装する場合は、不要なミドルウェアを削除するだけで済むのですから。ところでどうやってミドルウェアを削除するのでしょうか?
use
ステートメントは、単にconfig.ruでアプリのビルドを便利にするためのものであることを思い出しましょう。Webサーバーはアプリの最も外側の部分にしか注目していないのです。Railsのconfig/application.rb
にも、ミドルウェアを管理するのに便利なものがあります。
# config/application.rb
require_relative 'boot'
require 'rails/all'
Bundler.require(*Rails.groups)
module Blorgh
class Application < Rails::Application
# cookieを無効にする
config.middleware.delete ActionDispatch::Cookies
config.middleware.delete ActionDispatch::Session::CookieStore
config.middleware.delete ActionDispatch::Flash
# 独自のミドルウェアを追加する
config.middleware.use CaptchaEverywhere
end
end
ついにアプリまでたどり着いた!
ここまでミドルウェアについて見てきましたが、肝心のアプリはどうなったのでしょう?bin/rails middleware
の出力を見ると、use
がミドルウェア用、run
がアプリ用とわかるので、どうやらBlorgh::Application.routes
がそれに違いありません!
Railsコンソールで同じテストを実行すると、Rails.application.call
が無事Blorgh::Application.routes
に置き換わっているかどうかを確認できます。すべて問題なく動いています。だとすると、このRackアプリとは一体何なのでしょう?そしてこのRackアプリはどこから来たのでしょう?
このRackアプリはリクエストURLに着目し、膨大なルーティング規則とマッチさせて、呼び出すべき正しいコントローラとアクションを検索します。Railsはこのconfig/routes.rb
に基づいて、アプリを生成してくれます。
# config/routes.rb
Rails.application.routes.draw do
resources :posts
end
ほとんどのRails開発者はresources
DSLを既にご存知でしょう。resources
は、多くのルールを一度に定義するショートハンドです。resources
は最終的に7つのルーティングに展開されます。
# config/routes.rb
Rails.application.routes.draw do
# resources :postsから以下ができる
get '/posts' => 'posts#index'
get '/posts/new' => 'posts#new'
post '/posts' => 'posts#create'
get '/posts/:id' => 'posts#show'
get '/posts/:id/edit' => 'posts#edit'
put '/posts/:id' => 'posts#update'
delete '/posts/:id' => 'posts#destroy'
end
たとえばGET
リクエストを/posts
に送信するとPostsController#index
メソッドが呼び出され、PUT
リクエストを/posts/:id
に送信するとPostsController#update
メソッドが呼び出されます。
ところでこのposts#index
という文字列は一体何なのでしょう?PostsController
のindex
アクションだということぐらいはわかります。Railsでこのコードを追ってみると、最終的にPostsController.action(:index)
という形に展開されます。さてこれは一体?
Action Controllerのコードをうんとシンプルにしたものを以下に示します。
class ActionController::Base
def self.action(name)
->(env) {
request = ActionDispatch::Request.new(env)
response = ActionDispatch::Response.new(request)
controller = self.new(request, response)
controller.process_action(name)
response.to_a
}
end
attr_reader :request, :response, :params
def initialize(request, response)
@request = request
@response = response
@params = request.params
end
def process_action(name)
event = 'process_action.action_controller'
payload = {
controller: self.class.name,
action: action_name,
# ...
}
ActiveSupport::Notifications.instrument(event, payload) do
self.send(name)
end
end
end
上の冒頭にはaction
クラスメソッドがあり、lambda(->
)を返すことがわかります。このlambdaはenv
と呼ばれる引数を1つ受け取ります。このenv
は何かというと、驚かないでください、ハッシュなのです!そしてlambdaが返すのは何とarrayなのです!さらにこのlambdaは#call
に応答するのです!つまりこれがRackアプリです。隅から隅までRackアプリなのです。
最後にここまでの話をまとめて考えると、Rackアプリは以下のような姿のルーティングアプリであることが想像できるでしょう。
class BlorghRoutes
def call(env)
verb = env['REQUEST_METHOD']
path = env['PATH_INFO']
if verb == 'GET' && path == '/posts'
PostsController.action(:index).call(env)
elsif verb == 'GET' && path == '/posts/new'
PostsController.action(:new).call(env)
elsif verb == 'POST' && path == '/posts'
PostsController.action(:create).call(env)
elsif verb == 'GET' && path =~ %r(/posts/.+)
PostsController.action(:show).call(env)
elsif verb == 'GET' && path =~ %r(/posts/.+/edit)
PostsController.action(:edit).call(env)
elsif verb == 'PUT' && path =~ %r(/posts)
PostsController.action(:update).call(env)
elsif verb == 'DELETE' && path = %r(/posts/.+)
PostsController.action(:destroy).call(env)
else
[404, {'Content-Type': 'text-plain', ...}, ['Not Found!']]
end
end
end
上のコードは、渡されたリクエストパスとHTTP verbを、自分のルーティング設定で定義されたルールとマッチさせ、コントローラで適切なRackアプリに委譲します。こういうコードを手作りしなくて済むのは、ひとえにRailsのおかげです!
ところで、Railsはこの対応付けを行うために、どうやってこれをルーティング設定から生成し、すべてのリクエストを効率よくルーティングするのでしょうか?これについては昨年のRailsConfでちょうどこの話題を扱ったセッションがあります↓ので、どうぞご覧ください。
これで「すべてはRackアプリである」ことがわかりました。私たちはこれらを自由に「選んで組み合わせられる」のです1。ここでプロならではの技😆をいくつかお目にかけましょう。
1. Railsアプリの一部からRackアプリへのルーティングを次のように書けることをご存知ですか?
Rails.application.routes.draw do
get '/hello' => HelloWorld.new
end
2. 私たちは既にlambdaについて学んだので、以下のようにインラインで書くことも可能です。
Rails.application.routes.draw do
get '/hello' => ->(env) {
[200, {'Content-Type': 'text/plain'}, ['Hello World!']
}
end
3. ルーターのredirect
DSLは何をしていると思いますか?驚かないでください!実はRackアプリを返すのです。「信じられない」と思う人もいるかもしれませんが、もう皆さんも先ほどこの機能を使っています。
Rails.application.routes.draw do
# redirect(...) はRackアプリを返す!
get '/' => redirect('/hello')
end
4. その気になれば、Railsアプリの中でSinatraアプリをマウントすることすら可能です。まさかと思う人もいるかもしれませんが、SidekiqのWeb UIはSinatraで書かれています。つまり皆さんもRailsアプリの中でSinatraアプリを動かしたことがあるかもしれないのです。
Rails.application.routes.draw do
mount Sidekiq::Web, at: '/sidekiq'
end
原文追記
Sidekiq 4.2以降のWeb UIは、外部依存性を削減するためにカスタムフレームワークに移行しました。もちろん相変わらずRackプロトコルを使っています。
この移行のプルリクは、今回Rackプロトコルについて学んだことを元に最小版のSinatraをビルドするのに必要なものを学ぶうえで、興味深い事例です(特に基本的なルーティングの取扱、ビューのレンダリング、リダイレクトなど)。
5. もちろん、逆にSinatraアプリの中でRailsアプリをマウントすることも可能ですが、ここは皆さんのご想像にお任せしましょう。
ここで学んだことを応用すれば、controller#action
という文字列を以下のように置き換えることすらできます。
Rails.application.routes.draw do
get '/posts' => PostsController.action(:index)
get '/posts/new' => PostsController.action(:new)
post '/posts' => PostsController.action(:create)
get '/posts/:id' => PostsController.action(:show)
get '/posts/:id/edit' => PostsController.action(:edit)
put '/posts/:id' => PostsController.action(:update)
delete '/posts/:id' => PostsController.action(:destroy)
end
その気になれば、bin/rails middleware
の出力をconfig.ruファイルにコピペすることもできます。
なお、実際には皆さんのRailsアプリでこういうやんちゃをするのはおすすめしません。そんなことをしたら、オートローディングや一部のパフォーマンス最適化がバイパスされてgemでミドルウェアを足すこともできなくなり、Rails周りで将来変更が発生したときにつらくなるだけです。とはいえ、このようにしてすべてが組み立てられていることを理解するのは実にクールですよね。
そういうわけで、やっとのことで出発地点であるコントローラアクションまで帰ってきました。「ところでrender plain...
が、Rack仕様で要求されるレスポンスのタプルになるまでの道のりの話はどうなったのか?」今回は残念ながら時間がありませんが、続きは「The Lifecycle of a Rails Response」的なセッションかブログ記事を出すと思いますのでご期待ください。
お知らせ: Skylightのしくみ
本記事では、フレームワークが魔法でも何でもないことを学んできました。フレームワークはあくまで単なるレイヤの1つであり、統一的に十分定義されたプリミティブの上にそのレイヤがトッピングされたものに過ぎません。こうした規約(convention)を学ぶことは、Railsの使い方やあなたのスキルを他の開発者と共有するうえで有用ですが、何よりも、規約はコミュニティのすべての人々にツールを書く力を与えてくれます。
たとえば、私たちSkylightではリクエストの実行全体に要した時間を測定する手段を必要としています。ミドルウェアよりも効果的に測定をやれるのはどんな方法でしょうか?
$ bin/rails middleware
use Skylight::Middleware
use Rack::Sendfile
use ActionDispatch::Executor
...
run Blorgh::Application.routes
「設定より規約」は、単にRailsアプリを構築する方法論にとどまりません。Railsの規約があることで、Skylightは独自の設定を書かなくても顧客のWebアプリの詳細な情報を集められるのです。
当初、私たちはRailsがアクションをどのようにディスパッチしているかをActionController::Base#process_action
をシンプルにしたもので調べました。このメソッドの内部では、Railsがアクションをコントローラにディスパッチするときに組み込みのActiveSupport::Notifications
というinstrumentation(測定)システムを用いて、何らかの事象が発生していることをSkylightなどのライブラリに通知します。SkylightはこのAPIによって、顧客のWebアプリのエンドポイント名を設定なしで取得します。
Skylightでは平均レスポンス時間以外の情報も提供できるので、顧客のWebアプリのリクエスト全体について集約された情報をお届けします。私たちはActiveSupport::Notifications
やRailsコミュニティの規約を駆使して、RailsのテンプレートレンダリングやActive Recordの実行タイミングはもちろん、著名なHTTPライブラリやライブラリのキャッシュ状況、Mongoidなど他のデータベースライブラリについても、Rails規約に沿った詳細情報を提供します。
デフォルトでは顧客のWebアプリのリクエストの重要な部分のみを表示しますので、何が起こっているかを急いで調べることに集中できます。この例であれば、エンドポイントを高速化するにはおそらくAppSerializer
を集中的にチェックするべきでしょう。Skylightは、顧客がより多くの情報を表示する必要が生じれば、顧客のWebアプリで用いられているすべてのRackミドルウェアなどのさまざまな情報も収集します。
以上は、Skylightが提供するサービスの氷山の一角に過ぎません。ご興味がおありでしたら、こちらの採用情報をご覧ください!
おたより発掘
WebサーバーとRackとWebアプリケーションに思いを馳せていたら、ちょうど気になっていた記事の翻訳がデプロイされていた!
いつもありがとうございます🙏 > hachi8833さんhttps://t.co/Em4z0L3a8A> 「すべてはRackアプリである」
最高大好き🤘— しおい (Misaki Shioi) (@coe401_) October 3, 2019
Rackの公式ドキュメントとして公開してほしいくらい、わかりやすい!
Railsのリクエストのライフサイクルを理解する(翻訳) https://t.co/WmC7HpnNXf
— Masafumi Koba 🇺🇦 (@ybiquitous) October 3, 2019
関連記事
- 訳注:「MIX & Match」は韓国ドラマのタイトルです。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。画像は元記事の引用です。