【node.js】expressで必ずやる設定などまとめ

はじめに

expressでWebアプリを開発してみて次回以降もやるであろうなという作業をまとめておく
といっても自分の趣味レベルで作ったものなので大規模にも考慮してとかは全然できていません。

とはいえせっかくやったのでできる範囲で案件で使えるようにと考えながらやりました

開発環境作成

プロジェクトの初期化と必要なモジュールをインストール

mkdir -p dev-app/public;
cd dev-app/public;
npm init;
npm install express http body-parser ejs path domain express-session config express-domain-middleware log4js mysql connect-mongo mongoose date-utils sprintf-js pm2 --save;

必要なモジュールについては各々だいぶ違ってくると思うけど自分が実際に触ってみて毎回入れるだろうなというもの

express、http ・・・Webサーバーでつかう
body-parser ・・・Postパラメータを扱うのにつかう
ejs ・・・テンプレートエンジンとしてつかう
express-session ・・・セッション管理で使う
connect-mongo mongoose ・・・セッションの保存先をmongodbとするのに使う
config ・・・定数を管理するのに使う
log4js ・・・ログを管理するのに使う
mysql ・・・DBにMySQLを使用するので使う
pm2 ・・・Webサーバーのプロセス監視に使う

必要なディレクトリをつくる

まぁ、自分ルールですが。。。

cd dev-app;
mkdir public/config public/controllers public/libs public/models public/views;
mkdir logs

最終的に以下のようなプロジェクトの構成を想定しています
開発の場合(本番はappフォルダとする)

.
├── dev-app
    ├── public
    │   ├── app.js
    │   ├── config # 設定ファイルまとめる
    │   ├── controllers # コントローラまとめる
    │   ├── libs # 独自のモジュールとかまとめる
    │   ├── models # モデルまとめる
    │   ├── views # テンプレートまとめる
    │   ├── node_modules
    │   ├── package.json
    │   ├── process.json
    │   
    └── logs
        ├── access.log
        ├── error.log
        ├── system.log

設定ファイルを作る

DBの接続先など環境によって異なる設定ファイルと全環境共通の定数などのファイルになります

config/default.json ・・・全環境共通
config/develop.json ・・・開発環境
config/production.json ・・・本番環境

config/develop.json

{
    "server": {
        "port": 3000
    }   

    "db": {
        "host": "localhost",
        "user": "dbuser",
        "password": "dbpassword",
        "database": "dbname",
        "timezone": "utc",
        "dateStrings": "date"
    },

    "mongodb": {
        "dsn": "mongodb://localhost:27017/dev-app"
    },
    
    "log": {
        "appenders": [
            {
                "category": "access",
                "type": "dateFile",
                "filename": "/path/to/dev-app/logs/access.log",
                "pattern": "-yyyy-MM-dd"
            }, {
                "category": "app",
                "type": "dateFile",
                "filename": "/path/to/dev-app/log/system.log",
                "pattern": "-yyyy-MM-dd"
            }, {
                "category": "error",
                "type": "dateFile",
                "filename": "/path/to/dev-app/dev-app07/log/error.log",
                "pattern": "-yyyy-MM-dd"
            }, {
                "type": "console"
            }
        ]
    }
}

共通処理を作る

今のところはMySQLとログ処理あたりが毎回使うだろうということでコレ

libs/db.js

var app = require("express")();
var mysql = require("mysql");
var config = require("config");
var sprintf = require("sprintf-js").sprintf;
var log = require('../libs/log.js');
var date = require('date-utils');

var express = require("express");

module.exports = {

    connection: null,

    connect: function() {
        this.connection = mysql.createConnection(config.db);
        this.connection.connect();
    },

    errorHandler: function(err) {
        if (err) {
            throw new Error(err);
        }
    },

    query: function(query, params, callback) {
        log.app(query + "\n[" + params + "]");
        this.connection.query(query, params, callback);
    },

    getOne: function(query, params, callback) {
        var self = this;
        this.query(query, params, function(err, rows) {
            self.errorHandler(err);
            callback(rows.length < 1 ? null : rows[0]);
        });
    },

    getAll: function(query, params, callback) {
        var self = this;
        this.query(query, params, function(err, rows) {
            self.errorHandler(err);
            callback(rows);
        });
    },

    insert: function(table, data, callback) {
        var self = this;
        var query = "insert into " + table + " set ?";
        this.query(query, data, function(err, result, fields) {
            self.errorHandler(err);
            callback(result.insertId);
        });
    },

    update: function(table, data, id, callback) {
        var fields = "",
            params = [];
        for (var k in data) {
            fields += k + " = ?,";
            params.push(data[k]);
        }

        fields = fields.substr(0, fields.length - 1);
        params.push(id);

        var self = this;
        var query = "update " + table + " set " + fields + " where id = ?";

        this.query(query, params, function(err, result) {
            self.errorHandler(err);
            callback(result);
        });
    }
};

