Node.js ExpressでEJS+静的ファイルWebサーバー

# 概要

Node.jsでEJS実行環境と静的ファイルをそのまま返すwebサーバーを作ってみた

# 方針

目標はApache(Nginx)+PHPのような環境をNode.js(Express)＋EJSで構築すること

- 拡張子.html .ejsはEJSテンプレートとしてレンダリングして返す
- それ以外のファイルはそのまま返す

# インストールしたパッケージ

```json
{
  "dependencies": {
    "body-parser": "^1.20.0",
    "ejs": "^3.1.8",
    "express": "^4.18.1"
  }
}
```

# 参考

以下の記事を参考というかほとんどこれをベースにカスタマイズしました

https://note.affi-sapo-sv.com/nodejs-webserver.php?utm_source=pocket_mylist#i2

拡張子htmlをejsとして実行する方法

https://stackoverflow.com/questions/21922854/change-ejs-extension-to-html-using-parse-com-express-js?utm_source=pocket_mylist

EJS内で直接モジュールをrequireできないらしいので、server.jsでModuleを追加できるようにしました

https://stackoverflow.com/questions/12794860/how-to-use-node-modules-like-momentjs-in-ejs-views

# VSCode

VSCodeでフォルダを開いたときに自動的にサーバーが開始するようにすると大変便利です。

```tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "node webserver",
            "type": "shell",
            "command": "node server.js",
            "runOptions": {
                "runOn": "folderOpen"
            }
        }
    ]
}
```

また、設定すると右クリックで該当するローカルサーバーのURLでプレビューできる拡張機能を入れるとさらに便利です。

https://marketplace.visualstudio.com/items?itemName=PrimaFuture.open-php-html-js-in-browser


# コード

```server.js
const serverlib = require('./serverlib.js');
const WebRoot = './public';
serverlib.WebRoot = WebRoot;
serverlib.RenderExt = '';

// EJSで利用するモジュールの設定
const fs = require('fs');
const path = require('path');
serverlib.Modules = { fs: fs, path: path };

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

// .htmlをEJSでレンダーさせる設定
app.set('view engine', 'ejs');
app.engine('html',require('ejs').renderFile);
 
// expressでpostデータを受け取る3行のおまじない
const bodyparser = require('body-parser')
app.use(bodyparser.urlencoded({extended: true}))
app.use(bodyparser.json())
 
app.get( serverlib.getReg , ( request, response) => serverlib.SendFile(request, response) );
app.post( serverlib.postReg , ( request, response) => serverlib.SendFile(request, response) );
 
app.listen(3000);
```

```serverlib.js
const url = require('url');
const path = require('path');
const fs = require('fs');

(() => {

    let WebRoot = '';
    let RenderExt = '';
    let Modules = {};

    const funcs = {
        indexServe: (request, response) =>  // /でアクセスした場合index.htmlを表示
            render(request, response, 'index.html'),

        defaultServe: (request, response) => // ファイルをそのまま表示
            FileSend(fileExist(request), response),

        renderHtml: (request, response) =>  // .html
            render(request, response),
    };
    // 許可する静的ファイルの拡張子
    const AllowExtents = ['.js', '.css', '.jpg', '.png', '.gif', '.mp3', '.ogg', '.avi', '.mov', '.mpeg4', '.flv'];

    // 動的な処理をするファイルの拡張子と処理内容
    const fileExtentsFunc = {
        '/': funcs.indexServe,
        '.html': funcs.renderHtml,
        '.ejs': funcs.renderHtml,
    };
    // パスをテストするための正規表現を作成 post用
    const postReg = createReg(fileExtentsFunc);

    // fileExtentsFuncにAllowExtentsを統合
    AllowExtents.forEach(e => fileExtentsFunc[e] = funcs.defaultServe);

    // パスをテストするための正規表現を作成 get用
    const getReg = createReg(fileExtentsFunc);

    function createReg(obj) {
        return new RegExp('(' +
            Object.keys(obj)         // キーを配列に変換
                .sort((a, b) => b.length - a.length) // キーを文字数が多い順にソート
                .map(e => e.replace(/\./g, '\\.'))  // . を \\.に置換
                .join('|') +        // | を区切りとして一つの文字列へ
            ')$');
    }

    const FileSendeOpt = {
        dotfiles: 'deny'
    };

    // 静的ファイルの送信
    function FileSend(filename, response) {

        if (filename === null) resError(response, 404);
        else {
            response.sendFile(path.join(__dirname, filename), FileSendeOpt,
                function (err) {
                    if (err) resError(response, 404);
                });
        }
    }

    // テンプレートファイルがあればレンダーして送信
    // なければ静的ファイルを送信
    function render(request, response) {
        //const fpath = fileExist(request, addName + RenderExt);
        const fpath = fileExist(request);
        //リクエスト用の連想配列とモジュール用の連想配列を結合
        const renderObj = Object.assign({ __FILE__: path.join(__dirname, fpath), request: request}, Modules);
        if (fpath === null) {
            resError(response, 404);
        } else {
            //レンダリング
            response.render(path.join(__dirname, fpath), renderObj);
        }
        /*
        if (fpath !== null) response.render(path.join(__dirname, fpath)
            , { get: request.query, post: request.body });
        else FileSend(fileExist(request, addName), response);
        */
    }

    // ファイルがない=null ある=パス を返す
    function fileExist(request, addExt = '') {
        const path = url.parse(request.url, true).pathname + addExt;
        if (path.includes('/..')) return null;

        const rpath = WebRoot + path;

        try {
            fs.statSync(rpath);
            return rpath;
        } catch (error) {

            return null;
        }
    }

    function resError(response, type) {
        response.sendStatus(type);
    }

    // exportsにプロパティ登録・外部に公開
    Object.defineProperties(exports, {
        getReg: {
            value: getReg,
            enumerable: true,
        },
        postReg: {
            value: postReg,
            enumerable: true,
        },
        SendFile: {
            value: (request, response) => fileExtentsFunc[request.params[0]](request, response),
            enumerable: true,
        },
        WebRoot: {
            set: (r) => WebRoot = r,
            enumerable: true,
        },
        RenderExt: {
            set: (r) => RenderExt = r,
            enumerable: true,
        },
        //EJSで使うモジュールをserver.jsで設定できるようにする。
        Modules: {
            set: (r) => Modules = r,
            enumerable: true,
        }
    });
    Object.freeze(exports);

})();
```