Connect ソースコードリーディング(3) - middleware の作成方法
前置き
今回は、前回までのソースコードリーディングで得た知識を利用して、実際に Connect の middleware を作成してみます。
ソースは以下においてあります。
hakobera/connect-middleware-sample · GitHub
作成するもの
GET リクエストにのみ対応した Express 風の Router モジュールを作ります。
完成すると以下のようなソースが書けるようになります。
var connect = require('connect'), router = require('./lib/router'); var server = module.exports = connect.createServer(); server .use(connect.logger()) .use(connect.favicon()) .use(router(__dirname + '/views')); // <= これが今回作成した middleware // テンプレートを描画する router.get('/', function(req, res) { res.render('index'); }); router.get('/hello', function(req, res) { res.render('hello'); }); // レスポンスと直接返すこともできる router.get('/echo', function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); var message = req.query.message; res.end(message); }); server.listen(3000); console.log('Server is running at port: %d', server.address().port);
middleware の作り方
middleware の作り方は至って単純です。前回説明しましたが、Connect の middleware とは以下のシグネチャを持つ Function を返すファクトリメソッドです。
// エラー発生時( next(err) )にも呼び出して欲しい場合 function (err, req, res, next) { ... } // エラー発生時には呼び出して欲しくない場合 function (req, res, next) { ... }
なので、最もシンプルな middleware は以下のようになります。
exports = module.exports = function nop() { return function(req, res, next) {}; };
これに必要な処理を追加することで middleware になります。今回作成するのは Router なので、引数3つの後者のシグネチャを利用します。
全ソース
モジュールのソースは lib/router.js です。コメント込みで 60行程度です。
主にやっていること
- ルータの呼び出し前後に簡易ログを出力
- URL マッピング (URL と 処理のマッピング)
- ServerRequest に query プロパティを追加
- ServerResponse に render メソッドを追加
以降、ここからピックアップして解説していきます。
lib/router.js
var fs = require('fs'), url = require('url'), qs = require('querystring'); /** * Simple routing module for connect. * * @param {String} viewRoot Root path of view files. */ exports = module.exports = function router(viewRoot) { if (!viewRoot) { throw new Error('viewRoot is required'); } var stack = []; exports.get = function(path, handler) { stack.push({ path: path, handler: handler }); }; return function(req, res, next) { console.log('router#start'); if (req.method !== 'GET') { next(); } else { res.render = function(name) { var viewFile = viewRoot + '/' + name + '.html'; fs.readFile(viewFile, function(err, data) { if (err) { next(err); } else { res.writeHeader(200, { 'Content-Type': 'text/html' }); res.end(data); } }); }; var handled = false, path = url.parse(req.url, true), i, l, handle; for (i = 0, l = stack.length; i < l; ++i) { handle = stack[i]; if (path.pathname === handle.path) { console.log(handle); req.query = path.query; handle.handler(req, res, next); handled = true; break; } } if (!handled) { next(); } } console.log('router#end'); }; };
ルータの呼び出し前後に簡易ログを出力
return function(req, res, next) { console.log('router#start'); // ... next(); // ... console.log('router#end);
middleware はリクエストの前後に処理を挟み込めます。前処理は next メソッドの呼び出し前に、後処理は next メソッドの後ろに書きます。ここでは単純にメソッド呼び出し開始、終了をコンソールに出力しています。
前回、middleware は use した順番に呼び出されると書きましたが、以下のような場合、aの前処理、bの前処理、aの後処理、bの後処理の順番で処理が実行されます。(a が next を呼び出した場合に限る)
// a, b の順で use
connect.use(a()).use(b());
なお、req, res に関しては、next メソッドの呼出し前と後で内容が変わっている可能性があるので、実装の時には注意して下さい。
URL マッピング (URL と 処理のマッピング)
やっていることは、connect の URL マッピング同じような処理です。ただし、req.url には QueryString も含まれるため、一度 url.parse メソッドでパースして、パスをクエリに分けています。
exports = module.exports = function router(viewRoot) { // ... var stack = []; // URLに対応する handler の登録 exports.get = function(path, handler) { stack.push({ path: path, handler: handler }); }; return function(req, res, next) { // ... var handled = false, path = url.parse(req.url, true), // リクエスト URL をパースし、パスとQueryString オブジェクトを得る i, l, handle; for (i = 0, l = stack.length; i < l; ++i) { // 登録された URL に対応する handler を探す handle = stack[i]; if (path.pathname === handle.path) { console.log(handle); req.query = path.query; handle.handler(req, res, next); // 見つかったら handler に処理を移譲。 handled = true; break; // 最初に見つかったもののみ処理する } } // ... }
今回、組込みモジュールを真似て、router.get メソッドを Function内で定義しています。個人的に JavaScript の変数スコープもあってすっきり書けて良いかなと思いました。
exports = module.exports = function router(viewRoot) { // ... var stack = []; // URLに対応する handler の登録 exports.get = function(path, handler) { stack.push({ path: path, handler: handler }); };
http.ServerRequest に query オブジェクト追加
middleware では ServerRequest を拡張することができます。今回は GET のリクエストパラメータをパースして、オブジェクト化したものを query という名前のプロパティに代入してあります。
var handled = false, path = url.parse(req.url, true), i, l, handle; for (i = 0, l = stack.length; i < l; ++i) { handle = stack[i]; if (path.pathname === handle.path) { console.log(handle); req.query = path.query; handle.handler(req, res, next); handled = true; break; } }
これで登録した handler 内で以下のようにリクエストパラメータの値を取得できるようになります。
/* /echo?message=hello とアクセスすると */ router.get('/echo', function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); var message = req.query.message; // ここで 'hello' が取得できる res.end(message); });
ServerResponse に render メソッドを追加
middleware では ServerReponse を拡張することもできます。今回は指定された名前に対応する HTML ファイルをレスポンスとして書きだす render メソッドを追加しています。ServerResponse の拡張と言っても、普通に JavaScript のオブジェクトにメソッドを追加しているだけです。
res.render = function(name) { var viewFile = viewRoot + '/' + name + '.html'; fs.readFile(viewFile, function(err, data) { if (err) { next(err); } else { res.writeHeader(200, { 'Content-Type': 'text/html' }); res.end(data); } }); };
まとめ
- middleware はリクエストの前後に処理を挟み込める
- ServerRequest, ServerResponse にプロパティやメソッドを追加することができる。追加されたものは後続の middleware から参照可能
次回予告
組込み middleware、サードパーティー middleware から幾つかをピックアップして解説する予定(次回は1日飛んで日曜日の予定)