NodeのHTTPモジュールは素晴らしいが、より複雑なアプリケーションを簡単に開発したい場合にはExpresを利用することができる。Expressは数々のモジュールを統合して、短期間でWebアプリケーション開発を行うことができる柔軟なインターフェイスを提供する。

Expressプロジェクトの生成から始め、プロジェクトの構成を理解し、最後にはバックグラウンドのデータベースにMongoDBを使ったExpressアプリケーションを構築する。


Expressプロジェクトを作成する
Expressは、Nodeのモジュールとしてnodeのスクリプト内で動作するとともに、コマンドライン実行ファイルも提供する。Expressをコマンドラインで実行すると、さまざまなディレクトリとファイルで構成されたプロジェクトのテンプレートを出力する。このテンプレートをベースに、素早くアプリケーションを構築できる。

Expressモジュールは実行コマンドを持っているので、npmに-gオプションを付与してグローバルインストールする。

ターミナル
$ npm -g install express

Expressを利用してアプリケーションを構築する際に、アプリケーション名を決める必要がある。
今回アプリケーションの名前は、mimoeappにする。

ターミナル
$ express mimoeapp

このコマンドを実行すると、実行したディレクトリにmimoeappという新しいディレクトリが作成され、その中にさまざまファイルやディレクトリが生成される。

生成されたファイルの1つ、mimoeapp/package.jsonを開くと、mimoeアプリケーションのモジュール依存関係を参照できる。プロジェクト名(name)がデフォルトのままなので、mimoeappに変更しておく。

package.json
{
  "name": "mimoeapp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.4.8",
    "jade": "*"
  }
}

dependenciesプロパティにexpressとjade(テンプレートエンジン)が記載されている。

プロジェクトをまとめて管理するために、これらの必要なモジュールはすべてプロジェクトフォルダ(mimoeapp)にインストールしておくことが重要。mimoeappディレクトリで以下のコマンドを実行する。

ターミナル
$ npm install

このコマンドは、mimoeappディレクトリに新しいnode_modulesディレクトリを作成し、その中にアプリケーションが依存するモジュールをインストールする。
これで、Expressを使った開発を行うための基本的な準備が完了した。


expressコマンドを実行すると、Expressを使った開発に適したディレクトリ構造とファイルが生成される。プロジェクトのルートディレクトリにはapp.jsとpackage.jsonの2つのファイルが生成される。

package.jsonはCommonJSによって提唱されたフォーマットで、NodeモジュールやNodeアプリケーションのパッケージ内容を記載するために一般的に利用される。
npm installコマンドは実行ディレクトリのpackage.jsonを読み込み、dependenciesプロパティから依存関係を割り出して、node_modulesディレクトリを作成しその中に必要なモジュールをインストールする。

ローカルインストールはアプリケーションの安定性のために重要な役割を果たす。Nodeのrequire関数は、実行ディレクトリを基準に階層をさかのぼってnode_modulesディレクトリを探す。実行中のディレクトリにインストールされているモジュールが上位階層にインストールされているモジュールより優先して読み込まれるため、上位階層にあるモジュールのバージョンが変更されている場合でも、アプリケーションが構築された時点と同じバージョンのモジュールを使い続けることができる。また、プロジェクトディレクトリにすべての依存関係が含まれているので、ディレクトリをそのままプロジェクトとして簡単に配布することもできる。


app.jsファイルはExpressプロジェクトの基礎であり、アプリケーションそのもの。アプリケーションは常に以下のコマンドで起動する。


ターミナル
$ node app.js

express(1)コマンドはこれらのファイルの他に、public、routes、viewsという3つのサブディレクトリを生成する。

publicディレクトリはスタティックファイルを格納するディレクトリ。このディレクトリは、スタティックファイル配信を行うexpress.staticミドルウェアのパラメータに渡されるディレクトリ。このディレクトリには、images、javascripts、stylesheetsという、それぞれのコンテンツを格納するサブディレクトリが生成されている。

