5分でRailsでRackミドルウェアを試した備忘録。

こんな感じなミドルウェアなクラスを作る。

initializeの第一引数でappをインスタンス変数に保持して、callメソッドを定義するだけ。callメソッドは @app.call(env) を呼び出しつつ、戻り値をcallメソッドの戻り値として使うことに注意。

config.middleware.use でミドルウェアを登録する。

この状態でrailsを起動してアクセスすると、こんな感じで作ったミドルウェアの処理が差し込まれる。

仕組みについてざっくり

Rails::Serverはconfig.ruからRack::Builderのインスタンスを作ってRackアプリを起動している。Rack::Builderのコードはこんな感じ↓

initializeでブロックが渡されていると、instance_evalでブロックが評価される。つまり、インスタンスのコンテキストでブロックが評価される。このブロックはconfig.ruの中身が入る。config.ru内のuseやrunはRack::Builderのメソッドを呼び出していることになる。

useメソッドは@useを配列として、 Procで包んだミドルウェアのインスタンスを入れている。runは@runに起動するメインのRackアプリを入れている。callメソッドはto_app.callを呼び出しており、to_appは各オブジェクトをフリーズしつつミドルウェアでアプリを包んでいる。

キモは app = @use.reverse.inject(app) { |a,e| e[a].tap { |x| x.freeze if @freeze_app } } の部分。

@useにはProcの配列が入っており、これに対してinjectのループを回している。

injectの初期値はapp、つまりメインのRackアプリである。ループの一周目はaにはapp、eは@useの最後に格納したミドルウェアをラップしたProcが入る。Procは[]で引数付きの呼び出しとなるのでe[a] は Procの第一引数をappにした呼び出しとなる。引数のappはmiddleware.newの第一引数としてセットされる。次のループではこのappをラップしたミドルウェアが変数a、次のミドルウェアが変数eに入るので、次のミドルウェアの第一引数にはappをラップしたミドルウェア(変数a)が入ることになる。

このようにしてappとミドルウェアが次々に入れ子になっていく。こんなイメージ

このラップされたappをcallすると、最初に定義したミドルウェアから順にapp.call前の処理が評価されていき、appのcallが呼ばれ、app.callの後の処理が最後に定義したミドルウェアの順に処理されることになる。

参考URL