Express.jsのコードを読んでみたのでまとめることにしました。
普段の業務ではNode.jsもExpressも利用していないので、JavaScriptの基本的なコーディングやライブラリの内容なども合わせてまとめておこうと思います。
間違っている内容がありましたら編集リクエストをいただけますと幸いです。
Expressコードリーディング
対象のコード
今回コードリーディングする対象のコードです。
ただHello Worldするだけのコードになります。
constexpress=require('express')constapp=express()app.get('/',(req,res)=>{res.send('Hello World');})app.listen(3000);
nodeでindex.jsを起動します。
起動後curlでレスポンスを確認します。
$node index.js
$curl localhost:3000
Hello World
expressインスタンス生成
まずはconst app = express()
の処理を追っていきます。
ここで最初に呼び出されるのは、expressライブラリのルートディレクトリ上に存在するindex.jsファイルです。
module.exports=require('./lib/express');
上記はmoduleの作成のみで、処理本体はlib/express.js
ファイルです
/**
* Expose `createApplication()`.
*/exports=module.exports=createApplication;/**
* Create an express application.
*
* @return {Function}
* @api public
*/functioncreateApplication(){varapp=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 requestsapp.request=Object.create(req,{app:{configurable:true,enumerable:true,writable:true,value:app}})// expose the prototype that will get set on responsesapp.response=Object.create(res,{app:{configurable:true,enumerable:true,writable:true,value:app}})app.init();returnapp;}
一つずつ処理を見ていきます。
アプリケーション作成
まずはこちらになります。
varapp=function(req,res,next){app.handle(req,res,next);};
appのfunctionを生成していますが、こちらがExpressの入り口となるアプリケーション部分です。
HTTPリクエストをされるとここが実行されます。
プレーンなNode.jsだけでここを表現すると以下のような処理になります。
consthttp=require('http')varapp=function(req,res,next){app.handle(req,res,next);};app.handle=functionhandle(req,res,callback){res.writeHead(200,{'Content-Type':'text/plain'});res.end('Hello World\n');}http.createServer(app).listen(3000);
アプリケーションメソッドをコピーしてくる(継承する)
appはEventEmitterとprotoをコピーします。
varEventEmitter=require('events').EventEmitter;varmixin=require('merge-descriptors');varproto=require('./application');varreq=require('./request');varres=require('./response');中略mixin(app,EventEmitter.prototype,false);mixin(app,proto,false);// expose the prototype that will get set on requestsapp.request=Object.create(req,{app:{configurable:true,enumerable:true,writable:true,value:app}})// expose the prototype that will get set on responsesapp.response=Object.create(res,{app:{configurable:true,enumerable:true,writable:true,value:app}})
ここでコピーしているproto(application.js)
がapplicationのメイン処理が入っている部分です。
具体的な処理は後々出てくるのでそこで説明します。
その他、EventEmitterライブラリやExpressプロジェクトのルートディレクトリにあるrequest.js、response.jsの内容もコピーしています。
アプリケーション初期設定
app.init();
ここで読みだしているのは、lib/application.js
内に設定されているinit
メソッドです。
この中で行なっているのは、アプリケーションをデフォルトの設定で設定しています。
例えば、HTTPヘッダーにx-powered-by
を返却するなどはこの中で設定しています。
Routingの設定
次にRouting設定です。
app.get('/',(req,res)=>{res.send('Hello World');})
の処理です。
こちらで呼び出されるapp.get
はlib/application.js
ファイルの以下処理になります。
methods.forEach(function(method){app[method]=function(path){if(method==='get'&&arguments.length===1){// app.get(setting)returnthis.set(path);}this.lazyrouter();varroute=this._router.route(path);route[method].apply(route,slice.call(arguments,1));returnthis;};});
HTTPメソッドに紐づくfunctionの作成
以下の処理でHTTPメソッド(GET,POST,PUT,PATCH,DELETEなど)と同一名称のメソッドをappクラスに作成します。
varmethods=require('methods');methods.forEach(function(method){app[method]=function(path){// 中略};});
Routerクラスの移譲
次はthis.lazyrouter();
の部分です。
ここは以下の処理になります。
varRouter=require('./router');app.lazyrouter=functionlazyrouter(){if(!this._router){this._router=newRouter({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クラスの_router
プロパティにRouter
クラスを委譲しています。
Routerクラスはlib/router/index.js
ファイルを参照しています。
this._router.use
はMiddlewareの設定です。
this._router.use(query(this.get('query parser fn')));
の設定は、クエリストリングをObjectに変換してくれます。
this._router.use(middleware.init(this));
はMiddlewareの初期設定を行います。
X-Powered-Byヘッダーを付与するのもこちらになります。
Routeクラスの取得とLayerクラスをスタックに追加
続いては以下の処理です。var route = this._router.route(path);
こちらは先ほど作成した_routerのrouteメソッドを利用して、RouteクラスとLayerクラスを作成します。
実際の処理はこちらです。
varRoute=require('./route');varLayer=require('./layer');/**
* Create a new Route for the given path.
*
* Each route contains a separate middleware stack and VERB handlers.
*
* See the Route api documentation for details on adding handlers
* and middleware to routes.
*
* @param {String} path
* @return {Route}
* @public
*/proto.route=functionroute(path){varroute=newRoute(path);varlayer=newLayer(path,{sensitive:this.caseSensitive,strict:this.strict,end:true},route.dispatch.bind(route));layer.route=route;this.stack.push(layer);returnroute;};
RouteクラスとLayerクラスを生成します。
そしてLayerクラスのrouteプロパティにRouteクラスを移譲しています。
作成したLayerクラスはRouterクラスのstackに格納しています。
呼び出し元にはRouteクラスを返却しています。
RouteクラスにLayerクラスをセット
続いては以下の処理です。route[method].apply(route, slice.call(arguments, 1));
こちらは先ほど作成したRouteクラスに新しいLayerクラス作成したうえでセットします。
実際の処理はこちらです。
methods.forEach(function(method){Route.prototype[method]=function(){varhandles=flatten(slice.call(arguments));for(vari=0;i<handles.length;i++){varhandle=handles[i];if(typeofhandle!=='function'){vartype=toString.call(handle);varmsg='Route.'+method+'() requires a callback function but got a '+typethrownewError(msg);}debug('%s %o',method,this.path)varlayer=Layer('/',{},handle);layer.method=method;this.methods[method]=true;this.stack.push(layer);}returnthis;};});
ここではLayerクラスを作成してかつ、指定したメソッドをLayerクラスにセットします。
そして作成したLayerクラスをRouteクラスのstackに追加しています。
httpのlisten開始
最後に以下の構文でhttpをport3000で開始します。app.listen(3000);
実際の処理はこちらです。
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
中身については、アプリケーション作成
の部分で説明した内容と同じなので省略します。
リクエストを受け付けた時の挙動
最後にリクエストを受け付けた時の挙動についてです。
このような順番で処理されます。
app->app.handle->router.handle->Layer.handle->Route.dispatch->Layer.hanldle->Express利用者が設定したfunction
ここでRouter、Layer、Routeクラスの関係性について図にまとめてみました。
Expressで利用しているNode.jsの構文やライブラリについて
上記まででExpressの処理の流れをざっと確認してきましたが、実際にコードリーディングするにあたり、普段見慣れないNode.jsの構文やライブラリがいつかあると思います。
そこでそれぞれの構文がどういうものなのかをまとめます。
mixin
参考:merge-descriptors
まずはmixinからです。
Expressでは、以下のように利用しています。
varapp=function(req,res,next){app.handle(req,res,next);};mixin(app,EventEmitter.prototype,false);mixin(app,proto,false);
mixinがどういうものか見ていきます。
varmixin=require('merge-descriptors');varapp=function(){};varapplication=function(){}application.handle=function(){return'Hello mixin';}mixin(app,application,false);console.log(app.handle());// Hello mixin
このようにappクラスにapplicationクラスのメソッドを継承させています。
これがmixin(merge-descriptors)の利用方法になります。
EventEmitter
続いてはEventEmitterです。
EventEmitterを利用することでイベント駆動型の設計が可能になります。
varmixin=require('merge-descriptors');varEventEmitter=require('events').EventEmitter;varapp=function(){};mixin(app,EventEmitter.prototype,false);app.init=function(){app.on('mount',()=>{console.log('emit!');});console.log('init')}app.send=function(){app.emit('mount');}app.init();// initapp.send();// emit!
上記のように、on.('イベント名')
で事前に処理を受け付けるタイミングを作成してemit('イベント名')
を実行する際に、onの中に指定したfunctionが実行されます。
Object.create
続いてはObject.createについてです。
Expressでは以下のように利用されています。
// expose the prototype that will get set on requestsapp.request=Object.create(req,{app:{configurable:true,enumerable:true,writable:true,value:app}})
こちらもmixinと同じようにメソッドを継承します。
varapp=function(){};varreq=function(){};req.headers=function(){return'req!';}app.request=Object.create(req);console.log(app.request.headers());// req!
apply&call&bind
applyとcallとbindについてです。
Express.jsでは様々な場所でこのAPIが利用されています。
route[method].apply(route,slice.call(arguments,1));varargs=slice.call(arguments,1);
こちらは様々な記事で解説されているため、詳細は割愛しますが、簡単に試して見ます。
varapp=function(){}app.say=function(name,age){returnname+'は'+age+'歳です。';}console.log(app.say('Taro',16));// Taroは16歳です。console.log(app.say.apply(app,['Taro',16]));// Taroは16歳です。console.log(app.say.call(app,'Taro',16));// Taroは16歳です。varsay=app.say.bind(app,'Taro',16);console.log(say());
参考
他にもExpressの解説をされている方がいらっしゃいましたので紹介します。
エンジニアの教養、フレームワーク解剖学【Express編】