このエントリは、GMSアドベントカレンダー2日目のものです。
昨日は、Hiroshi OdaさんのAWSでドキュメントサイトを最速で公開する方法。自動デプロイもあるよ!でした。
Node.jsで最も使われているフレームワーク「Express」。職場ではExpressを用いて開発しているのですが、自分にとってExpress内部が完全にブラックボックス化していました。こりゃまずい。ということで、Expressをソースコードから理解してみた際のまとめ記事です。
Expressは最小限で柔軟なアプリケーションフレームワークと言われていることもあり、他のフレームワークと比べてフレームワークの内部挙動を把握しやすいと思います。そのため、Expressを知らない、Node.jsを知らないという方でも、参考になるかと。
この記事を読破すると達成できること
- 天才が作った人気OSSアプリケーションフレームワークの内部構造を把握できる。
- Expressのソースコードを読めるようになる。
※ 本記事は、あくまでExpressの内部挙動について解説するものです。Expressの環境の整え方や便利テクニック等について言及するものではありません。
Expressとは?
【画像引用】[Expressjs.com](https://expressjs.com)まずExpressの特徴を軽く触れておきたいと思います。
Express は、Web アプリケーションとモバイル・アプリケーション向けの一連の堅固な機能を提供する最小限で柔軟な Node.js Web アプリケーション・フレームワークです。
(引用:Expressjs.com)
**最小限で柔軟なアプリケーションフレームワークがExpressです。**あとで見ていきますが、最小限と謳っているだけあって、ソースコード量も少なく、整理されていて、非常に理解しやすいです。
またExpressは、天才TJ Holowaychuk氏によって作成されたそうです。この方は他にも様々なlibraryを作ってきました。
Stop me if you’ve heard or used any of these libraries before: Express, Connect, Dox, N, Apex, git-extras, Jade, Stylus, Mocha, Superagent, EJS, Co, Koa, Commander, Should…
This is just a partial list of the high profile open source projects created by TJ. And these represent just a fraction of the amount of projects that TJ created or has been involved in.
引用:TJ Holowaychuk
Expressのファイル構成
ExpressのGithubにあるレポジトリのリンク→Express github
とりあえずExpressのファイル構成を見てみましょう。
フォルダは以下のような構成になっていることが分かります。
- benchmarks (ベンチマーク)
- examples (Expressをどうやって使うかの例文集)
- lib (Expressの内部ロジック)
- test (テスト)
Expressの内部ロジックは、libフォルダに全て詰まっています。そしてlibフォルダの中を見ると分かりますが、
libフォルダの中は、たったの11ファイル、、、!!
Expressの内部ロジックが全てこの11ファイルに詰まっているのです!
本記事では、Expressフォルダ直下にあるindex.jsと、Expressを把握する上で重要なlibフォルダ内のファイル5つを中心に、見ていきたいと思います。
以下、それぞれのファイルについての紹介を軽く書いておきました。
ちなみに画像内で、require()という関数名を用いていますが、これはNode.jsのコアモジュールとしてBuilt inされている関数になります。初めて聞いたという方は、Node.jsのAPIDocument「Modules」でrequireの挙動について詳しく解説されているので、参考にしてみてください。
Expressを理解する上で重要な2つの概念
Expressの処理の流れを理解するために、まず2つの概念を理解する必要があります。
- Middleware
- Routing
MiddlewareとRouting
とりあえずMiddlewareとRoutingについて、ざっくりと説明します。
例えばサーバーを作るとすると、以下のように考えると思います。
「ユーザーからAリクエストが来たら、A'処理を実行してレスポンスを返したい。」
「ユーザーからBリクエストが来たら、B'処理を実行してレスポンスを返したい。」
この、「ユーザーが何のリクエストをして来たら、どんな処理をしてレスポンスを返すようにしたいのか」を定義するのがRoutingです。そして、リクエストが飛んできた際のその処理は「どんな処理なのか」という内部ロジックを定義するのがMiddlewareです。
順番に詳しく見ていきましょう。
Middleware
Middlewareについてまとめた図を載せておきます。
Middlewareとは名前のごとく、「リクエスト-レスポンスサイクルの間(middle)で実行される処理」のことを指します。図が示すように、Middleware関数内でnext()を呼び出してrequestを前に繋いでいくというのが、Expressサーバーの基本スタイルです。
Middlewareのイメージを掴んでいただいたところで、とりあえず公式サイトのMiddlewareの概念説明を見てみましょう。
最後のリクエストハンドラーの前に Express ルーティング層によって呼び出される関数。そのため、未加工要求と最後の目的のルートの間に配置される。
引用:Middleware
もういっちょ。
ミドルウェア 関数は、リクエストオブジェクト (req)、レスポンスオブジェクト (res)、およびアプリケーションのリクエストレスポンスサイクルにおける次のミドルウェア関数に対するアクセス権限を持つ関数です。次のミドルウェア関数は一般的に、next という変数で表されます。
引用:Express アプリケーションで使用するミドルウェアの作成
また、Middlewareは以下の4つのタスクを実行します。
- 任意のコードを実行する。
- リクエストオブジェクトとレスポンスオブジェクトを変更する。
- リクエストレスポンスサイクルを終了する。
- スタック内の次のミドルウェアを呼び出す。
引用:Express アプリケーションで使用するミドルウェアの作成
また、Middlewareは次の5種類に分けられるそうです。以下で一応引用しておきますが、これはただの概念的なものなので特に覚える必要はありません。ただ、Middlewareの概念の整理のために有用だとは思います。
- Application level middleware
- Router level middleware
- Built-in middleware
- Error handling middleware
- Thirdparty middleware
Routing
ExpressではRoutingを理解する上で以下の3つの概念を理解する必要があります。
- Routerオブジェクト(/lib/router/index.jsで定義されている。)
- Layerオブジェクト(/lib/router/layer.jsで定義されている。)
- Routeオブジェクト(/lib/router/route.jsで定義されている。)
な、なんか難しそう。。。
とりあえずイメージを掴みやすいように図に表してみました。
それぞれ順に見ていきたいと思います。
Routerオブジェクト
上の図を参考にしながら、まずRouterオブジェクトについて見ていきましょう。
Routerオブジェクトの特徴
- Routerオブジェクトは、Appオブジェクトの_routerというプロパティに紐づく。
- Appオブジェクトと、Routerオブジェクトは1対1の関係。
- Routerオブジェクトは複数のLayerオブジェクトを挿入できるスタック構造のプロパティを持つ。
特に重要なのが、RouterオブジェクトはLayerオブジェクトのstack構造を有しているというところです。
Routerオブジェクトの初期化コード
/**
* Initialize a new `Router` with the given `options`.
*
* @param {Object} [options]
* @return {Router} which is an callable function
* @public
*/
var proto = module.exports = function(options) {
var opts = options || {};
function router(req, res, next) {
router.handle(req, res, next);
}
// mixin Router class functions
setPrototypeOf(router, proto)
router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = [];
return router;
};
router.stack = [];
というコードでrouterオブジェクトのstackプロパティに配列が指定されているのが分かるかと思います。このstackプロパティにLayerオブジェクトが挿入されていきます。
RouterオブジェクトとRouting
Routerオブジェクトと関係している部分のみを抜粋してRouting処理の流れを見てみましょう。
- Routerオブジェクトに紐づいているLayerオブジェクトのpathプロパティが、ユーザーからのリクエストのパラメータと一致しているかチェックする。
- 一致している場合は、そのLayerオブジェクトのhandlerのmiddleware関数を実行する。
- 一致していない場合は、次のLayerオブジェクトに進み、1から再実行
Routerインスタンスの作成
Expressサーバーを作成するには、このRouterオブジェクトを作成する必要があります。では、このRouterインスタンスは、どのようにして作成されるのか簡単に図に表してみました。
Routerオブジェクトの作成を担うのは、app.lazyrouter()という関数です。コードを見ておきましょう。
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};
new Router
でRouterオブジェクトを作成していますね。this._router.useの部分については後で別に見ていきたいと思います。
Layerオブジェクト
次はLayerオブジェクトですね。さっきの図をもう一度貼っておくので、今度はLayerオブジェクトに注目して見てください。
Layerオブジェクトの特徴
- Routerオブジェクトに紐づくLayerオブジェクト
- Routerオブジェクトのstackプロパティに挿入されていく。
- pathとmiddleware関数をプロパティに持つ。
- Routeオブジェクトが紐づいている場合、middleware関数にはroute.dispatch()が入る。
- Routeオブジェクトに紐づくLayerオブジェクト
- Routeオブジェクトのstackプロパティに挿入されていく。
- methodとmiddleware関数をプロパティに持つ。
Layerオブジェクトの形式
先ほど紹介した図でも示しましたが、Routerオブジェクトに紐づくLayerオブジェクトには3つの形式があり、Routeオブジェクトに紐づくLayerオブジェクトには2つの形式があります。
- Routerオブジェクトに紐づくLayerオブジェクト
- path無し + middleware関数
- path有り + middleware関数
- path有り + route.dispatch()
- Routeオブジェクトに紐づくLayerオブジェクト
- method無し + middleware関数
- method有り + middleware関数
[path無し + middleware関数]の場合、ユーザーからの全リクエストに対してmiddleware関数が実行されます。内部的には、Layerを登録する際にpath指定が無ければ、デフォルトでpath = '/';となるからです。(参考リンクとして、app.use()というRouterオブジェクトにLayerオブジェクトを挿入するメソッドの、pathがデフォルトで代入されている部分を載せておきます。:app.use())
Layerオブジェクトの初期化コード
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %o', path)
var opts = options || {};
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
// set fast path flags
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}
LayerオブジェクトとRouting
サーバーに届いたリクエストは、Routerオブジェクトに紐づいたLayerオブジェクトのpathプロパティと順に比較されていきます。そしてリクエストのpathとLayerオブジェクトのpathが合致した場合、そのLayerオブジェクトのMiddleware関数がrequestに対して実行されます。そして、また次のLayerオブジェクトに進みます。
ここで重要なのが、Middleware関数がリクエストに対して実行されていく順序は、LayerオブジェクトがRouterオブジェクト、もしくはRouteオブジェクトに挿入された順序であるということです。
要するにExpressサーバーを作成する際には、Layerオブジェクト作成の順序を気にする必要があるということです。
Layerインスタンスの作成
最後にLayerインスタンスがどのようにして作成されるのか内部挙動を追ってみましょう。
Routeオブジェクト
最後はRouteオブジェクトです。
Routeオブジェクトの特徴
- Routeオブジェクトは、Routerオブジェクトに紐づいたLayerオブジェクトと1対1の関係て紐づく。
- RouteオブジェクトはRouterオブジェクトと同様に、複数のLayerオブジェクトを挿入できるスタック構造のプロパティを持つ。
Routeインスタンスの作成
Routeオブジェクトの初期化処理
/**
* Initialize `Route` with the given `path`,
*
* @param {String} path
* @public
*/
function Route(path) {
this.path = path;
this.stack = [];
debug('new %o', path)
// route handlers for various http methods
this.methods = {};
}
Routeオブジェクトのpathプロパティに代入されているpathは、Routeオブジェクトに紐づいているLayerオブジェクトのpathと同じpathです。これは、Routeオブジェクトを作成する_router.route()メソッドを読むと分かります。
RouteオブジェクトとRouting
Routeオブジェクトに関係する部分でのRouting処理を抜粋すると以下のような流れになります。
- リクエストのpathと、Routerオブジェクトに紐づいているLayerオブジェクトに登録されているpathが一致した。
- そのLayerオブジェクトのhandleプロパティがmiddleware関数ではなくroute.dispatch()だった。
- そのLayerオブジェクトに紐づいたRouteオブジェクトへと進む
- リクエストのhttp method(GETとかPOSTとか)と、Routeオブジェクトに紐づいているLayerオブジェクトに登録されているmethodが一致した場合、そのLayerオブジェクトに登録されているmiddleware関数を実行する。
以上でRoutingの説明が終わり、Expressを理解する上で重要なMiddlewareとRoutingをあなたは理解したことになります!
ここからはExpressのGithubレポジトリで紹介されているサンプルコードを用いて、実際にどのようにしてExpressサーバーが作成されているのか見ていきたいと思います。ここまでで出てきた概念の確認も兼ねて読んでみて下さい。
Expressのサンプルコードを用いた実践解剖
Expressのgithubレポジトリ上で紹介されているサンプルコードは以下です。
const express = require('express')
const app = express()
app.get('/', function (req, res) {
res.send('Hello World')
})
app.listen(3000)
【引用】Express Github
なんというシンプルなコード。。。このコードでExpressサーバーが完成します。また、このサンプルコードは、Expressで重要な概念を全て網羅していますし、Expressの内部構造を理解するのに適しています。
では、このサンプルコードを3部に分けて内部挙動を見ていきたいと思います。
1 Expresアプリケーションの作成
const express = require('express');
const app = express();
2 Routing処理・Middleware関数の登録
app.get('/', function (req, res) {
res.send('Hello World');
});
3 Expressサーバーの作成
app.listen(3000);
1. Expresアプリケーションの作成
const express = require('express');
const app = express();
const express = require('express');
で、expressモジュールがimportされています。importで、expressフォルダの最上位層にあるindex.jsが呼ばれます。(requireの挙動については、Modulesを参考にしてみて下さい。)
ではindex.jsを見ていきましょう。
index.js
'use strict';
module.exports = require('./lib/express');
index.jsには上の2行しか含まれていません。require('./lib/express');
で、./lib/expressファイルをimportしていますね。さらに辿って、./lib/expressを見てみましょう。
./lib/express.js
ここで紹介するコードは抜粋されています。
/**
* Module dependencies.
*/
var bodyParser = require('body-parser')
var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var Route = require('./router/route');
var Router = require('./router');
var req = require('./request');
var res = require('./response');
/**
* Expose `createApplication()`.
*/
exports = module.exports = createApplication;
/**
* Create an express application.
*
* @return {Function}
* @api public
*/
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
exports = module.exports = createApplication;
で、createApplication関数がexportされています。
つまり、const express = require('express');const app = express();
の流れは以下のようになります。
- Expressアプリケーションのコンストラクタ関数であるcreateApplication関数が実行される
- createApplication関数でreturnされるapp関数がapp変数に代入される。
後で説明しますが、このExpressアプリケーション、つまりapp関数は、サーバーに届いたリクエストに対するリクエストハンドラーとして登録されます。
ちなみに、この./lib/express.jsファイルには他にも様々な記述があり、それらはExpressのAPI Documentでも説明されているので、時間があれば、どんなものがあるか見てみると良いと思います。
2. Routing処理・Middleware関数の登録
さて、前項でExpressアプリケーションの雛形の作成は完了しましたね。しかしまだアプリケーションのルーティング処理が一切実装されていません。内部的に言うとRouterオブジェクトが作成されていないということです。それはまずいので、Routing処理とMiddleware関数を追加しましょう。
それが以下のサンプルコードです。
app.get('/', function (req, res) {
res.send('Hello World');
});
app.getは./lib/application.jsにコードがあります。ちなみにapplication.jsでは、Expressのアプリケーションのプロトタイプが定義されています。サンプルコードと関係している部分を抜粋して見ていきましょう。
/**
* Application prototype.
*/
var methods = require('methods');
var app = exports = module.exports = {};
/**
* Delegate `.VERB(...)` calls to `router.VERB(...)`.
*/
methods.forEach(function(method){
app[method] = function(path){
//省略
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
var methods = require('methods');
で、methodsモジュールがimportされています。これによってmethods変数には、Node.jsがサポートするhttp_method(小文字)の配列が返ってきます。ここではhttp methodをforEachで繰り返し処理しています。
app[method]内では以下の4つの作業が行われています。
- Routerオブジェクトの作成
- Routerオブジェクトに紐づいたLayerオブジェクトの作成
- Layerオブジェクトに紐づいたRouteオブジェクトの作成
- Routeオブジェクトに紐づいたLayerオブジェクトの作成
わ、訳が分からん。。。
というわけで、図にしてみました。
ちなみにサンプルコードに即して説明すると、各Layerオブジェクトのプロパティは以下のようになります。
- Routerオブジェクトに紐づいたLayerオブジェクト
- pathプロパティの値は、'/'
- handlerは、route,dispatch()
- Routeオブジェクトに紐づいたLayerオブジェクト
- methodプロパティの値は、'get'
- handlerは、function (req, res) {res.send('Hello World');}
3. Expressサーバーの作成
ここまででRouting処理、Middleware関数の登録が行われているExpressアプリケーションが完成しています。ですが、ユーザーからのリクエストに対してExpressアプリケーションを実行するように命令する処理がまだありません。それが以下のサンプルコードで定義されています。
app.listen(3000)
このコードに関連した部分を抜粋した内部コードは以下です。
var http = require('http');
/**
* Application prototype.
*/
var app = exports = module.exports = {};
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
まず、var server = http.createServer(this);
について。
Node.jsのコアモジュールであるhttpモジュールがimportされ、http.createServer(requestListener)というメソッドがapp変数を引数として内部で実行されています。これによって、簡易サーバーが作成され、ユーザーからのリクエストに対してapp変数(これは先ほど説明しましたが、createApplication()関数のことです。)が実行されるようになります。
そして、return server.listen.apply(server, arguments);
ですね。
これもhttpモジュールで定義されているserver.listen()メソッドが使用されています。これによって、サンプルコード上で引数として渡されたポート番号3000を使用したサーバーが起動します。
つまり、まとめるとapp.listen(3000)
というコードは内部的には以下の2つの処理を行っています。
- リクエストに対してExpressアプリケーションを返すサーバーの作成
- そのサーバーの起動
Expressサーバーを作成する流れのまとめ
以上でサンプルコードを辿る作業が終わりました!
サンプルコードから分かる、Expressサーバーを作成する際の流れをまとめておきます。
- Expressアプリケーションの雛形作成
- ExpressアプリケーションにRouting処理・Middlewareの追加
- Expressサーバーの作成・起動
Expressを理解するプラスアルファ集
middlewareフォルダ
特に重要ではないのですが、libフォルダ内で一度も触れていない./lib/middlewareフォルダについて軽く見ておきたいと思います。middlewareフォルダの中には以下の2つのファイルが入っています。
- middleware
- init.js
- query.js
middlewareフォルダのファイルは何のために存在するのか
まず、その存在理由について見ていきましょう。init.jsとquery.jsはどこで使われているのか。
これらは、前に説明した、Routerオブジェクトを作成する関数「app.lazyrouter()」の中で使われています。
var middleware = require('./middleware/init');
var query = require('./middleware/query');
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};
ちなみに、app.lazyrouter()は、唯一Routerオブジェクトを作成する機能を持つメソッドでしたね。
routerオブジェクトのuseメソッド(_router.use())は、layerを作成し、作成したlayerをrouterのstackプロパティにpushするメソッドです。
つまり、app.lazyrouter()が呼び出されると、処理の流れは以下のようになります。
- Routerオブジェクトの作成
- query関数をhandlerとして持つLayerオブジェクトの作成、そして作成されたLayerオブジェクトをRouterオブジェクトのstackプロパティに挿入する
- middleware.init関数をhandlerとして持つLayerオブジェクトの作成、そして作成されたLayerオブジェクトをRouterオブジェクトのstackプロパティに挿入する
つまり、query関数とmiddleware.init関数は、Routerオブジェクトに最初に挿入される「始祖のlayer」のmiddleware関数になります。そしてそれらは、ユーザーからの全てのリクエストに対して一番最初に実行されるというわけです。
query関数が定義されているのは./middleware/query.jsであり、middleware.init関数が定義されているのは./middleware/init.jsです。
順番にそれぞれのファイルを見ていきたいと思います。
./lib/middleware/query.js
コードを見てみましょう。
var parseUrl = require('parseurl');
var qs = require('qs');
module.exports = function query(options) {
//省略
var queryparse = qs.parse;
//省略
return function query(req, res, next){
if (!req.query) {
var val = parseUrl(req).query;
req.query = queryparse(val, opts);
}
next();
};
};
next()が関数内の最後に定義されていることからも、このqueryという関数がmiddleware関数として使われていることが分かります。
このexportされたquery関数が、前に見た始祖のLayerオブジェクトのhandlerになっているわけですね。このqueryメソッドの主な処理の流れは以下のようになります。
- requestのurlをparseして、queryを取得
- 取得したqueryをオブジェクトへとparseして、requestのqueryプロパティに代入
var val = parseUrl(req).query;
では、parseUrlモジュールが使われていますね。parseUrl(req)でrequestオブジェクトのurlプロパティがparseされ、そのqueryプロパティがvalに代入されています。
そしてvar queryparse = qs.parse;req.query = queryparse(val, opts);
では、qsモジュールが使われていますね。(ちなみにこのqsモジュールもExpressの生みの親であるTJ Holowaychukが作ったモジュールだそうです。天才かよ!)
stringのqueryが入っているvalをqueryparse()でオブジェクトへとparseしています。そしてrequestのqueryプロパティに代入しています。
では何故このquery関数を全リクエストに対して実行させているのか。推測ですが、リクエストのqueryをオブジェクトとして簡単に参照できるようにするためでしょう。実際この処理のおかげで、例えば'index.js?foo=bar'といったurlに対してリクエストがきた場合、req.query.fooと書くことで簡単にbarが取り出せるようになりました。
./lib/middleware/init.js
続いてinit.jsについても見ていきましょう。コードは以下です。
exports.init = function(app){
return function expressInit(req, res, next){
if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
//省略
next();
};
};
if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
というコードから、responseのヘッダーに('X-Powered-By', 'Express')を適用していることが分かります。
このコードが中々有害で、セキュリティ的によろしくないそうです。Expressの公式サイトでもセキュリティ対策として、このコードを打ち消すコードを追加しろって言っています。(自分でソースコードに入れといて、セキュリティやばいって勧告している。。。。)
At a minimum, disable X-Powered-By header
If you don’t want to use Helmet, then at least disable the X-Powered-By header. Attackers can use this header (which is enabled by default) to detect apps running Express and then launch specifically-targeted attacks.
引用:Production Best Practices: Security
最後に
少し時間足らずで、本記事ではExpressを最後まで深掘りできませんでした。もしこの記事が好評なら続編出してみたいな。
あと誤りあれば、ご一報いただけると嬉しいです🙇♂️
明日のアドベントカレンダーの担当はkooogeさんです!
参考
Express Github
Express API Reference
Node.js http Documentation
Understanding Expressjs
Express tutorial for ExpressJS
How Node JS middleware Works?
How express.js works - Understanding the internals of the express library
Express.js under the hood