前回,前々回の記事では,Rackの生まれた背景,Rackとは何か,実際にRackアプリケーションを作る際に使えるものをご紹介しましたが,もう一つまだ説明していない重要な要素がRackにはあります。今回は,そのミドルウェアという仕組みについてご紹介します。
ミドルウェアとは
ミドルウェアとは何かを一言で言うと,「別なアプリケーションをラップして,リクエストやレスポンスを加工したり,処理を切り換えたりするRackアプリケーション」です。
この仕組みがあることで一体何ができるのでしょうか。Webアプリケーションを作っていると,リクエストやレスポンスをアプリケーションに行く前やアプリケーションの処理の後に加工したくなることはよくあります。例えば,条件に応じてURLの書き換えをしたり,エンコーディングの変換をしたり,Cookieの処理をしたり…といったことが日常茶飯事です。こういう処理を,サーバとアプリケーションの中間で行なうのがこのミドルウェアなのです。
ミドルウェアの作りかた
まずは,ミドルウェアを作ってみてその挙動を見てみましょう。とは言え,特に難しいことはありません。ミドルウェアが満たしていなければいけない要件は以下の二点です。
- Rackアプリケーションの仕様を満たしていること
- newの第一引数に他のRackアプリケーションを取ること
例えば,次のようなコードを書いて,neco_filter.rbという名前で保存してください。
neco_filter.rb
# coding: utf-8
class NecoFilter
def initialize(app)
@app = app
end
def call(env)
res = @app.call(env)
res[2].each do |body|
body.gsub!(/!|?|。|,/) { "にゃ#{$&}" }
end
res
end
end
そして,config.ruを次のように変更します。
# config: utf-8
require 'simple_app'
require 'neco_filter'
use NecoFilter
run SimpleApp.new
早速rackupでアプリケーションを起動してみましょう。出力が書き換えられているのがわかりますね。前回のSimpleApp同様,NecoFilterもとても簡単なものですが,ミドルウェアを理解する上で重要なポイントがいくつかあります。
まず,元のアプリケーションに一切変更を加えていないということです。出力や入力を書き換える汎用的な処理をこうしてミドルウェアとして実装しておけば,アプリケーションとは独立して付けたり外したりすることができます。また,サーバ側にも依存していないため,例えばApacheeからNginxに切り換えることになったときでも特にアプリケーションに変更を加える必要はなく,同じように使えます。
そして先ほど,ミドルウェアの要件として「他のRackアプリケーション内部に持つRackアプリケーションであること」と言及しました。ミドルウェアそのものもまたRackアプリケーションですので,ミドルウェアを複数重ねることもできます。上記のサンプルではミドルウェアは一つしか使っていませんが,サーバのモジュールやフレームワークのプラグインと同じように,組み合わせて使うことができます。
最後にこれは重要なことですが,Rackベースのフレームワークで実装したアプリケーションに対しても使えるということです。「Rackの仕様に則ってWebアプリケーションフレームワークを実装する」ということはすなわち「一個のRackアプリケーションとして動作する」,つまりRamazeやSinatraを使って開発されたアプリケーションはサーバやから見れば一つのRackアプリケーションになっているわけです。そのため,先程のNecoFilterミドルウェアも既存のSinatraアプリケーションに適用することができます。こうして汎用的な機能をミドルウェアとして切り出しておくことで,フレームワークをまたいで使うことができるのです。
もちろんパフォーマンスが何より重要な状況であれば,Webサーバのモジュールとして実装した方が当然高速でリソースも節約できます。あるいは,フレームワーク内部のプラグイン機構やフックを利用した方が,自由度はあがるでしょう。ですが,そういう制約がないのであれば,ミドルウェアとして実装されていた方が「サーバとフレームワークを自由に組み合わせることができる」というメリットをより享受できるのです。
余談ですが,ミドルウェアはある決まった形式だというだけのただのRackアプリケーションですので,config.ruで次のように書いても同じ意味になります。
# config: utf-8
require 'simple_app'
require 'neco_filter'
run NecoFilter.new(SimpleApp.new)
useはRack::Builderで用意されたDSLの記法で,後続のミドルウェアやアプリケーションを引数に取ってnewしてHandlerの引数にする,ということをおこなっています。ミドルウェアの要件として「newの第一引数がRackアプリケーションであること」があるのはこの記法を利用するためです。もしuseを使わないのであれば,Rack::URLMapのように,独自のイニシャライザを定義してアプリケーションとして実装してもいいかもしれません。
またconfig.ruやRack::Builderを使わない場合,たとえばCGI等で直接アプリケーションをハンドラに渡す場合は,
use MiddlewareA
use MiddlewareB
run App.new
となっていたものが,以下のような形になります。
Rack::Handler::CGI.run MiddlewareA.new( MiddlewareB.new( App.new ) )

