TENTIAL ウェルネス事業部リードエンジニアの湧川です。
弊社事業部では、一貫してjavascriptでコードを書いており、フロントエンドにはVue, nuxtjs。バックエンドにはexpressをAPIサーバーとして開発しています。
そんな中、expressとはどのようなフレームワークで、コードの中身はどうなっているのか気になりました。
Railsくらいにいい意味で枯れている、Nodejsで最も使用されているといっても過言でないExpress.jsの深淵なるコードを探して中身を見ていこうと思います。
tl;dr
expressの各種メソッドがどうやって定義されて、どんなコードを実行しているかを追う。
プログラムについて
Initialize
まずは宣言のコード。 expressクラスをrequireしてそれを実行することでappを取り出しています。
// Initialize
const express = require('express')
const app = express()
app.get('/', function (req, res) {
res.send('Hello World')
})
app.listen(3000)
index.jsはlib/e/expressを読み出しています。
// index.js
'use strict';
module.exports = require('./lib/express');
そのlib/expressですが、Module dependencisで各種APIを読み出してますね。
// 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');
...
(おまけ)mixin
mixinは以下のライブラリで、プロトタイプの深いメソッドまでをマージするためのライブラリです。(使われている人の割にスターが少なすぎたので、偽物かなと一瞬思ったのは余談です) https://github.com/component/merge-descriptors
(おまけ)EventEmitter
Nodejsを触る上では絶対に理解しないといけない神器。
イベント処理を施すためのライブラリです。イベントリスナーをつくってイベント待ちをしたり、emitでイベント発火をしたりするライブラリです。
https://github.com/browserify/events
createApplication
Initializeのexpress()はexportsしたcreateApplicationを実行しているようです。
// lib/express.js
/**
* 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;
}
createApplicationでやっていることは
- app()の定義(lib/application - app.handleを実行)
- protoとEventEmitterのprototypeのmixin
- appにrequestとreqを生やす
- initの実行
- appを渡す
の5つのフローです。 それではそれぞれ何を実行しているか見ていきます。
1. app()の定義
その前にappは何かというと、2でmixinされたprotoです。protoはModule dependenciesで取り込まれているlib/applicationです。
`lib/application.js`` は、appのprototypeをつくるためにObjectを宣言していますね。
その後、handleのようなprototype methodをどんどん定義しています。
// lib/application.js
...
/**
* Dispatch a req, res pair into the application. Starts pipeline processing.
*
* If no callback is provided, then default error handlers will respond
* in the event of an error bubbling through the stack.
*
* @private
*/
app.handle = function handle(req, res, callback) {
var router = this._router;
// final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);
};
app.handleがやっていることは
- nextにある関数がなければ、最後に実行する関数
finalhandlerを実行すること。 - routerの確認
- あればrouterのhandle実行
- なければないとdebugエラー吐く
の主に2つになります。
(おまけ)finalHandler
finalHandlerは、公式に「Node.js function to invoke as the final step to respond to HTTP request.」と書いてあるとおり、何かしらのエラーでexpressの処理がコケたときに最終的に実行される400・500系が実際に実行される関数です。
https://github.com/pillarjs/finalhandler#readme
2. protoとEventEmitterのprototypeのmixin
前述に記載したmixinライブラリを使用して、appオブジェクトにlib/applicationとeventEmitterをマージし、1のapp.handleなどがcreateApplication関数内で実行できるようになります。
3. appにrequestとreqを生やす
// 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 }
})
Module dependenciesで呼び出したreq,resをappのrequest,responseとして、オブジェクト生成して代入していますね。
なぜにreq,resをそのまま入れないかというと、そのまま入れた場合はapp.responseはresの参照になるため、req,resに変更があった場合はapp.response等にも影響が出てしまいます。
app.response = Object.create(res)
以上でも参照代入になってしまうので、実体をいれるべく、lodashのcloneDeepみたく
{ configurable: true, enumerable: true, writable: true, value: app }
とオプションを設定しています。
これで晴れてappにreq,resが定義できました。
4. app.init()
/**
* Initialize the server.
*
* - setup default configuration
* - setup default middleware
* - setup route reflection methods
*
* @private
*/
/**
* Application prototype.
*/
var app = exports = module.exports = {};
...
app.init = function init() {
this.cache = {};
this.engines = {};
this.settings = {};
this.defaultConfiguration();
};
次にapp.initですが、こちらはlib/applicationのinitと見れば一目瞭然です。
cacheやengines, settingsなどを空オブジェクトに初期化しているのみです。
this.defaultConfiguration()は、各種thisの環境設定値を決めています。
Object.defineProperty以降もコードが並んでいますが今回の記事では割愛します。続きは以下URLです。
app.defaultConfiguration = function defaultConfiguration() {
var env = process.env.NODE_ENV || 'development';
// default settings
this.enable('x-powered-by');
this.set('etag', 'weak');
this.set('env', env);
this.set('query parser', 'extended');
this.set('subdomain offset', 2);
this.set('trust proxy', false);
// trust proxy inherit back-compat
Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
configurable: true,
value: true
});
...
}
5. appを渡す
1~4まで実行したあとに、最後にはcreateApplicationの返り値としてappを渡します。appとはfunctionであり、中身はapp.handleを実行していました。
であれば以下Initializeに戻って、以下コードのexpress()部分は
app.handle()を実行してappを返しているというわけですね。
何気なく書いていたapp
const express = require('express')
const app = express()
app.get('/', function (req, res) {
res.send('Hello World')
})
app.listen(3000)
app.handleの最後の実行はrouter.handle()なので、createApplicationで色々設定しappが出来た後に、結局はrouterを実行している手順になっています。
長くなったので、router.handleは次回記事で説明しようかなと思います!
最後に
今やhapi, Koa, Fastify, nest.jsなど群雄割拠になってしまったNodejsフレームワーク群で枯れてしまったexpressですが、枯れてしまったということはそれほど安定して使われてきたということだとも思っています。
ecmascriptもes5で書かれているし、Typescriptでもないですが、昔のNodejsやjavascriptはこう書かれていたんだ。という歴史を振り返ることも時には悪くはないと思いました。
なんならプロトタイプや継承、参照について復習することができましたし、これらの概念はjs使いには一生逃れられない代物です。
そんな一生JS使いとしてエンジニア極めたいぜ!という方はぜひTENTIALへ!