3StepでRailsとRackに入門する
この記事の背景
Railsを使っていると、Rackという言葉をよく耳にしますが、説明しろと言われると「。。。」という感じなので、
Rackとは何か、Railsでどう動いているのかをいるのかを調べてみました。
僕と同じような人が、この記事でRack説明できるようになったり、ミドルウェア層でのデバッグができるようになると嬉しいです。
Step1. RackとRackミドルウェアとは?について調べる
Rackとは?
Rackは文脈によってアーキテクチャのことなのか、ライブラリのことなのかを使い分ける必要があります。
アーキテクチャとしてのRackは、サーバーとアプリケーションの接続を簡単にするためにインターフェースの定義をしたものです。
このインターフェースの定義では下記の3つを満たすことを必要としています。
- callメソッドに応答すること
- callメソッドはHTTPリクエストの情報が詰まった1つの引数を取ること
- callメソッドは、ステータス、ヘッダー、ボディの三つを含んだ配列を返すこと
GemとしてのRackはこの定義に沿って、HTTPリクエストを標準化してアプリケーション層に渡し、レスポンスを返してくれるライブラリです。
Railsでも内部的にRackを採用しているので、Railsは一つのRackアプリケーションといえます。
Rackミドルウェアとは?
Rackアプリケーションをラップする形で、リクエストの前後で処理を行うためのコンポーネントです。(抽象的すぎるので、以降のStepのサンプルをご覧ください🙇)
Rackアプリケーションに、リクエスト情報を渡す前にデータを加工したり、Rackアプリケーションが返すレスポンスに手を加工したりできます。
画像の通り入れ子構造で、データが流れてイメージです。
Step2.RailsとRackがどのように関わっているか調べる
RailsアプリケーションはRackアプリケーション?
Railsガイドから、bin/rails s
コマンドは、Rackの起動コマンドをラップしたもののようです。
Railsアプリケーションには、config.ruというファイルがあります。
bin/rails s
を実行すると、rackupコマンドが実行されてこのファイルが読み込まれ、Rackサーバーの設定とアプリケーションの起動を行います。
試しに下記のように書き換えてサーバーを起動すると、このファイルが読み込まれていることがわかります。
require_relative "config/environment"
puts "=== Rackアプリケーションを起動"
run Rails.application
Rails.application.load_server
runメソッドの引数には、Rackに準拠したアプリケーションを渡す必要があるため、Rails.applicationはRackアプリケーションであることがわかります。
Railsに組み込まれたRackミドルウェアの確認
railsサーバーを起動して、下記にアクセスすると、Rackの情報が表示されます。
http://localhost:3000/rails/info/properties
下記のコマンドでも、Rackミドルウェアの一覧を確認できます。
bin/rails middleware
ここで表示されるミドルウェアは、Railsアプリケーションに組み込まれているミドルウェア全てが実行順で表示されます。
つまり、リクエストが上から順に流れていきRailsアプリケーションに渡され、レスポンスを返す時には下から上に流れていきます。
use Rack::Cors
↓ ↑
use ActionDispatch::HostAuthorization
↓ ↑
use Rack::Sendfile
↓ ↑
use ActionDispatch::Static
↓ ↑
use ActionDispatch::Executor
↓ ↑
use ActionDispatch::ServerTiming
.
.
↓ ↑
run HogeApi::Application.routes
Step3.RailsアプリケーションにRackミドルウェアを追加してみる
なんとなくRackについてわかってきたところで、最後に実際にRackミドルウェアを作成してRailsアプリケーションに追加してみます。
Rackミドルウェアの作成
標準出力だけを行う二つのミドルウェアを作成して、ミドルウェアの実行順を確認します。
# Rackミドルウェアは、Rackの規約に則る必要があります
# つまり、callメソッドを持ち、引数としてenvを受け取り、
# ステータス、ヘッダー、ボディの配列を返すオブジェクトである必要があります
class HelloWorld
def initialize(app)
# このappは、次に呼ばれるミドルウェアやアプリケーションが入っています
@app = app
end
def call(env)
puts "Hello, World! before"
# appは次のミドルウェアが入っているので、リクエストに対する処理はここに書きます
status, headers, body = @app.call(env)
# 一番下の階層のアプリケーションまでたどり着いたらここに戻ってくるので、
# レスポンスに対する処理はここに書きます
puts "Hello, World! after"
# 規約に則り、ステータス、ヘッダー、ボディの配列を返しています
[status, headers, body]
end
end
class ByebyeWorld
def initialize(app)
@app = app
end
def call(env)
puts "Byebye, World! before"
status, headers, body = @app.call(env)
puts "Byebye, World! after"
[status, headers, body]
end
end
Rackミドルウェアを組み込む
これだけだと、Railsアプリケーションには読み込まれないので、config/application.rbに追記します。
# config/application.rb
require_relative "../lib/byebye_world"
require_relative "../lib/hello_world"
class Application < Rails::Application
config.middleware.use HelloWorld
config.middleware.use ByebyeWorld
end
bin/rails middleware
で確認すると、追加したミドルウェアが設定ファイルに記載した順で表示されると思います。
上記の例でいくと、リクエストを受け付ける時は、最初にHelloWorldが実行され、その後にByebyeWorldが実行されるという流れになります。
逆に、レスポンスを返す時は、ByebyeWorldが最初に実行され、その後にHelloWorldが実行されるという流れになります。
もちろん指定した位置にミドルウェアを置くこともできます。
実際に動くところを確認する
実際にサーバーを起動して、出力を確認すると、組み込んだ順に処理が進んでいることがわかります。
まとめ
Rackについて調べたり、動かしてみてRailsのサーバーとしての機能をより知ることができました。
また、ミドルウェア層でのデバッグや、ログ取得ができるようになったので、今後の開発に活かせそうです。
ご指摘を大いに受け付けていますので、コメントいただけると大変嬉しいです。
参考リンク
Discussion