libs/log.js

var log4js = require('log4js');
var config = require("config");
log4js.configure(config.log);

var logApp = log4js.getLogger("app");
var logAccess = log4js.getLogger("access");
var logError = log4js.getLogger("error");

exports.access = log4js.connectLogger(log4js.getLogger("access"), {
    level: log4js.levels.INFO
});

exports.app = function(message) {
    logApp.info(message);
};

exports.error = function(message) {
    logError.info(message);
};

メイン処理を作る

アプリ本体の実装です。
何度も使ったわけではないので違くなる可能性もあるかもしれませんが。まぁ同じようなコードになるんじゃないかと思っています
404ページやエラーページなどもここで実装。

app.js

// Node.js各種モジュール読み込み
var app = require("express")();
var http = require("http").Server(app);
var path = require("path");
var domain = require("domain");
var config = require("config");
var session = require("express-session");
var mongoose = require("mongoose");
var mongoStore = require("connect-mongo")(session);
var bodyParser = require('body-parser');

// オリジナルモジュール読み込み
var db = require("./libs/db.js");
var log = require('./libs/log.js');

// アクセスログ開始
app.use(log.access);

// テンプレートエンジン設定
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

// セッション管理
mongoose.connect(config.mongodb.dsn);
app.use(session({
    secret: "secret key",
    resave: true,
    saveUninitialized: true,
    store: new mongoStore({
        mongooseConnection: mongoose.connection
    })
}));

// DB接続
db.connect();

// POSTパラメータ取得できるように
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// 500エラー(Node.js)
app.use(function(req, res, next) {
    var reqd = domain.create();
    reqd.on("error", function(err) {
        showServerError(res, err);
    });
    reqd.run(next);
});

// URL振り分け
app.use("/", require("./controllers/index.js")());
app.use("/detail", require("./controllers/detail.js")());

// 404エラー
app.use(function(req, res, next) {
    showNotFound(res);
});

// 500エラー(ミドルウェア)
app.use(function(err, req, res, next) {
    showServerError(res, err);
});

// HTTPサーバー開始
http.listen(config.server.port, function() {
    console.log("listening on *:" + config.server.port);
});

// NotFound処理
var showNotFound = function(res) {
    res.status(404);
    res.render("404");
};

// サーバーエラー処理
var showServerError = function(res, err) {
    log.error(err);
    res.status(500);
    res.render("500");
};

Controller/Model/Viewの各ひな形

実際に機能を追加するときはこんな感じのファイルを作ってゆくイメージ。
内容はDBからデータを取得して表示するだけです。

controllers/index.js

var express = require("express");
var test = require("../models/test.js");

module.exports = function() {
    var router = express.Router();
    router.get("/", function(req, res, next) {
        test.getAll()
            .then(function(rows) {
                res.render("index", { items: rows });
            });
    });
    return router;
};

model/test.js

var db = require("../libs/db.js");
var sprintf = require("sprintf-js").sprintf;

module.exports = {
    getAll: function() {
        return new Promise(function(resolve, reject) {

            var query = sprintf(
                "select * from test"
            );

            db.getAll(query, [], function(row) {
                resolve(row);
            });
        });
    }
};

開発用と本番用の起動スクリプトを作る

開発ではwatchオプションをつけてファイルが更新されたら再起動。
本番ではファイルを同期しおわったタイミングで再起動するスクリプトで。

process.json

{
    apps: [
        {   // 開発
            name: "dev-app",
            script: "./app.js",
            watch: true,
            env: {
                "NODE_ENV": "develop"
            },
        },{ // 本番
            name: "app",
            script: "./app.js",
            env: {
                "NODE_ENV": "production"
            }
        }
    ]
}

開発起動

cd /path/to/dev-app;
./node_modules/pm2/bin/pm2 start process.json --only dev-app;

本番起動

cd /path/to/app;
./node_modules/pm2/bin/pm2 start process.json --only app

pm2にはデプロイ機能とかもあるみたいだけど今のところは
rsyncなりgitのリポジトリからPULLするなりで大丈夫かな。。。

ちゃんと考えろと言われそうだけど。。。

さいごに

冗長化とかまだまだ考えなくてはならないことがとりあえず基本的な部分はこんな感じかな

長い間運用してみないと気づきずらいこととかはまだわからないけど。。。

とりあえず以上です