LoginSignup
7
4

More than 5 years have passed since last update.

Node.jsとExpress.jsを使ってみた

Last updated at Posted at 2017-08-28

NodeとExpressと、ついでにMongoDBを使ってみました。サンプルとして、「保通協の型式試験の実施状況(PDF)をHTMLで表示する」というのを作ってみることにします。あまりに多くアクセスするのはよくないので、一度アクセスしたらDBへ保存し、前回のアクセスから30分経っていない場合は、DBから取得するようになっています。

準備

$ npm install -g express-generator
$ express --view=pug somerset

とりあえず、express-generatorのサンプル通りです。

実装

以下、試行錯誤の結果だけを残しておきます。

エントリポイント

とりあえずエントリポイントはapp.jsでした。

app.js
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var index = require('./routes/index');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {message:'データが見つかりません'};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

最初のexpress-generatorで作成されるサンプルは削除しています。また、app.jsは全体の設定ファイルの役割を持っているような印象でしたので、実装自体はrouter/index.jsにまとめることにしました。(もしかしたら作法とは違う・・?)

Router

エントリポイントであるapp.jsから、パスを辿る経路の処理をRouterと呼んでいるようです。

優先順位

リクエストされたパスを正規表現で検索し、マッチした場合のみ、その処理を行います。検索はファイルの上から行われ、順に処理されます。つまり、優先順位の高いものがある場合は、上に記述する必要があります。

routes/index.js
  .. 省略 ..

router.get('/setup', function(req, res, next) {
    if ('production' == process.env.NODE_ENV) {
        res.redirect(302, './');
    } else {
        co(function* () {
            yield db.init();
            const data = yield db.setup();
            yield db.fin();

            res.render('index', {
                title : '型式試験の実施状況(初期設定)',
                update: moment(data[0][0].update).format('YYYY年MM月DD日 HH時mm分'),
                list  : data[1],
                id    : [],
            });
        })
        .catch(console.error);
    }
});

  .. 省略 ..

上記の処理は、/よりも優先して実行したかったので、その処理よりも上に書いています。routes/index.jsで、/setupを一番上においていることを確認できます。

環境変数

Nodeでの環境変数はprocess.envを参照します。

routes/index.js
const dotenv    = require('dotenv');
dotenv.load();
const node_env  = process.env.NODE_ENV;

上記のコードのように、developmentproduction環境で、環境変数を変える方法として、.envファイルを作成し、読み込む方法採用しました。ただ、evennodeではWebコンパネ上から環境変数が設定できるので、.envは作成していません。(ローカルデバッグでのみ使っています)

リクエストクエリ

リクエストクエリは、routerのcallback関数の引数にセットされます。具体的にはapp.use('/', function(req, res, next){...});と設定した場合、req.queryにセットされます。

routes/index.js
const query_id = (req.query.id?req.query.id.split(','):[]);

/?id=12345,678901のようなクエリを,で分割します。クエリがない場合、req.query.idundefined(falsy)になるため、空配列を返すようにしています。

routes/index.js
    if (req.query.format && 'json' === req.query.format.toLowerCase()) {    
    }

こちらもreq.query.formatundefined(falsy)でない場合、jsonであるかをチェックしています。(大文字小文字は判定しないので、無条件に小文字に変換しています)

レスポンス(JSON)

res.header('Content-Type', 'application/json; charset=utf-8');
res.json({ list:jsonData });

JSONを返す際にはヘッダーを適切に設定することと、charset=utf-8をつけた方が良いようです。(ブラウザなどが誤爆しないように・・)

レスポンス(PUGでレンダリング)

routes/index.js
res.render('index', {
    title : req.html.title,
    update: req.html.update,
    list  : req.html.list,
    id    : query_id,
});

引数であるindexviews/index.pugを参照します。続くデータはPUG内に引き渡されます。

GET/POSTを用いた例

router.get('/check', function(req, res, next) {
    res.render('check', {});
});
router.post('/check', function(req, res, next) {

    .. 省略 ..

    try {
        data = JSON.parse(req.body.checkData);
        data = (data.list?data.list:[]);

        .. 省略 ..

        res.render('checkresult', {title:'チェック結果', list:ret});
    } catch (err) {
        res.render('check', {data:req.body.checkData, error:err});
    }
});

GETでアクセスした場合はフォームを表示、POSTでデータ送信した場合はDBと送信したデータをチェック、エラーであれば送信したデータを初期値としてフォームを表示します。

データベース(MongoDB)アクセス

init→(DBの操作)→finという流れにしたかったので、module.exportsにハンドラとメソッド群をまとめました。

routes/db.js
'use strict';

const co     = require('co');
const moment = require('moment');
const pdf    = require('./pdf');

const mongodb= require('mongodb');
const client = mongodb.MongoClient;

const dotenv = require('dotenv');
dotenv.load();
const node_env      = process.env.NODE_ENV;
const mongo_url     = process.env.MONGO_URL;
const mongo_working = process.env.MONGO_COLLECTION_WORKING;
const mongo_finished= process.env.MONGO_COLLECTION_FINISHED;

