はじめに
こんにちは、みんなのマーケットのテックチームのクイです。
今回、ExpressJS
フレームワークと弊社の利用の仕方を紹介します。
ExpressJS
とは
サーバーサイドJavaScriptのNode.jsのWebアプリケーションフレームワークである --wikipedia
ExpressJS
は軽いウェブフレームワークですが、弊社のシステムの要求に応じてカスタムしやすいです。なので選択して利用しました。
特徴
- 高速、柔軟、最小限のウェブフレームワーク。
- 様々な
npm
パケージに提供されています。
単純なフレームワークですから、ExpressJS
を利用する時色々な処理を自分で実装しなければなりません。それはExpressJS
の欠点だと思います。
NodeJSのHTTPモジュールからExpressJSのルートまで
- NodeJSのHTTPサーバーモジュールはHTTP(Hyper Text Transfer Protocol)メソッドでデータを送信し、受信するというモジュールです。HTTPの様々な機能をサポートできるように設計がされます。
- まずはExpressJSを使わないで、サーバーを書いてみます:サーバーが3000ポートで動いています。
const http = require("http"); const server = http.createServer(); const callback = function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello world!\n"); }; server.on("request", callback); server.listen(3000);
http://localhost:3000
にアクセスしたらHello world!
文字列をもらえます。
- リクエスト処理の方法:
上のコードを見たら、毎回サーバーがリクエストを受けると、callback
関数が実行されます。なのでcallback
みたいな関数はリクエスト処理が可能になります。
だが色々な処理ならどうするのか。幸いにもNodeJSのEventEmit
クラスがその問題を解決できます。EventEmit
はEventEmit.emit()
を呼び出してEventEmit.on()
の処理をアタッチするクラスです。
ExpressJS
のコードも同じです。以下はExpressJS
のコードからの抜粋です。
function createApplication() { var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false); // merge-descriptorsパケージでEventEmitterとappの記述をマージします。 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(); // protoのinit()メソッドがあるから、マージした後appも用いられるようになりました。 return app; }
app
変数はEventEmitter
の継承です。サーバーがリクエストを受ける時
function(req, res, next) { // 処理 };
みたいなハンドルで処理します。
ExpressJS
に上みたいな関数はMiddleware
と呼ばれます。
ExpressJS
のMiddleware
:
Middleware
関数は要求オブジェクト、応答オブジェクト、次のミドルウェアの関数に対する権限を持つ関数です。
Middleware
関数のできること:- ロジックを更新可能。
- 要求オブジェクト、応答オブジェクトを変更可能。
- 要求応答サイクルをストップ可能。
- 次のミドルウェアを呼び出す可能。
要求オブジェクトreqは
http.IncomingMessage
が継承され、色々な便利なメソッドが追加されています。
例えば:
header(name: string): string; accepts(...type: string[]): string | boolean; acceptsCharsets(...charset: string[]): string | boolean; acceptsEncodings(...encoding: string[]): string | boolean; param(name: string, defaultValue?: any): string; ...
応答オブジェクトnextは応答ステータスを設定できるとか、
テキストあるいはファイルなど応答もできます。
status(code: number): Response; sendFile(path: string, options: any, fn: Errback): void; render(view: string, callback?: (err: Error, html: string) => void): void; redirect(url: string, status: number): void;
ExpressJS
におけるスタックで一連のミドルウェアを保存します。next()
関数を実行する時スタック内の次のミドルウェアを呼び出します。
次の例はミドルウェアをバインドするケースです。
- アプリケーションレベルのミドルウェア:
const express = require("express"); const app = express(); app.use(function(req, res, next) { // 処理 next(); });
- エラー処理ミドルウェア:
app.use(function(err, req, res, next) { // エラー処理 });
- ルートレベルのミドルウェア:
const app = express(); const router = express.Router(); router.get("/index/", function(req, res, next) { res.render("index"); }); app.use(router);
- サードパーティのミドルウェア:
const csurf = require('csurf'); const express = require("express"); const app = express(); app.use(cookieParser());
ExpressJS
+ Typescript
以下は弊社の利用方法の一つです。
Typescript
がDecorator
でルートとかミドルウェアなどをバインドします。
まずDecorator
を作成します。
export class Router { public static get(path: string): MethodDecorator { return (target: Function, key: string, descriptor: TypedPropertyDescriptor<any>) => { this.defineMethod(Method.GET, path, descriptor.value); return descriptor; }; } private static defineMethod(method: Method, path: string, target: Object): void { let metadata: IMetadata = <IMetadata>Reflect.getMetadata(METHOD_METADATA, target); if (!metadata) { metadata = { urls: [], before: [], after: [] }; } metadata.urls.push({ path: path, method: method }); Reflect.defineMetadata(METHOD_METADATA, metadata, target); // TypescriptのReflectでメソッドの`metadata`を保存する } }
Decorator
を利用して実装する。
import { Router } from "../../vendor/router"; export class HomeController { constructor() { } @Router.get("/") @Router.get("/index") public async index(req: express.Request, res: express.Response, next: express.NextFunction): Promise<any> { return res.json({ message: "Hello world" }); } }
ルートのミドルウェアをバインドします。
export class Route { public static resolve(dir: string, router: express.Router): void { klawAsync(dir, { nodir: true }) // コントローラディレクトリのファイル一覧を探す。 .map(file => file.path) .filter(file => file.endsWith(".js")) // javascriptファイルをフィルタリングする。 .forEach(file => { const module: Object = require(file); // javascriptファイルのモジュールをインポートする。 const controllerContainer: string[] = []; Object.keys(module) // 各モジュール名を取得する .filter(m => m.endsWith("Controller")) // コントローラをフィルタリングする。 .forEach(m => { if (controllerContainer.indexOf(m) !== -1) return; // もしコントローラが登録されたら次のコントローラを処理する。 controllerContainer.push(m); const instance = new module[m](); // コントローラインスタンスを作成する。 Object.getOwnPropertyNames(Object.getPrototypeOf(instance)) // コントローラのメソッドを取得する。 .filter(method => method != 'constructor') //constructorメソッドが無視される。 .forEach(method => { const metadata = <IMetadata>Reflect.getMetadata(METHOD_METADATA, instance[method]); // メソッドの定義されたmetadataを取得する const middleware = async (req, res, next) => { // ミドルウェアを定義する。 const result = instance[method](req, res, next); if (result["then"]) { await result; } } metadata.urls.forEach(item => { const args = [item.path, ...metadata.before, middleware, ...metadata.after]; // 一連のミドルウェアを作成する router[item.method].apply(router, args); // 一連のミドルウェアを登録する。 }); }); }); }); } }
コードの参考はこちらです
最後に
今後、カスタムしたExpressJS
の構成をもっと独立性が守られるように他のテクニックを当てはまる予定です。ExpressJS
に興味があるとか素敵な技術を持っている方はぜひお待ちしてます!。
次回は、DuyさんによるReSwiftについての記事です。