routesディレクトリにはindex.jsファイルが生成されている。app.jsはrequireでindex.jsを呼び出している。URLルーティングを行うには、ルートのリクエスト時に実行する関数をindex,jsのexportsオブジェクトに追加する。routes/index.jsはサーバ機能を提供するコードとURLルーティングのためのコードを分離し、app.jsの肥大化を防ぐ。

viewsディレクトリには、テンプレートファイルが格納される。



app.jsの内容を読む
app.jsファイルを開き、そのコードを見てみる。

app.js
/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

最初に、expressメソッドを実行してExpressアプリケーションオブジェクトを作成し、app変数に格納している。このオブジェクトはアプリケーションに必要な変数やメソッドを保持し、提供する。

環境設定(app.~)が2か所で実行されている。最初の宣言は、アプリケーション全体の設定を行っている。次に呼び出されているのには、developmentが最初の引数として渡されている。ここでは開発時のみに適用する設定を行う。環境名を引数として渡すことで、環境によって設定を分岐させることができる(development、productionなど)。

アプリケーション全体の設定を行う最初のコールバックには、app.setを使ってデフォルトのviewディレクトリ(views)や、テンプレートエンジン(view engine)が設定されている。

次に、app.useを使ってexpress.urlencoded、express.methodOverride、app.router、express.staticの各ミドルウェア(function(req,res,next){}の形式を持った関数)が設定されている。

Expressは、favicon、logger、urlencodedなどのConnectの基準ミドルウェアをexpressアプリケーションオブジェクト(app変数)のメソッドとして格納している。例えば、urlencodedはexpress.urlencodedとして呼び出すことができる。

app.useに渡されたミドルウェアはアプリケーションスタックに追加される。クライアントからリクエストを受け取ると、そのリクエストはスタックに渡されて、スタックされている一連のミドルウェアで順番に処理される(そのため、app.useでスタックに渡す順番を考慮する必要がある)。

app.useで最初にスタックに追加されているfaviconは、ブラウザからのfaviconリクエストを処理する。loggerは受信したリクエストのログを出力する。デフォルトでは基本的なログをコンソールに出力するが、カスタマイズも可能。

methodOverrideは、擬似的に生成されたPUTやDELETEなどのHTTPリクエストを取り扱うことができるメソッド。ほとんどのブラウザは、HTMLフォームでGETとPOST以外のメソッドでフォームデータを送信することはない。そのため、フォームの送信時に以下のようなタグで_methodというhiddenデータを加えて、サポートされていないメソッドのリクエストを擬似的に生成する。

<input type="hidden" name="_method" value="DELETE">

