November 2015

Volume 30 Number 12

働くプログラマ - MEAN あれこれ: Express のルーティング

Ted Neward | 年 11 月 2015

Ted Neward「ノーディスト」の皆さん、おかえりなさい (これが、Node.js を日常的に使う人々を指す公式用語かどうかはわかりませんが、個人的には「ノードヘッド」や「ノードラーティ」、「ノードフェラトゥ」よりも聞こえがよいと思います)。

前回 (msdn.com/magazine/mt573719) は、Node.js と連携させる Express をインストールして、アプリケーション スタックを "N" スタック (Node のみ) から "EN" (Express と Node) スタックへと拡張しました。他にもテーマはありますが、Express (およびサポート パッケージとライブラリ) についてもう少し掘り下げて考えたいと思います。今回扱うトピックの 1 つ、Express のルーティングについては前回、コードで "/" 相対 URL パスに対する HTTP 要求への応答として "Hello World" を表示する関数をセットアップしたときに少し触れました。今回は、Express の世界をもう少し掘り下げて、さらに効果的に使用する方法を紹介します。

ちなみに、このシリーズの一環として作成した最新かつ最高のコードに関心のある方は、シリーズ最新のコードを使用した Microsoft Azure のサイトを参照してください (msdn-mean.azurewebsites.net)。発行日を考えると、このコラムの内容はサイトで使用しているものとおそらく一致しません。したがって、今後の動向を掴むにはこのサイトが役立ちます。

帰ってきたルーティング

図 1 は、前回扱った、シンプルでも必要なものをすべて含む app.js ファイルです。"エンドポイントが 1 つ" という、これまでにビルドしたアプリケーションの性質を示しています。

図 1 Express で作成した "Hello World" のコード

// Load modules
var express = require('express');
var debug = require('debug')('app');
// Create express instance
var app = express();
// Set up a simple route
app.get('/', function (req, res) {
  debug("/ requested");
  res.send('Hello World!');
});
// Start the server
var port = process.env.PORT || 3000;
debug("We picked up",port,"for the port");
var server = app.listen(port, function () {
  var host = server.address().address;
  var port = server.address().port;
  console.log('Example app listening at http://%s:%s', host, port);
});

問題は、「Set up a simple route」(シンプルなルートをセットアップする) というコメントの部分です。ここでは、HTTP 動詞 ("get") と相対 URL エンドポイント ("get" メソッドに最初の引数として渡している "/") によってマップされる、1 つのエンドポイントを確立します。

その他の HTTP 動詞のパターンはかなり簡単に推測できます。"POST" 要求には post、"PUT" 要求には put、"DELETE" 要求には delete を使用します。Express がサポートする動詞は他にもありますが、明らかに、これら 4 つの動詞は最も重要です。各動詞はその後、2 つ目の引数として関数を受け取ります。図 1 の例では、2 つ目の引数は、着信 HTTP 要求を処理する関数リテラルです。

MEAN の "E"

ほとんどの場合、ノーディストが Express ベースのアプリケーションを作成する方法は、私たち「ドットネッター」が ASP.NET アプリケーションを作成する方法と同じです。サーバーは、データと混然一体となったプレゼンテーション (HTML) を含む HTML ドキュメントを生成し、ブラウザーに送信します。その後、ユーザーがフォームに入力して、入力データを Express に "POST" します。または、ユーザーがリンクをクリックし "GET" を生成して Express に 返し、サーバー側のサイクルをすべて再実行します。Node.js で HTML を手書きするのは、Visual Basic や C# の場合と同じくらい大変なので、従来の ASP.NET アプリケーションにおける Razor 構文と同じ目的を果たすよう設計されている多くのツールが Node.js の世界にも登場しています。その結果、データとコードを混在させすぎずにプレゼンテーション層を記述することが容易になっています。

ただし、MEAN ベースのアプリケーションでは、AngularJS が完全なクライアント側のエクスペリエンスを形成するため、Express は ASP.NET MVC と同じ役割を引き受けます。この役割とは、具体的には単なるトランスポート層で、クライアントから未加工のデータ (通常は JSON 形式) を取得して、そのデータで操作 (通常は "格納"、"変更"、または "関連付けられたデータや関連するデータの検索" のいずれか) を実行し、未加工のデータ (こちらも通常は JSON 形式) をクライアント層に戻します。ここからは、テンプレート フレームワーク (Node.js では複数存在し、"handlebars" と "jade" の 2 つがよく使用される) をテーマにするのを避け、単なる JSON の送受信にはっきりと特化して、Express の話を続けます。これを RESTful エンドポイントと呼ぶ人もいますが、率直に言うと、REST には HTTP と JSON よりも多くのものがかかわり、Roy Thomas Fielding が承認している RESTful システムの構築は、このシリーズの対象範囲を大きく超えています。

