expressでapp全体の設定値をroutesで使う(module間で変数を受け渡す)方法いろいろ

Expressでテンプレートからプロジェクトを作ると、現在の最新版2.5.4では

.
├── app.js
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   └── index.js
└── views
    ├── index.jade
    └── layout.jade

6 directories, 6 files

のようにファイルとディレクトリが作られる。app.jsが本体で、

var express = require('express')
  , routes = require('./routes')

var app = module.exports = express.createServer();

...

app.get('/', routes.index);

のような形でパスに対するハンドラを登録する形になっている。routes/index.jsでは

exports.index = function(req, res){
  res.render('index', { title: 'Express' });
};

となっていて、ここでそれぞれリクエストに対する処理を記述する形で作っていける。

問題

例えばoauth認証を行うハンドラを作る際に、oauthのconsumer_keyやconsumer_secretなどの値はroutes/index.jsにはベタ書きしたくない。環境変数で管理しても良いけど、設定値の数が多くなってくるとつらいので、例えば起動時にapp.jsでconfigファイルを読み込んで使うようにする(その上で環境変数でちょっと弄ったりもできる)。((追記: 知らなかったけどNode v0.6以降はjsonファイルもrequireで読み込めるそうで… var config = require('./config');だけで良い))

var config = JSON.parse(require('fs').readFileSync('config.json'));
config.foo = process.env.NODE_ENV === 'production' ? 'bar' : 'baz';

ここで作ったconfig変数で一元管理するようにしたい。が、そうすると、その変数はroutes/index.jsからは使えない。
こういうときにどうすれば良いか。近い話が先日、nodejs_jpのMLで議論されていた。
Google グループ
自分で読んで理解した範囲で、幾つかの方法を検証してメリットとデメリットを考えてみた。自分だったらこうするかな、という形で書いたのでMLの内容とは違うアプローチだったりするけど。

  1. global変数にする
  2. app.setを使ってmodule.parentから参照する
  3. クロージャを使う
  4. 共有オブジェクトとして使うmoduleを作る

1. global変数にする

var宣言を外す((追記: もしくはglobalオブジェクトを使って明示的にglobal.config = ...と書く))ことで、変数がグローバルになる。

config = JSON.parse(require('fs').readFileSync('config.json'));
config.foo = process.env.NODE_ENV === 'production' ? 'bar' : 'baz';

グローバル変数ならroutes/index.jsからも参照できる。

exports.index = function (req, res) {
    console.log(config);        // available
    res.render('index', { title: 'Express' });
};
長所

簡単に使える…ってことくらい?

短所

グローバル汚染は致命的。どこで上書きされるか分からないし管理出来ない。

2. app.setを使ってmodule.parentから参照する

MLを読むまでmodule.parentって使ったことなくて知らなかった。requireした側のexportsオブジェクトを参照出来る…?

var app = module.exports = express.createServer();

var config = JSON.parse(require('fs').readFileSync('config.json'));
config.foo = process.env.NODE_ENV === 'production' ? 'bar' : 'baz';
app.set('config', config);

とセットしておくと、routes/index.jsからは

exports.index = function (req, res) {
    console.log(module.parent.exports.set('config')); // available
    res.render('index', { title: 'Express' });
};

といったかたちで、module.parent.exportsでapp.jsのexports(すなわちapp.js内のvar app)を得られるので、そこからsetを使って値を参照できる。

長所

グローバル汚染せずに、コード変更も少なくて済む

短所

app.setはexpressに依存しているし、express内でもapp.setを使っているので"env"や"view"などのキー名で設定値を弄ると危険。そしてmodule.parentを使って参照するのは相互の依存が強すぎて、例えばapp.jsが

var app = express.createServer();

としか書いていなくてexportsにappを入れていないと参照できないなどの問題が考えられる。

3. クロージャを使う

routesの使い方から変更する形になるけれど、例えばroutes/index.js

module.exports = function (config) {
    return {
        index: function (req, res) {
            console.log(config);
            res.render('index', { title: 'Express' });
        }
    };
};

のように「引数として設定値を受け取る関数」を返すように変形してやるこの関数がハンドラやオブジェクトを返す。で、app.js

var config = JSON.parse(require('fs').readFileSync('config.json'));
config.foo = process.env.NODE_ENV === 'production' ? 'bar' : 'baz';
routes = routes(config);

と、require('routes')で返ってきた「関数」にconfigを引数として渡してやることで、そのconfigを内部で使用するハンドラやオブジェクトを得ることができる。この例では実行結果を同名の変数に上書きしているけど、変えてやれば混乱することはないと思う。

長所

スコープが絞れる。引数にapp丸ごと渡して2.と同様にapp.setで参照することもできるが、そこまでやる必要がないので最低限のデータ受け渡しで済む。また、app->routesの一方的な渡し方になるので、例えばroutes側でconfigが勝手に書き換えられるといった心配がなくなる。

短所

関数呼び出しを忘れないように気をつけなければならない、とか routes側のコードの見通しが少し悪くなる…とか? まぁちょっとカッコ悪い気はする

4. 共有オブジェクトとして使うmoduleを作る

例えばlib/share.jsというのを作って、

module.exports = {};

とだけ書いておく。これを使って、データを共有する。app.jsでは

var config = JSON.parse(require('fs').readFileSync('config.json'));
config.foo = process.env.NODE_ENV === 'production' ? 'bar' : 'baz';
require('./lib/share').config = config;

といったかたちでshareオブジェクトに代入。routes/index.jsでは

exports.index = function (req, res) {
    console.log(require('./../lib/share').config); // available
    res.render('index', { title: 'Express' });
};

という具合に参照できる。
パスに"."とか".."とか書きたくない場合は環境変数NODE_PATHに通して起動してやれば良いらしい。

$ NODE_PATH=lib node app.js

もしくはpackage.json

{
    "name": "application-name"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.4"
    , "jade": ">= 0.0.1"
  }
  , "scripts": {
    "start": "NODE_PATH=lib node app.js"
  }
}

とstartコマンドで環境変数指定を書いてしまって

$ npm start

で起動。こうしてパスを通しておくと、どこからでもrequire('share')で参照できるようになる。ただし例えば"share"とうモジュールはnpmパッケージに存在しているので、このネーミングには注意する必要がある。

長所

共有するオブジェクトを定義しておくだけでrequire使えばどこからでも使えるようになるので便利

短所

上述のパス指定とモジュール名の衝突の懸念くらいか。個人的にはrequireしてきた値を変更する、というのが何か違和感があるのだけど… どこからでも変更できてしまうので最終的にどこでどういう変更が行われてどういう値になっているのか、がわかりにくくなりそうな気はする。

まとめ

MLの議論では他の方法も紹介されていたし他にも方法はあると思うので上記4つから選ばないといけないわけではない。結局それぞれ長所と短所があるので、それらを把握した上で使いどころを考えるしかないかな、と。ただグローバル汚染は問題外だと思うのでよっぽどのことがない限り使うべきではないでしょう。
個人的には3. の方法が好きなので自分はこの方式でやっていくつもり。