💨

Express.js徹底解説

2025/01/04に公開

ExpressはNode.jsで極めて一般的に使用されるウェブサーバーアプリケーションフレームワークです。基本的に、フレームワークとは特定のルールに則ったコード構造であり、2つの重要な特性を持っています:

  • それはAPIをカプセル化し、開発者がよりビジネスコードの記述に集中できるようにします。
  • それは既に定義されたプロセスと標準仕様を持っています。

Expressフレームワークの主要機能は以下の通りです:

  • 様々なHTTPリクエストに応答するためのミドルウェアを設定できます。
  • 異なる種類のHTTPリクエストアクションを実行するためのルートテーブルを定義します。
  • テンプレートにパラメータを渡してHTMLページの動的レンダリングを実現することをサポートします。

この記事では、簡単なLikeExpressクラスを実装することにより、Expressがどのようにミドルウェア登録、nextメカニズム、およびルート処理を実装するかを分析します。

Expressの分析

まず、2つのExpressコード例を通じてそれが提供する機能を見てみましょう:

Express公式サイトのHello World例

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`);
});

エントリファイルapp.jsの分析

express-generatorスキャフォールディングで生成されたExpressプロジェクトのエントリファイルapp.jsのコードは以下の通りです:

// マッチしないルートによるエラーを処理する
const createError = require('http-errors');
const express = require('express');
const path = require('path');

const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');

// `app`はExpressのインスタンス
const app = express();

// ビューエンジンの設定
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// postリクエスト内のJSON形式データを解析し、`req`オブジェクトに`body`フィールドを追加する
app.use(express.json());
// postリクエスト内のurlencoded形式データを解析し、`req`オブジェクトに`body`フィールドを追加する
app.use(express.urlencoded({ extended: false }));

// 静的ファイルの処理
app.use(express.static(path.join(__dirname, 'public')));

// トップレベルのルートを登録する
app.use('/', indexRouter);
app.use('/users', usersRouter);

// 404エラーをキャッチしてエラーハンドラに転送する
app.use((req, res, next) => {
    next(createError(404));
});

// エラーハンドリング
app.use((err, req, res, next) => {
    // 開発環境でエラーメッセージを表示するためのローカル変数を設定する
    res.locals.message = err.message;
    // 環境変数に応じて完全なエラーを表示するか決定する。開発時は表示、本番時は非表示。
    res.locals.error = req.app.get('env') === 'development'? err : {};
    // エラーページをレンダリングする
    res.status(err.status || 500);
    res.render('error');
});

module.exports = app;

上記の2つのコードセグメントから、Expressインスタンスappには主に3つのコアメソッドがあることがわかります:

  1. app.use([path,] callback [, callback...]):ミドルウェアを登録するために使用されます。リクエストパスが設定されたルールと一致する場合、対応するミドルウェア関数が実行されます。
    • path:ミドルウェア関数を呼び出すためのパスを指定します。
    • callback:コールバック関数は様々な形式を取ることができます。単一のミドルウェア関数、カンマで区切られた一連のミドルウェア関数、ミドルウェア関数の配列、または上記すべての組み合わせです。
  2. app.get()app.post():これらのメソッドはuse()と似ており、ミドルウェアの登録にも使用されます。ただし、それらはHTTPリクエストメソッドにバインドされています。対応するHTTPリクエストメソッドが使用された場合のみ、関連するミドルウェアの登録がトリガーされます。
  3. app.listen():httpServerを作成し、server.listen()に必要なパラメータを渡す責任があります。

コードの実装

Expressコードの機能分析に基づき、Expressの実装は主に3点に焦点が当てられています:

  • ミドルウェア関数の登録プロセス。
  • ミドルウェア関数内のコアのnextメカニズム。
  • ルート処理であり、特にパスマッチングに重点が置かれています。

これらの点に基づき、以下で簡単なLikeExpressクラスを実装します。

1. クラスの基本構造

まず、このクラスが実装する必要がある主要なメソッドを明確にしましょう:

  • use():一般的なミドルウェア登録を実装します。
  • get()post():HTTPリクエストに関連するミドルウェア登録を実装します。
  • listen():基本的にはhttpServerのlisten()関数です。このクラスのlisten()関数では、httpServerを作成し、パラメータを渡し、リクエストをリッスンし、コールバック関数(req, res) => {}を実行します。

ネイティブのNode httpServerの使い方を見てみましょう:

const http = require("http");
const server = http.createServer((req, res) => {
    res.end("hello");
});
server.listen(3003, "127.00.1", () => {
    console.log("node service started successfully");
});

それに応じて、LikeExpressクラスの基本構造は以下の通りです:

const http = require('http');

class LikeExpress {
    constructor() {}

    use() {}

    get() {}

    post() {}

    // httpServerのコールバック関数
    callback() {
        return (req, res) => {
            res.json = function (data) {
                res.setHeader('content-type', 'application/json');
                res.end(JSON.stringify(data));
            };
        };
    }

    listen(...args) {
        const server = http.createServer(this.callback());
        server.listen(...args);
    }
}

module.exports = () => {
    return new LikeExpress();
};

2. ミドルウェアの登録

app.use([path,] callback [, callback...])からわかるように、ミドルウェアは関数の配列または単一の関数であることができます。実装を簡素化するため、ミドルウェアを一律に関数の配列として処理します。LikeExpressクラスでは、use()get()post()の3つのメソッドがすべてミドルウェア登録を実装できます。ただし、トリガーされるミドルウェアはリクエストメソッドの違いにより異なります。そこで、以下を考慮します:

  • 一般的なミドルウェア登録関数を抽象化する。
  • これら3つのメソッド用のミドルウェア関数の配列を作成し、異なるリクエストに対応するミドルウェアを格納する。use()はすべてのリクエスト用の一般的なミドルウェア登録メソッドであるため、use()ミドルウェアを格納する配列はget()post()の配列の合併です。

ミドルウェアキュー配列

ミドルウェア配列はクラス内のメソッドがアクセスしやすいようにパブリック領域に配置する必要があります。そこで、ミドルウェア配列をconstructor()コンストラクタ関数内に配置します。

constructor() {
    // 格納されるミドルウェアのリスト
    this.routes = {
        all: [], // 一般的なミドルウェア
        get: [], // getリクエスト用のミドルウェア
        post: [], // postリクエスト用のミドルウェア
    };
}

ミドルウェア登録関数

ミドルウェア登録とは、対応するミドルウェア配列にミドルウェアを格納することです。ミドルウェア登録関数は入力されたパラメータを解析する必要があります。最初のパラメータはルートまたはミドルウェアである可能性があるため、まずそれがルートかどうかを判断する必要があります。ルートであれば、そのまま出力します;そうでなければ、デフォルトはルートルートとし、残りのミドルウェアパラメータを配列に変換します。

register(path) {
    const info = {};
    // 最初のパラメートがルートの場合
    if (typeof path === "string") {
        info.path = path;
        // 2番目のパラメートから配列に変換し、ミドルウェア配列に格納する
        info.stack = Array.prototype.slice.call(arguments, 1);
    } else {
        // 最初のパラメートがルートでない場合、デフォルトはルートルートで、すべてのルートが実行される
        info.path = '/';
        info.stack = Array.prototype.slice.call(arguments, 0);
    }
    return info;
}

use()get()post()の実装

一般的なミドルウェア登録関数register()があれば、use()get()post()を簡単に実装できます。ただし、ミドルウェアを対応する配列に格納するだけです。

use() {
    const info = this.register.apply(this, arguments);
    this.routes.all.push(info);
}

get() {
    const info = this.register.apply(this, arguments);
    this.routes.get.push(info);
}

post() {
    const info = this.register.apply(this, arguments);
    this.routes.post.push(info);
}

3. ルートマッチング処理

登録関数の最初のパラメートがルートの場合、リクエストパスがルートまたはそのサブルートと一致する場合にのみ、対応するミドルウェア関数がトリガーされます。そこで、リクエストメソッドとリクエストパスに応じてマッチするルートのミドルウェア配列を抽出するルートマッチング関数が必要です。その後、callback()関数が実行できるようにします:

match(method, url) {
    let stack = [];
    // ブラウザの組み込みアイコンリクエストを無視する
    if (url === "/favicon") {
        return stack;
    }

    // ルートを取得する
    let curRoutes = [];
    curRoutes = curRoutes.concat(this.routes.all);
    curRoutes = curRoutes.concat(this.routes[method]);
    curRoutes.forEach((route) => {
        if (url.indexOf(route.path) === 0) {
            stack = stack.concat(route.stack);
        }
    });
    return stack;
}

次に、httpServerのコールバック関数callback()で、実行するミドルウェアを抽出します:

callback() {
    return (req, res) => {
        res.json = function (data) {
            res.setHeader('content-type', 'application/json');
            res.end(JSON.stringify(data));
        };
        const url = req.url;
        const method = req.method.toLowerCase();
        const resultList = this.match(method, url);
        this.handle(req, res, resultList);
    };
}

4. nextメカニズムの実装

Expressミドルウェア関数のパラメータはreqresnextであり、nextは関数です。これを呼び出すことで、ミドルウェア関数が順番に実行されます。これはES6 Generatorのnext()と似ています。私たちの実装では、次の要件を満たすnext()関数を書く必要があります:

  • ミドルウェアキュー配列から順番に1つのミドルウェアを抽出する。
  • 抽出したミドルウェアにnext()関数を渡す。ミドルウェア配列はパブリックなので、next()が実行されるたびに、配列内の最初のミドルウェア関数が取り出されて実行され、それによりミドルウェアが順番に実行される効果が得られます。
// コアのnextメカニズム
handle(req, res, stack) {
    const next = () => {
        const middleware = stack.shift();
        if (middleware) {
            middleware(req, res, next);
        }
    };
    next();
}

Express Code

const http = require('http');
const slice = Array.prototype.slice;

class LikeExpress {
    constructor() {
        // 格納されるミドルウェアのリスト
        this.routes = {
            all: [],
            get: [],
            post: [],
        };
    }

    register(path) {
        const info = {};
        // 最初のパラメータがルートの場合
        if (typeof path === "string") {
            info.path = path;
            // 2番目のパラメートから配列に変換し、スタックに格納する
            info.stack = slice.call(arguments, 1);
        } else {
            // 最初のパラメータがルートでない場合、デフォルトはルートルートで、すべてのルートが実行される
            info.path = '/';
            info.stack = slice.call(arguments, 0);
        }
        return info;
    }

    use() {
        const info = this.register.apply(this, arguments);
        this.routes.all.push(info);
    }

    get() {
        const info = this.register.apply(this, arguments);
        this.routes.get.push(info);
    }

    post() {
        const info = this.register.apply(this, arguments);
        this.routes.post.push(info);
    }

    match(method, url) {
        let stack = [];
        // ブラウザの組み込みアイコンリクエスト
        if (url === "/favicon") {
            return stack;
        }

        // ルートを取得する
        let curRoutes = [];
        curRoutes = curRoutes.concat(this.routes.all);
        curRoutes = curRoutes.concat(this.routes[method]);
        curRoutes.forEach((route) => {
            if (url.indexOf(route.path) === 0) {
                stack = stack.concat(route.stack);
            }
        });
        return stack;
    }

    // コアのnextメカニズム
    handle(req, res, stack) {
        const next = () => {
            const middleware = stack.shift();
            if (middleware) {
                middleware(req, res, next);
            }
        };
        next();
    }

    callback() {
        return (req, res) => {
            res.json = function (data) {
                res.setHeader('content-type', 'application/json');
                res.end(JSON.stringify(data));
            };
            const url = req.url;
            const method = req.method.toLowerCase();
            const resultList = this.match(method, url);
            this.handle(req, res, resultList);
        };
    }

    listen(...args) {
        const server = http.createServer(this.callback());
        server.listen(...args);
    }
}

module.exports = () => {
    return new LikeExpress();
};

Leapcell: ウェブホスティング、非同期タスク、Redis向け次世代サーバーレスプラットフォーム

最後に、Expressをデプロイするのに非常に適したプラットフォームを紹介しましょう。それはLeapcellです。

Leapcellは以下の特徴を持つサーバーレスプラットフォームです:

1. 多言語サポート

  • JavaScript、Python、Go、またはRustで開発できます。

2. 無制限のプロジェクトを無料でデプロイ

  • 使用量に応じて支払います。リクエストがなければ、料金はかかりません。

3. 比類なきコスト効率

  • 使った分だけ支払い、アイドル時の料金はかかりません。
  • 例えば、25ドルで平均応答時間60msで694万件のリクエストをサポートします。

4. 簡素化された開発者体験

  • 直感的なUIで簡単にセットアップできます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • アクション可能な洞察を得るためのリアルタイム指標とロギング。

5. 簡単なスケーラビリティと高性能

  • 自動スケーリングで高い並列性を簡単に処理できます。
  • オペレーションオーバーヘッドはゼロ。開発に集中できます。

ドキュメントでもっと見る!

Leapcell Twitter: https://x.com/LeapcellHQ

Discussion