したがって、今のところは、JSON を使用するすべてのクライアントが使用する、単純な読み取り専用のエンドポイントをいくつか確立することについて説明します。

JSON での表現

通常 Web API は、データを取得する場合、次のようにかなり緩やかな構造に従います。

  • "persons" など、特定のリソースの種類に GET 要求を行うと、オブジェクトの配列である JSON の結果が生成されます。各オブジェクトには、(1 つずつ取得できるようにするために) 少なくとも一意の識別子が含まれます。また通常は、オプションの一覧で表示するのに適した簡単な説明文のようなものも含まれます。
  • URL の一部として識別子 (たとえば "persons/1234" など。1234 は、関心のある人物を一意に特定する識別子) を含む特定のリソースに GET 要求を行うと、JSON の結果が生成されます。通常この結果は、ある程度詳細にリソースを説明する単一の JSON オブジェクトになります。

Web API も PUT、POST、DELETE を使用しますが、ここでは単にデータの取得に注目することにします。

したがって、リソースの種類が "persons" の場合、2 つのエンドポイントを作成します。1 つが "/persons"、もう 1 つが "/persons/<一意識別子>" です。まずは、操作対象として、人物を格納した小さな "データベース" が必要です。姓、名、および現在の "状態" (現在何をしているか) で十分です (図 2 参照)。

図 2 人物を格納した小さなデータベースを作成

var personData = [
  {
    "id": 1,
    "firstName": "Ted",
    "lastName": "Neward",
    "status": "MEANing"
  },
  {
    "id": 2,
    "firstName": "Brian",
    "lastName": "Randell",
    "status": "TFSing"
  }
];

厳密には SQL Server ではありませんが、当面はこれで十分です。

次に、以下のように、人物の完全なコレクションのエンドポイントが必要です。

var getAllPersons = function(req, res) {
  var response = personData;
  res.send(JSON.stringify(response));
};
app.get('/persons', getAllPersons);

この場合、ルートのマッピングは単独の関数 (getAllPersons) を使用していますが、こちらの方が一般的です。というのも、この関数は、モデル - ビュー - コントローラー (MVC: Model-View-Controller) におけるコントローラーとして機能し、懸案事項の分離がより明確に保たれるためです。今のところは、JavaScript オブジェクトの配列を JSON 表現にシリアル化するために JSON.stringify を使用しますが、後ほど、もっと洗練されたものを使用する予定です。

次に、個人のオブジェクトのエンドポイントが必要になりますが、人物の識別子をパラメーターとして選択する必要があるため、やや面倒です。Express には、まさにこれを実行するための方法が備わっています。見かけ上、最も簡単と言える 1 つの方法が、ルートで指定されているパラメーターをフェッチするために要求オブジェクト (ルート マップで使用されている関数の "req" パラメーター) の "params" オブジェクトを使用するものです。しかし、Node.js では、以下のように、特定の名前付け規則のパラメーターが発見されたときに呼び出される一種のフィルターとしてパラメーター関数を使用することもできます。

app.get('/persons/:personId', getPerson);
app.param('personId', function (req, res, next, personId) {
  debug("personId found:",personId);
  var person = _.find(personData, function(it) {
    return personId == it.id;
  });
  debug("person:", person);
  req.person = person;
  next();
});

ASP.NET MVC のように、ルートが呼び出されると、"/persons" に続くもの ("/persons/1" など) はすべて "personId" という名前のパラメーターにバインドされます。ところが、":personId" のあるルートが呼び出されるときに呼び出される param 関数を使用すると、関連付けられている関数が呼び出されます。この関数は、上記のコード スニペットのように、"lodash" パッケージの関数 find を使用して、小さな personData データベースから検索を実行します。ただしこの関数はその後、他に呼び出されるもの (この場合は getPerson 関数) でも使用可能になるように、"req" オブジェクトに追加されます (JavaScript オブジェクトは常に動的に型指定されるため、簡単に実行できるのが理由です)。次のように、返すオブジェクトは既にフェッチされているので、非常に簡単に追加できます。

var getPerson = function(req, res) {
  if (req.person) {
    res.send(200, JSON.stringify(req.person));
  }
  else {
    res.send(400, { message: "Unrecognized identifier: " + identifier });
  }
};

"簡単" と言った理由がおわかりいただけたでしょうか。

まとめ

Express についてはまだテーマが残っていますが、残念ながらスペースがありません。とりあえずは、コーディングを楽しんでください。


Ted Newardは、コンサルティング サービス会社の iTrellis で CTO を務めています。これまでに 100 本を超える記事を執筆している Ted は、さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox、2010 年) もその 1 つです。F# MVP であり、世界中のカンファレンスで講演をしています。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。ブログを ted@itrellis.com (英語) に公開しています。

この記事のレビューに協力してくれた技術スタッフの Shawn Wildermuth に心より感謝いたします。