let _conn = null;
module.exports = {

    // create collections
    create: co.wrap(function* () {
        yield [
            _conn.createCollection(mongo_working),
            _conn.createCollection(mongo_finished),
        ];
    }),
    // delete a document
    delete: co.wrap(function* () {
    }),
    // disconnect to MongoDB
    fin: co.wrap(function* () {
        yield _conn.close();
        _conn = null;
        return _conn;
    }),
    // connect to MongoDB
    init: co.wrap(function* () {
        _conn = yield client.connect(mongo_url);
        return _conn;
    }),
    // insert a document to MongoDB
    insert: co.wrap(function* (doc) {
        yield _conn.collection(mongo_working).insertOne(doc);
    }),
    /*
     * 更新確認用データの存在チェック
     * true : 更新する
     * false: 初期設定する
     */
    isUpdate: co.wrap(function* () {
        const flagData = yield _conn.collection(mongo_working).find({ 'id':0 }).toArray();
        if(flagData[0]) {
            // データ有
            return true;
        }
        // データ無
        return false;
    }),
    // listing data
    list: co.wrap(function* () {
        return yield[
            _conn.collection(mongo_working).find({ 'id':0 }).toArray(),
            _conn.collection(mongo_working).find({ 'id':{'$gt':0} }).toArray(),
        ];
    }),

    // get one data
    findOne: co.wrap(function* (_id) {
        return yield[
            _conn.collection(mongo_working).find({ 'id':0 }).toArray(),
            _conn.collection(mongo_working).find({ 'id':Number(_id) }).toArray(),
        ];
    }),

    /*
     * 1 development環境で/setupがGETされた場合
     * 2 /indexがGETされ、isUpdateの結果がfalseの場合
     */
    setup: co.wrap(function* () {
        const now = moment();

        // 無条件に全データを削除、初期データを投入する
        yield _conn.collection(mongo_working).remove({});
        yield _conn.collection(mongo_working).insertMany(yield pdf.get(now));
        yield _conn.collection(mongo_working).insertOne({
            id    : 0,
            update: Number(now),
        });

        return yield[
            _conn.collection(mongo_working).find({ 'id':0 }).toArray(),
            _conn.collection(mongo_working).find({ 'id':{'$gt':0} }).toArray(),
        ];
    }),

    /*
     * 30分(development環境では1秒)間隔でPDFデータを取得し、データベースを更新する
     */
    update: co.wrap(function* () {
        const now = moment();
        const diff = ('production' === node_env?(1000*60*30):1000);

        const flagData = yield _conn.collection(mongo_working).find({ 'id':0 }).toArray();

        //規定更新時間経過チェック
        if (Number(now) - Number(moment(flagData[0].update)) > diff) {
            //データ更新
            const pdfData = yield pdf.get(now);
            //pdfデータがDBに存在する場合は更新し、存在しない場合は挿入する
            let data = null;
            for (let item of pdfData) {
                data = yield _conn.collection(mongo_working).find({ 'id':item.id }).toArray();
                if (data[0]) {
                    data = yield pdf.update(data[0], item, now);
                } else {
                    data = item;
                }
                yield _conn.collection(mongo_working).update(
                        {'id':item.id},
                        {$set:data},
                        {upsert:true});
            }
            //更新日時を更新
            yield _conn.collection(mongo_working).update(
                    {'id':0},
                    {$set: {update: Number(now)} },
                    {upsert:true});
            //今回更新されなかったデータを削除
            const unupdated = yield _conn.collection(mongo_working).find({ 'update': {'$ne':Number(now)} }).toArray();
            if (unupdated.length > 0) {
                const date = unupdated[0].update;
                yield _conn.collection(mongo_working).remove({ 'update':date });
            }
        }

        return yield[
            flagData,
            _conn.collection(mongo_working).find({ 'id':{'$gt':0} }).toArray(),
        ];
    }),
};

メンバメソッドsetupあたりで、削除、挿入、取得を色々と使っています。

PDF操作

PDF操作用にpdf2tableを使い、データを抜き出しています。PDF内のテーブルにアクセスしてデータを取得するのですが、空欄の場合、前詰めされてしまうようです。今回は、空欄となるパターンが、プログラム側で判定可能だったため、問題にはなりませんが、少し注意が必要です。

PUG

少し書いていれば慣れてきました。楽でいいですね。

views/index.pug
extends layout

block content

  div.container
     .page-header
       .row
         .col-lg-12
           h1= '型式試験の実施状況'
           p= '更新日時:' + update
     table.table.table-striped.table-hover
       thead
         tr
           th 管理番号
           th 設計書等審査
           th 対比照合審査
           th 遊技機の試験
           th 試験終了予定
       tbody
         each item in list
           - var _id = '000000' + String(item.id);
           - _id = _id.substring(_id.length - 6)
           - if (id.length == 0 || (id.length > 0 && id.indexOf(_id) >= 0))
             tr
               td=  _id
               td= (item.test[0].status == 0?'':(item.test[0].status == 1?'試験中':'試験終了'))
               td= (item.test[1].status == 0?'':(item.test[1].status == 1?'試験中':'試験終了'))
               td= (item.test[2].status == 0?'':(item.test[2].status == 1?'試験中':'試験終了'))
               td= item.guess

tbodyのところでid(GETメソッドで指定されたid)の判定をしています。指定がなければ全件表示、指定があれば一致したものだけ表示しています。

成果物

参考文献

7
4
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
7
4