くらしのマーケット開発ブログ

くらしのマーケットを開発する、みんなのマーケットによる技術ブログ

ExpressJSフレームワークの紹介

はじめに

こんにちは、みんなのマーケットのテックチームのクイです。
今回、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クラスがその問題を解決できます。EventEmitEventEmit.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と呼ばれます。

  • ExpressJSMiddleware:
    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();
});

アプリケーションレベルのAPI

  • エラー処理ミドルウェア:
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);

ルートのAPI

  • サードパーティのミドルウェア:
const csurf = require('csurf');
const express = require("express");
const app = express();
app.use(cookieParser());

ExpressJS + Typescript

以下は弊社の利用方法の一つです。
TypescriptDecoratorでルートとかミドルウェアなどをバインドします。
まず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についての記事です。