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

expressコードリーディング その2 connect/http.jsを読む

node.js

前回に引き続きconnect.jsを読む。今回はconnect/http.js。ここがconnectのコア。

まず最初にコンストラクタ。ここでhttp.Serverを継承してる。んでhttp.Server.call(this, this.handle)ってやってるのでリクエストハンドラをthis.handleにしてる。リクエストは全部このthis.handleで処理されるということになる。

var Server = exports.Server = function HTTPServer(middleware) {
  this.stack = [];
  middleware.forEach(function(fn){
    this.use(fn);
  }, this);
  http.Server.call(this, this.handle);
};

/**
 * Inherit from `http.Server.prototype`.
 */

Server.prototype.__proto__ = http.Server.prototype;

んで次にServer.prototype.use。これは受け取ったハンドラをstackにpushするだけ。引数のタイプとかでちょいちょい何かやってるけど基本それだけ。大事なのはハンドラ(とroute)をstackにpushするってこと。

Server.prototype.use = function(route, handle){
  this.route = '/';

  // default route to '/'
  if ('string' != typeof route) {
    handle = route;
    route = '/';
  }

  // wrap sub-apps
  if ('function' == typeof handle.handle) {
    var server = handle;
    server.route = route;
    handle = function(req, res, next) {
      server.handle(req, res, next);
    };
  }

  // wrap vanilla http.Servers
  if (handle instanceof http.Server) {
    handle = handle.listeners('request')[0];
  }

  // normalize route to not trail with slash
  if ('/' == route[route.length - 1]) {
    route = route.substr(0, route.length - 1);
  }

  // add the middleware
  this.stack.push({ route: route, handle: handle });

  // allow chaining
  return this;
};

ここの引数のhandleってのは普通にリクエストハンドラで、res、req、nextの引数を受け取る。nextが呼ばれると次のハンドラを順々に呼んでいくっていうのがconnectの仕組み。
routeっていうのはベースとなるパスを設定できる。

var connect = require('connect');
connect.createServer()
.use('/foo', function(req, res, next) {
  res.write('request: ' + req.method + ' ' + req.url);
  res.end();
})
.listen(3000);

こう書いたら、http://localhost:3000/foo/* のみでハンドラが実行される。http://localhost:3000/foo/barの結果は「request: GET /bar」になる(req.urlが置き換わってる)。

んで最後にServer.prototype.handle。これは全体のリクエストハンドラで、リクエストがきたらとりあえずこいつが実行される。useでpushされたstackを順々に実行していってnextの処理とかエラー処理とかrouteの処理とかをうまいことやってくれる。

Server.prototype.handle = function(req, res, out) {
  var writeHead = res.writeHead
    , stack = this.stack
    , removed = ''
    , index = 0;

  function next(err) {
    var layer, path, c;
    req.url = removed + req.url;
    req.originalUrl = req.originalUrl || req.url;
    removed = '';

    layer = stack[index++];

    // all done
    if (!layer) {
      // but wait! we have a parent
      if (out) return out(err);

      // otherwise send a proper error message to the browser.
      if (err) {
        var msg = 'production' == env
          ? 'Internal Server Error'
          : err.stack || err.toString();

        // output to stderr in a non-test env
        if ('test' != env) console.error(err.stack || err.toString());

        res.statusCode = 500;
        res.setHeader('Content-Type', 'text/plain');
        res.end(msg);
      } else {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Cannot ' + req.method + ' ' + req.url);
      }
      return;
    }

    try {
      path = parse(req.url).pathname;
      if (undefined == path) path = '/';

      // skip this layer if the route doesn't match.
      if (0 != path.indexOf(layer.route)) return next(err);

      c = path[layer.route.length];
      if (c && '/' != c && '.' != c) return next(err);

      // Call the layer handler
      // Trim off the part of the url that matches the route
      removed = layer.route;
      req.url = req.url.substr(removed.length);

      // Ensure leading slash
      if ('/' != req.url[0]) req.url = '/' + req.url;

      var arity = layer.handle.length;
      if (err) {
        if (arity === 4) {
          layer.handle(err, req, res, next);
        } else {
          next(err);
        }
      } else if (arity < 4) {
        layer.handle(req, res, next);
      } else {
        next();
      }
    } catch (e) {
      if (e instanceof assert.AssertionError) {
        console.error(e.stack + '\n');
        next(e);
      } else {
        next(e);
      }
    }
  }
  next();
};

connectのコアなコードはこれくらい。そんなに難しくない。同梱されてるmiddlewareがけっこうたくさんあるのでそのコード見るとどういう感じでmiddleware書けばいいかわかりそう。