Expressを利用したアプリはその構成要素をサーバーサイドの処理、3rd Partyのライブラリ群、UIを構成するテンプレート、静的ファイルなど幾つかの種類にコンポーネントを分類できます。
これらを見通しよく、容易に拡張できるような構成にするためには整頓されたディレクトリ構成を組むことが重要で、多くの人がこのベストプラクティスについて思い悩んでいるのではないかと思います。今回はこのディレクトリ構成のベストプラクティス、というより、どのようなパターンでも共通する基本の考え方についてまとめてみようと思います。
ディレクトリ構成
様々な議論があるところですが、共通して言えるのは自分で一から考えるよりも、なんらかのジェネレーターが生成する構成をベースにすべきというところでしょう。こういったジェネレーターは先人の経験をもとにExpressアプリに必要とされるコンポーネントの雛形をコマンド一つで生成してくれます。
ただ実際にはこういったベストプラクティスは入門者にとっては少しばかり圧倒される内容かもしれません。なぜこのように分ける必要があるのかを理解するにはまずapp.jsの肥大化などアンチパターンで課題を感じる必要があるからです。なのでまずは小振りなアプリを思うままに開発してみて、その後にジェネレーターに戻ってくると理解が進むのでは、と個人的には思います。
もっとも基本的なジェネレーターはExpressの開発元が提供するexpress-generatorだと思います。express-generatorは従来はExpress本体にバンドルされていたようですが、現在は別途インストールする必要があります。
$ npm install -g express-generator
インストールが完了するとexpressコマンドが利用できるようになります。
expressコマンドの基本的な書式は下記の通りです。私はView EngineにEJSを使うので-eオプションによってEJSを指定しています。
$ express -e YOUR_PROJECT
$ cd YOUR_PROJECT/
$ ls
app.js bin package.json public routes views
上記のようにプロジェクトディレクトリの配下にいくつかのファイルとディレクトリが作成されます。それぞれの役割は下記の通りです。
- app.js: このアプリのメインファイル。アプリ全体・すべてのリクエストに共通する設定・処理を記述します。
- bin: サーバープロセスの起動処理などを格納するディレクトリ。デフォルトではwwwファイルが生成されており、3000番ポートでサーバープロセスを起動するようになっています。
- package.json: このプロジェクトの名前、バージョン、依存関係などを定義する設定ファイル。
- public: 静的リソース(Javascript, CSS, イメージファイルなど)を格納するディレクトリ。
- routes: ルーティング処理を格納するディレクトリ。ルーティングとはクライアントが要求するURIに応じてどのような処理を実行するかを返すかを振り分ける処理のことです。
- views: UIを格納するディレクトリ。コンテンツは選択したテンプレートエンジンに対応したフォーマットで記述していきます。EJSであればindex.ejsのように拡張子を設定します。
この中で私がExpressの肝だと思うのがapp.jsとroutesの使い分けです。routesはルーティングを格納するためのディレクトリとして位置付けられていますが、ごく小規模なものであればapp.jsにすべて記述することも不可能ではありません。
どういった基準でroutes配下のファイルを作成していくのか、app.jsに記述すべきもの、routes配下に記述すべきものはそれぞれ何か? この問いに答えるにはExpressの根幹となるルーティングとミドルウェアについて理解しておく必要があると思います。
ミドルウェアって何?
ミドルウェアはExpressアプリを構成する最も大きな要素でありながら、その名前も手伝って何なのかがわかりにくいコンポーネントだと思います。
まず最初に理解しておきたいのは、 Expressアプリは「ルーティング」と「処理」をつなげていくことで構成される ということです。
ここでいうルーティングとはクライアントからのリクエストを受けたとき、URIとHTTPメソッドの組み合わせによってどの処理をおこなうかを振り分けることです。
ルーティングには例えば下記のようなパターンがあります。
- GET "/" を受けたら処理Aを実行。
- POST "/" を受けたら処理Bを実行。
- GET "/api/photo/list" を受けたら処理Cを実行。
- どんなリクエストでも受けたら処理Dを実行。
こういったルーティングを簡単におこなえることがExpressの醍醐味の一つと言えます。
そして、 上記の処理A, 処理B, 処理C, 処理Dというのがすべてミドルウェアに該当します。
ミドルウェアの中身はテンプレートを出力したり、データベースで検索をおこなってその結果をJSON形式で返したり、どんな処理も盛り込めます。そして一様にクライアントからのリクエストをあらわすrequestオブジェクトと、サーバーからのレスポンスをあらわすresponseオブジェクトが利用できます。
ユーザーは自分でミドルウェアを作成してどのリクエストに適用するかをルーティングを使って決めることができます。または3rd Partyのミドルウェアを適用することもできます。
ちなみに公式ドキュメントではリクエストのURIのことは「マウントポイント」または「マウントパス」と呼ばれ、そのリクエストにミドルウェアを適用することは「マウントする」と表現されています。
例えば下記のコードではfunction以下の無名関数がミドルウェアで、"/api/photo/list"というマウントポイントにそのミドルウェアをマウントしている(適用している)ことになります。つまり"/api/photo/list"にGETリクエストがあった場合、photoListをJSONフォーマットで返す、という処理になります。
app.get("/api/photo/list", function(req, res, next){
res.json(photoList);
});
あるいは下記のコードではexpress-sessionというセッション管理のための3rd Partyミドルウェアをすべてのリクエストに対して適用しています。こちらはすべてのリクエストに対して指定したオプションを元にセッション張る、という処理になります。
var session = require("express-session");
app.use(session({
secret: "YOUR_SECRET",
resave: false,
saveUninitialized: false,
cookie: {maxAge: 172800000}
}));
ちなみにapp.use()はすべてのHTTPメソッドにおいてミドルウェアを適用するためのメソッドです。逆にapp.get()はGETリクエストにのみミドルウェアを適用します。
以上を踏まえ、構成としては下記2点のことが言えるのではないかと思います。
- メイン処理を記述するapp.jsにはすべてのリクエストに共通するミドルウェアの適用をおこなう。
- routesディレクトの配下にはリクエストによって異なるミドルウェア(処理)を適用すべきものを記述する。
つまり前者のミドルウェアについてはURIが"/api/photo/list"と細分化されているため、routes配下のファイルに記述するのが適切。後者のミドルウェアはすべてのリクエストに提供すべき設定なのでメインファイルのapp.jsに記述するのが適切、となります。
まとめ
ディレクトリ構成とは言いつつも、今回はあくまでもルーティングとミドルウェアについて重きを置いてまとめてみました。他にもフロントエンド用パッケージの管理方法など考慮事項はありますが、これがExpressアプリで理解しておくべき構成の大筋だと思っています。
ルーティング、ミドルウェアの基本的なコンセプトを理解した上でexpress-generatorが生成する構成をカスタマイズしたり、はたまたyeomanのようなジェネレーターに手を伸ばしてみたりすれば、アンチパターンに陥らずに自分なりのベストプラクティスを見出す一助になるのではないかと思っています。