hakobera's blog

技術メモ。たまに雑談

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日飛んで日曜日の予定)