express v2.5.0 掘削

とりあえず、example/mvc/app.js を起点に。
以下なんですが

var express = require('../../lib/express');

var app = express.createServer();

require('./mvc').boot(app);

app.listen(3000);
console.log('Express app started on port 3000');

一行目でいきなり express が出てきます。ちょっと package.json 確認しとく。

{
  "name": "express",
  "description": "Sinatra inspired web development framework",
  "version": "2.5.0",
  "author": "TJ Holowaychuk <tj@vision-media.ca>",
  "contributors": [ 
    { "name": "TJ Holowaychuk", "email": "tj@vision-media.ca" }, 
    { "name": "Aaron Heckmann", "email": "aaron.heckmann+github@gmail.com" },
    { "name": "Ciaran Jessup", "email": "ciaranj@gmail.com" },
    { "name": "Guillermo Rauch", "email": "rauchg@gmail.com" }
  ],
  "dependencies": {
    "connect": "1.7.x",
    "mime": ">= 0.0.1",
    "qs": ">= 0.3.1",
    "mkdirp": "0.0.7"
  },
(中略
  "engines": { "node": ">= 0.4.1 < 0.5.0" }
}

connect も確認してみたら version が 2.0.0alpha1 になっているので 1.7.1 なタグを checkout しておきます。

とゆーことは

まだ 0.6 は出たばっかなので安定版という意味では express 使うのであれば

  • node は 0.4 系
  • express は 2.5 系

をということになるのか。しかも node はマイナーが偶数番号なら安定版ということと理解すると、もうすこしすればライブラリもこれに対応したソレ達がお出ましになるのかどうなのか。

閑話休題

ええと、connect.middleware ってのが何なのかが若干 (?) 微妙。
とりあえず repl で確認してみるか、って 0.4 系が入ってないな。入れるか。

$ nvm install v0.4.12

暫く時間がかかるな。

入った

It worked
$ nvm list
v0.4.12  v0.5.5  v0.5.9  v0.6.0
current:        v0.4.12
$

ので repl 起動してみたんですが、connect が 2.0.0alpha だったりしたので node のバージョンの問題ってことにして node_modules を削除して再度 npm install してみました。どうなるか。ここでも再び少々時間がかかりますな。

qs@0.3.1 ./node_modules/qs 
mkdirp@0.0.7 ./node_modules/mkdirp 
mime@1.2.4 ./node_modules/mime 
should@0.3.2 ./node_modules/should 
express-messages@0.0.2 ./node_modules/express-messages 
connect@1.7.2 ./node_modules/connect 
ejs@0.4.2 ./node_modules/ejs 
node-markdown@0.1.0 ./node_modules/node-markdown 
hamljs@0.5.1 ./node_modules/hamljs 
connect-redis@1.1.0 ./node_modules/connect-redis 
└── redis@0.6.7
stylus@0.13.0 ./node_modules/stylus 
├── growl@1.1.0
└── cssom@0.2.0
jade@0.16.2 ./node_modules/jade 
└── commander@0.1.0
connect-form@0.2.1 ./node_modules/connect-form 
└── formidable@1.0.7
expresso@0.9.2 ./node_modules/expresso 
$

を、ちゃんと package.json の言う通りに作っているのかどうか。

$ node
> var connect = require('connect');
> connect.middleware
{ favicon: [Getter],
  router: [Getter],
  limit: [Getter],
  csrf: [Getter],
  static: [Getter],
  compiler: [Getter],
  directory: [Getter],
  query: [Getter],
  logger: [Getter],
  session: [Getter],
  vhost: [Getter],
  profiler: [Getter],
  cookieParser: [Getter],
  bodyParser: [Getter],
  staticCache: [Getter],
  methodOverride: [Getter],
  errorHandler: [Getter],
  responseTime: [Getter],
  basicAuth: [Getter] }
> 

これ、connect.js の以下な部分で云々してる結果ですね。

fs.readdirSync(__dirname + '/middleware').forEach(function(filename){
  if (/\.js$/.test(filename)) {
    var name = filename.substr(0, filename.lastIndexOf('.'));
    exports.middleware.__defineGetter__(name, function(){
      return require('./middleware/' + name);
    });
  }
});

middleware ディレクトリの中身とも一致してます。基本的には require した結果を戻しているようですね。で、express 側では以下な記述があるので

var exports = module.exports = connect.middleware;

express もこれを持ってるはず。repl で確認を。

$ node
> var express = require('express')
> express
{ favicon: [Getter],
  router: [Getter],
  limit: [Getter],
  csrf: [Getter],
  static: [Getter],
  compiler: [Getter],
  directory: [Getter],
  query: [Getter],
  logger: [Getter],
  session: [Getter],
  vhost: [Getter],
  profiler: [Getter],
  cookieParser: [Getter],
  bodyParser: [Getter],
  staticCache: [Getter],
  methodOverride: [Getter],
  errorHandler: [Getter],
  responseTime: [Getter],
  basicAuth: [Getter],
  version: '2.5.0',
  createServer: [Function],
  HTTPServer: [Function: HTTPServer],
  HTTPSServer: [Function: HTTPSServer],
  Route: [Function: Route],
  view: { [Function: View] register: [Function], compile: [Function], lookup: [Function] },
  View: { [Function: View] register: [Function], compile: [Function], lookup: [Function] } }
> 

basicAuth より下のナニが exports への代入式以下に書いてあるはず。いっちゃん下のソレは

// Error handler title

exports.errorHandler.title = 'Express';

repl で確認したところによれば以下。

> express.errorHandler
{ [Function: errorHandler] title: 'Express' }
> 

このあたり微妙に腑にオチない部分があったりします。が、そこはスルーとして

  • https.js
  • http.js
  • router/route.js
  • view.js
  • response.js
  • request.js

の確認が必要なのか。

https.js

とりあえず。なんとなくな理解で。まず最初に以下。

/**
 * Expose `HTTPSServer`.
 */

exports = module.exports = HTTPSServer;

コメントの通りなんですが、HTTPSServer は手続きオブジェクトです。直下で定義されてるのですが、これを express 側から呼び出しているのが以下なのかどうか。

exports.createServer = function(options){
  if ('object' == typeof options) {
    return new HTTPSServer(options, Array.prototype.slice.call(arguments, 1));
  } else {
    return new HTTPServer(Array.prototype.slice.call(arguments));
  }
};

createServer 手続きの定義なんですが、new という演算子を使ってます。色々確認してみるに、javascript では class みたいな演算子は無いのかな。ちょっと順に掘削してみたいと思います。
まず、express の中の https.js にある HTTPSServer な手続きの定義が以下。

function HTTPSServer(options, middleware){
  connect.HTTPSServer.call(this, options, []);
  this.init(middleware);
};

ここの this は新たに生成されたオブジェクトを指してるのか。で、connect.HTTPSServer が何かというと末端で expose されてます。

// expose constructors

exports.HTTPServer = HTTPServer;
exports.HTTPSServer = HTTPSServer;

これらは先頭で定義されていて以下。

var HTTPServer = require('./http').Server
  , HTTPSServer = require('./https').Server

実体は connect の中で定義されてる https.js になるのか。Server というソレも expose されてて定義は以下らしい。

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

上記、call を使って this も渡されているのでこの中の this も生成したオブジェクトになる訳ですか。
あと、謎なのが上記手続き定義の末端にもありますが、https というソレです。定義は以下になってます。

var HTTPServer = require('./http').Server
  , https = require('https');

これ、express 側の https.js でも require してますね。

var connect = require('connect')
  , HTTPServer = require('./http')
  , https = require('https');

repl にて確認。

$ node
> var https = require('https')
> https
{ Server: { [Function: Server] super_: { [Function: Server] super_: [Object] } },
  createServer: [Function],
  getAgent: [Function: getAgent],
  Agent: { [Function: Agent] super_: { [Function: Agent] super_: [Function: EventEmitter], defaultMaxSockets: 5 } },
  request: [Function],
  get: [Function] }
> 

こりゃ、Node が持ってる https なオブジェクト (じゃねぇやモジュール) なのかどうか。
とりあえず middleware が云々とか use がなんたら、というあたりはスルーしよう。
あとは express 側も connect 側も http なメソドを mixin してます。両方とも以下なカンジ (以下は express 側の引用です)。

// mixin HTTPServer methods

Object.keys(HTTPServer.prototype).forEach(function(method){
  app[method] = HTTPServer.prototype[method];
});
また、親子関係として
>||
express の https -> connect の https -> https

という関係になっているようです。まず express での記述が以下。

/**
 * Inherit from `connect.HTTPSServer`.
 */

app.__proto__ = connect.HTTPSServer.prototype;

次に connect 側の記述が以下。

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

Server.prototype.__proto__ = https.Server.prototype;

むむ。

http.js

ボリューム的にはこっちの方が、ですがこっちも Node が用意してる http を云々、という気がしております。とりあえず手続きをコメントと一緒に列挙してみるか。

  • init
    • Initialize the server.
  • remove
    • Remove routes matching the given `path`.
  • lookup
    • Lookup routes defined with a path equivalent to `path`.
  • match
    • Lookup routes matching the given `url`.
  • onvhost
    • When using the vhost() middleware register error handlers.
  • registerErrorHandlers
    • Register error handlers.
  • use
    • Proxy `connect.HTTPServer#use()` to apply settings to mounted applications.
  • mounted
    • Assign a callback `fn` which is called when this `Server` is passed to `Server#use()`.
  • register
    • See: view.register.
  • helpers
  • locals
    • Register the given view helpers `obj`. This method can be called several times to apply additional helpers.
  • dynamicHelpers
    • Register the given dynamic view helpers `obj`. This method can be called several times to apply additional helpers.
  • param
    • Map the given param placeholder `name`(s) to the given callback(s).
  • error
    • Assign a custom exception handler callback `fn`. These handlers are always _last_ in the middleware stack.
  • is
    • Register the given callback `fn` for the given `type`.
  • set
    • Assign `setting` to `val`, or return `setting`'s value. Mounted servers inherit their parent server's settings.
  • enabled
    • Check if `setting` is enabled.
  • disabled
    • Check if `setting` is disabled.
  • enable
    • Enable `setting`.
  • disable
    • Disable `setting`.
  • redirect
    • Redirect `key` to `url`.
  • configure
    • Configure callback for zero or more envs, when no env is specified that callback will be invoked for all environments. Any combination can be used multiple times, in any order desired.
  • forEach
    • Delegate `.VERB(...)` calls to `.route(VERB, ...)`.
  • all
    • Special-cased "all" method, applying the given route `path`, middleware, and callback to _every_ HTTP method.
  • del
    • del -> delete alias

流石に多い。とは言え

  • connect が Node 標準実装な http モジュールを拡張している
  • express の http モジュールは connext な http モジュールを拡張している
  • https についても同様

というのは理解できた。てかその理解は正しいはず。

router/route.js

どこで使ってるのか分からんかったのですが、http.js の init にて以下な記述を発見。

  // router
  this.routes = new Router(this);
  this.__defineGetter__('router', function(){
    this.__usedRouter = true;
    return self.routes.middleware;
  });

Router って何だっけ、と言いつつ上を探してみると以下な記述が。

var qs = require('qs')
  , connect = require('connect')
  , router = require('./router')
  , Router = require('./router')
  , view = require('./view')
  , toArray = require('./utils').toArray
  , methods = router.methods.concat('del', 'all')
  , url = require('url')
  , utils = connect.utils;

ちなみに ./router ってのはディレクトリです。中身は

  • collection.js
  • index.js
  • methods.js
  • route.js

という方々がいらっしゃいます。なんとなく index.js 開いてみたのですがビンゴなのかどうなのか。いちおう自分以外の .js を require してらっしゃる模様。
コンストラクタのプロトタイプも当たりに見えます。詳細は別途、ってことにしといてざっくり列挙したソレを全部さらっておきたいと思います。

view.js

expose されているらしい手続きを列挙してみます。

  • compile
  • lookup
    • compile から呼び出されてますね

ええと、時間切れかな。具体的に何を routing しているのかが分からんぞ。render なあたり、若干明確にイメージできていなかったりしてます。
てゆーか、Rails でいう route なナニは何処にあるのか。ええと mvc 配下で言えば mvc.js の bootController 手続きなのか。もう一段ウワから確認。
bootControllers で controllers 配下のファイル毎に bootController 手続きが呼び出されてます。

// Bootstrap controllers

function bootControllers(app) {
  fs.readdir(__dirname + '/controllers', function(err, files){
    if (err) throw err;
    files.forEach(function(file){
      bootController(app, file);
    });
  });
}

で、ファイル毎に諸々。

// Example (simplistic) controller support

function bootController(app, file) {
  var name = file.replace('.js', '')
    , actions = require('./controllers/' + name)
    , plural = name + 's' // realistically we would use an inflection lib
    , prefix = '/' + plural; 

  // Special case for "app"
  if (name == 'app') prefix = '/';

app.js だったら / なソレになる模様。あと、prefix が user.js だったら /users って形になるらしい。actions は例えば mvc の controllers/app.js であれば以下が取り出されて云々、なのかな。

module.exports = {
  
  // /
  
  index: function(req, res){
    res.render();
  }
};

で、mvc.js の中で定義されている bootController 手続きにて

  Object.keys(actions).map(function(action){
    var fn = controllerAction(name, plural, action, actions[action]);
    switch(action) {
      case 'index':
        app.get(prefix, fn);
        break;

みたいな記述になってます。controllerAction 手続きは手続きオブジェクトを戻していらっしゃいます。戻される手続きオブジェクトは何なんでしょ。
上記で言うと action には 'index' で actions[action] にはそれに対応する手続きオブジェクトが割り当てられてると見てるんですが、res.render に手続きオブジェクトが割り当てられて手続きオブジェクトが apply されるって見方で良いのかなぁ。

  return function(req, res, next){
    var render = res.render
      , format = req.params.format
      , path = __dirname + '/views/' + name + '/' + action + '.html';
    res.render = function(obj, options, fn){
      res.render = render;
      // Template path
      if (typeof obj === 'string') {
        return res.render(obj, options, fn);
      }

      // Format support
      if (action == 'show' && format) {
        if (format === 'json') {
          return res.send(obj);
        } else {
          throw new Error('unsupported format "' + format + '"');
        }
      }

      // Render template
      res.render = render;
      options = options || {};
      // Expose obj as the "users" or "user" local
      if (action == 'index') {
        options[plural] = obj;
      } else {
        options[name] = obj;
      }
      return res.render(path, options, fn);
    };
    fn.apply(this, arguments);
  };

うう、でも結局 res.render が戻されておるのう。このあたりの協調な関係がまだ見えてないなぁ。類推多いし。

むむ

これ、なんとかして動かせないかなぁ。今ってもしかして動く状態なのかどうなのか。とは言え今日はもう駄目だ。

とりあえず

以下は別途かなorz

  • response.js
  • request.js