読者です 読者をやめる 読者になる 読者になる

北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

Connectのソースを読む(2)

JS node

前回の続きで、Connect 1.8.4のソースを読む。今日はミドルウェアから適当に抜粋。残ってるのはミドルウェアだけなので、今日で終わりのつもり(気が向いたら残りも書くかも?)。

middleware/logger.js

アクセスログを書き込むだけだが、connectのミドルウェアの仕組みではリクエスト完了後に割り込むことができない*1のでhackが必要となる。

具体的に言うと、res.endをラップして終了処理に割り込む。そして、何もせずnextを呼べば、リクエストの完了時に割り込んで処理をすることができる。ただし、immediate オプションを渡した場合はこのhackを行わず、ミドルウェアに処理が移った段階でログを吐く*2

ログに使える:date、:method などは全てloggerオブジェクトのプロパティにメソッドとして定義されており、req, res, field の3引数を渡すことで値を取り出せる。ログのフォーマットとして文字列を渡すと、これらのメソッドを適宜呼び出すような関数へコンパイルされる。

middleware/errorHandler.js

4引数を持つ唯一のミドルウェア。4引数のミドルウェアはエラーハンドリングのために呼び出される。

~accept.indexOf の形式が多用されているが、ビット反転すると-1が0になることを使っていて、文字列が見つからなかった場合にのみfalseとなる。

内部はAcceptヘッダを見てレスポンスをhtml、json、text に分けてスタックを出力するだけの簡単な処理。

utils.pause, resume

ミドルウェアから使われているutils.js 内の関数。任意のObjectについてpauseすると、以後のdataイベントとonイベントがキャッシュされる。キャッシュされたイベントはresumeで再び元のObjectへ渡される。

ミドルウェアから非同期で後続のミドルウェアへ処理を渡そうとする場合は、このメソッドを使う必要がある。そうしないと、後続のミドルウェアがreqからデータを読むためにdataイベントやendイベントを拾おうとしている場合に、それらのミドルウェアがリスナーをしかける前に発生したdataやendのイベントが全て捨てられてしまう。

middleware/basicAuth.js

BASIC認証の実装。Authorization ヘッダがあって認証が成功していればreq.remoteUserへユーザ名をセット、そうでなければ401ステータスを返して認証を促す、というのを愚直に実装している。

basicAuth()の引数の取り方は3パターンで、「user, pass, realm」「cb(user, pass), realm」「cb(user, pass, cb(err, user)), realm」の3通り。最後のパターンは非同期で認証を行うパターンで、認証が完了したらcbへエラー、またはユーザ名を返せばよい。

middleware/router.js

Sinatraライクのルータの実現。ミドルウェアの中でソースは一番行数が多い。

router関数の中で、まずDSLを実現するためにgetやらpostやらのメソッドを持つmethodsというオブジェクトを作っている。利用者はこいつのgetやらpostやらへルーチング情報を渡せばいいというスンポー。getやpostには、Sinatraなどと同じようにPATHとハンドラを渡せるが、ハンドラに加えてミドルウェアも任意個指定ができる。

後、ドキュメント化されてないapp.paramって関数が生えていて、こいつを呼ぶとparamsという変数へコールバックを貯められるが、この辺の実装はかなり怪しい。要はルーチングでマッチしたパラメータに前処理するためのコールバックなのだけど、普通の関数の他にcb(req, res, next, val) というミドルウェアっぽい(けど違う)引数をとるコールバックも指定できて、整理されてない感がある(非同期対応が必要なのはDBから値を読むことを想定してるんだろうけど)。

ルーチングの情報はroutesオブジェクトへメソッドごとに分けて保存される。getやらpostを呼んだときの処理を作っているのがgenerateMethodFunction で、ここでは基本的にroutesの中へルーチング情報を詰め込むだけ。PATHの正規表現へのコンパイルはnormalizePath関数が行う。

本丸は内部のrouter(req, res, next)関数でこいつが実際にルーチングをする。match関数によってマッチするルートがあるか調べられ、マッチした場合はキャプチャーした値がfn.params へ保存される*3

さらに内部にあるparamという関数は、前述のparamを前処理するコールバックを反復的に適用するために再帰呼び出しされる関数。これはcb(req, res, next, val)のシグネチャを持つ前処理関数が非同期呼び出しに対応しているために必要となる。なお、この前処理の最中でnextへエラーとして'route'という文字列を渡すと、このハンドラでの処理を諦めて大域脱出し、後続のルーターのマッチ処理へ戻る。

paramの再帰の最後で、ハンドラと共に指定したミドルウェアを適用するため、さらに内部にあるnextMiddleware という関数による再帰が起こる。ここでもミドルウェアがnextへエラーとして'route'を渡すと大域脱出できる。ここのミドルウェアの適用は自前で処理しているが、app(req, res, nextMiddleware) の形式のものしかサポートしていないため、エラー処理をするfn(err, req, res, nextMiddleware)の形式のミドルウェアを渡しても機能しない。

ここまでを経て、ようやく最終的にユーザがgetやpostへ指定したハンドラがキックされる。ハンドラ内からnextを呼ぶと、次のルーチングの検索へ移る。その必要がない場合はres.endでレスポンスを完了すればよい。

最終的にマッチするルートがなかった場合は、OPTIONSメソッドの場合はAllow:ヘッダ入りのレスポンスをするか、それにも該当しなければ後続のミドルウェアへ処理を渡す。

作成されたrouterミドルウェアには、.remove, .lookup, .match のメソッドが生えており、作成後にルーティングを削除したり、どんなリクエストがマッチするのかテストしたりできる。

*1:リクエストを完了すると、後続のミドルウェアには処理を移さないため

*2:immediateを指定した場合は、当然ステータスコードなどを記録することはできない

*3:fn はgetやpostに渡したハンドラで、こいつを処理結果の共有の(グローバルの)一時保存場所にしててすごくよくない。paramsという名前もコールバックを保存する変数名と被ってて、この辺の実装はすごくよくない。