0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-16

概要

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

方針

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

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

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

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

参考

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

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

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

VSCode

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

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

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

コード

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);

})();
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?