HTTPプロトコルには、GETやPOST以外のさまざまなメソッドが規定されている。
(HTTPプロトコルのメソッドについてはhttp://www.w3.org/Protocols/rfc2616/rfc2616-sec9.htmlを参照する)
Expressは擬似メソッドを使ってブラウザがサポートしていないメソッドを処理するが、通常のHTTPメソッドを処理することもできる。

app.routerは、app.getやapp.postなどで定義されているすべてのルートを保持している。アプリケーションスタックで、app.routerを処理する順番が回ってくると、保持しているルートの処理を実行する。アプリケーションスタックにapp.routerが渡されていない場合は、app.getなどで定義されたルート(ルート定義自体がミドルウェア)が渡された順番でアプリケーションスタックの末尾に追加される。

Express(Connect)のミドルウェア(関数)は以下の構造を持っている。

function(req,res,next){
 //処理
 next();
}

nextパラメータはコールバックのような仕組みで、次に実行するミドルウェアを呼び出す。app.jsではapp.routerがexpress.staticの前に配置されているため、リクエストが行われると、app.routerに格納されている(app.getやapp.postなどで設定した)ルートに合致するかどうかを判定する。app.routerでルートが発見されずnext()呼ばれた場合にのみ、express.staticでスタティックファイルを探す。



routes/index.jsの内容を読む
routes/index.jsはapp.jsからrequireで呼び出される。

app.js
var routes = require('./routes');

index.jsを直接指定していないが、requireにディレクトリが渡された場合、Nodeは自動的にindex.jsを探す。
(requireメソッドが読み込むファイルを判断するステップについては、http://nodejs.org/api/modules.html#modules_all_togetherを参照にする)

routes/index.js
/*
 * GET home page.
 */

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

exports.indexにルートを処理する関数を定義している。routes/index.jsはapp.jsで呼び出されてroutesオブジェクトに格納される。次のapp.get文にroutes.index関数が渡され、実行時にroutes/index.jsのexports.indexが呼び出される。

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

routes.indexは、基本的にhttp.createServerのコールバックと同じ構造だが、app.getの最初にパラメータに渡されたルートに対してのみ実行される。また、リクエスト(req)とレスポンス(res)パラメータはExpressによって拡張されている。

routes.indexはres.renderメソッドを呼び出している。titleプロパティで文字列を渡して、views/index.jadeから読み込んだテンプレートをもとにページを生成して返す。



環境の定義と設定
開発コードと本番コードでは、異なる設定が求められる。例えば、開発中はクライアントに対してデバッグ用の詳細なエラーメッセージを返したいが、本番環境では公開するエラーメッセージの情報を最小限に押さえたい場合などが考えられる。

Expressではapp.configureを使って、そのようなケースに対処することができる。複数の実行環境を設定し、それぞれの環境に異なった設定を行うことができる。

Expressが作成したapp.jsの中身を見てみる。

app.js
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

環境名文字列を引数として渡すと、それがNODE_ENV環境変数(app.get('env')、もしくはprosess.env.NODE_ENV)とマッチした場合に、2つ目の引数として渡されているコールバック関数を実行する。

expressコマンドでプロジェクトを作成した時点では、NODE_ENV環境変数にdevelopmentが入力された場合の設定を行う準備ができている。app.jsに以下の設定を追記すると、環境変数にproductionを指定して起動した場合はdevelopment向けの設定を無視してproduction向けのコールバックで記述した設定を行う。

app.js
if ('production' == app.get('env')) {
  app.set('port',8000);
}


Expressは、ワークフローごとの設定を分離する機能を提供する。環境名の文字列を渡すだけで、環境に応じた設定を使い分けることができる。

Expressは環境名を判断するために、process.envを利用してNODE_ENV環境変数の取得を試みる。NODE_ENVが設定されている場合はその値を、設定されていない場合はデフォルト値としてdevelopmentを使用する。

NODE_ENV環境変数にproductionを設定してアプリケーションを起動するには、以下のコマンドを実行する。

ターミナル
$ NODE_ENV=production node app.js



新たな環境を設定する
アプリケーションの開発中は、本番環境では使用しない特別な設定を行うことによって、よりスムーズに作業を行うことができる。例えば、本番環境に近いステージング環境の設定を開発用マシンで整えたい場合などに、環境設定の分岐を利用することができる。

本番サーバ上ではアプリケーションが特定のポート(80番)で動作するものの、開発用サーバでは、root権限を持たないため80番で動作させることができない場合、本番環境と同じ設定を行ったステージング環境を用意し、ポート番号だけを80番以外の番号に変更して最終テストを行うことができる。

app.jsに本番環境(production)とステージング環境(staging)を加えて、環境によってポート番号を変更するように設定する。

app.js
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

if ('staging' == app.get('env')) {
  app.use(express.errorHandler());
//ステージング環境の設定(本番環境の設定と同じ)
}

if ('production' == app.get('env')) {
  app.use(express.errorHandler());
  //本番環境の設定
  app.set('port',80);
}

全環境向けの環境設定ではポート番号が3000番(もしくは環境変数PORTの値)に設定されているが、本番環境では80番ポートを使用するように設定した。これを確認するために、アプリケーションを実行してみる。以下のように本番環境を起動する。

ターミナル
$ sudo NODE_ENV=production node app.js
Password:★入力する
Express server listening on port 80

実行時には、80番ポートで他のアプリケーション(httpdなど)が動作していないかどうかを確認する。



NODE_ENVを永続的に変更する
アプリケーション開発がステージングの段階に入って動作確認する際に、毎回ENV_NODE=stagingを入力していると手間がかかる。本番環境(production)の場合も同じ。本番環境の場合にサーバを再起動する回数は少ないが、起動するときにNODE_ENVを指定することを覚えておく必要がある。

ターミナル
$ export NODE_ENV=staging

この設定は、コマンドを実行したターミナル上で、ターミナルが開いている間有効。この設定を常時したい場合は、ランコマンド(rc)ファイルに書き込んでおく必要がある。シェルの種類により、rcファイルの名前は異なる。bashの場合は~/.bashrc(~はホームディレクトリ)、shの場合は~/.shrc、kshの場合は~/.kshrcなどとなる。

NODE_ENVにstagingを設定し、rcファイルに書き込む場合は以下のコマンドを発行する(bashの場合)。

ターミナル
$ echo -e "export NODE_ENV=staging\n" >> ~/.bashrc



ダイナミックURLルーティング
ExpressはさまざまなURLルーティングの方法を凌駕したパワフルなインターフェイスを提供する。Expressで動的なURLルーティングを設定する。

mimoeappディレクトリとその中身を使う。

Mr.Pageという架空のキャラクターのページを/pageに追加する。app.jsでURLのルートを設定している場所(app.get('/',routes.index)などがある場所)に次のコードを挿入する。

app.js
app.get('/', routes.index);
app.get('/page',function(req,res){
res.send('こんにちは。Mr.Pageです。');
});

ターミナル
$ node app.js
Express server listening on port 3000

http://localhost:3000/pageにアクセスすると、
こんにちは。Mr.Pageです。になる。

リクエストされたルートの一部をreq.paramsに格納して利用することもできる。同じ場所に下記のコードを追加して確かめる。

app.js
app.get('/:page',function(req,res){
res.send('こんにちは。Mr.' + req.params.page + 'です。');
});

/以下の文字列が:pageプレースホルダによって、req.params.pageに格納される。例えば、http://localhost:3000/mimoeにアクセスすると、
こんにちは。Mr.mimoeです。になる。

各ルートで返される値を直接app.jsに記述しても動作する。しかし、app.jsスクリプトの肥大化と複雑化を避けるために、通常はコールバック関数の内容を分離して、routes/index.jsに記述する。

routes/index.js
exports.mrpage = function(req,res){
res.send('こんにちは。Mr.Pageです。');
};

exports.anypage = function(req,res){
res.send('こんにちは。Mr.' + req.marams.page + 'です。');
};

app.jsがこれらのコールバック関数を呼ぶよう修正する。

app.js
app.get('/page', routes.mrpage);
app.get('/:page', routes.anypage);

コード変更前と同様の動作が実現できる。

app.getを使用して、/pageへの固定ルートを設定し、このルートへのアクセスがあった場合のサーバの処理をapp.getのコールバックに記述する。今回は、res.sent(httpモジュールのres.writeを拡張したメソッド)を使ってシンプルなテキストを出力している。

Expressは固定ルート設定の他にも、プレースホルダを使ったフレキシブルなURLルーティング機能を持っている。プレースホルダの例として、コードに:pageプレースホルダを記述してルーティングを行っている(:はプレースホルダを表し、その後に続く文字列はプレースホルダの名前)。

リクエストURLがプレースホルダの形にマッチすると、プレースホルダにあたる文字列がreq.paramsのパラメータとして格納される。その際のパラメータ名は:を除いたプレースホルダの名前(page)。本コードの場合は、例えば/NodeCookbookにアクセスがあると、req.params.pageにNodeCookbookという文字列が格納される。

ブラウザでhttp://localhost:3000/pageにアクセスすると「こんにちは。Mr.Pageです。」と表示され、http://localhost:3000/NodeCookbookにアクセスすると「こんにちは。Mr.NodeCookbookです。」と表示される。



ルートのバリエーション
プレースホルダを使ったURLルーティングを行う場合、プレースホルダに格納する文字列を、正規表現を使って制限できる。例えば以下のようなコードを記述することができる。

app.js
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set('case sensitive routing',true);      //追加



app.get('/', routes.index);
app.get('/:page([a-z]+)',routes.anypage);  //修正


pageプレースホルダに文字列マッチングを行い、小文字のアルファベットで構成された文字列に対してのみroutes.anypageコールバック関数を呼び出す。ただし、ルート判定の文字列で大文字小文字を認識するためには、環境設定内で、app.setでcase sensitive routingの設定を行っておく必要がある。

app.set('case sensitive routing',true);

http://localhost:3000/nodecookbookは「こんにちは。Mr.NodeCookbookです。」と返すが、http://localhost:3000/NodeCookbookは404エラーを返す。



オプションルート
ルートで呼び出すコールバック関数を使って、オプションルートを設定することができる。以下のコードをルート設定に追記する。

app.js
app.get('/:page/:admin',routes.anypageAdmin);

routes/index.jsに以下のコールバック関数を記述する。

routes/index.js
exports.anypageAdmin = function(req,res){
var admin = req.params.admin;
if(admin){
if(['add','delete'].indexOf(admin)! == -1){
if(admin === 'add'){
admin = '追加';
}else{
admin = '削除';
}
res.send(req.params.page + 'ページを' + admin + 'したいですか?');
return;
}
res.send(404);
}
}

routeの:adminプレースホルダに文字列が存在するかどうかを確認する。存在する場合は、その文字列が想定した文字列(addもしくはdelete)かどうかを判別して、それぞれに対応したレスポンスを返す。指定されている文字列ではない場合は404エラーを返す。


http://localhost:3000/nodecookbook/add   にアクセスすると
nodecookbookページを追加したいですか?    となり、 http://localhost:3000/nodecookbook/delete  にアクセスすると
nodecookbookページを削除したいですか?    となる。


この例では、指定する文字列が増えた場合にも容易に拡張できるが、拡張を行う必要性がない場合は、正規表現を使って下記のような記述も使用できる。

app.js
app.get(/^\/(\w+)\/(add|delete)?$/,routes.anypageAdmin);

routes/index.js
exports.anypageAdmin = function(req,res){
  res.send(req.params.page[0] + 'ページを' + req.params.admin[1] + 'したいですか?');
}




ワイルドカード
ルート指定時にアスタリスク(*)をワイルドカードとして使用できる。例えば次のようなルートを設定する。

app.js
app.get('/:page/*',routes.anypage);   //修正

routes.anypageは次のように記述する。正規表現やワイルドカードで取得された、プレースホルダの名前を持たないパラメータは、req.params[n]で指定することができる。

routes/index.js
exports.anypage = function(req,res){
var subpage = req.params[0];
var parentPage = subpage ? req.params.page + 'の' : ' ';
res.send(parentPage + (subpage || req.params.page) + 'へようこそ!');
};

http://localhost:3000/Node/Cookbookにアクセスすると、「NodeのCookbookへようこそ!」と表示される。http://localhost:3000/Node/にアクセスすると「Nodeへようこそ!」と表示される。

以下のようにワイルドカードを使うと、pageという文字列を含むすべてのURLがMr.Pageのページになる。

app.js
app.get('/*page*',routes.mrpage);





Nodeクックブック p.163