NodeとExpressと、ついでにMongoDBを使ってみました。サンプルとして、「保通協の型式試験の実施状況(PDF)をHTMLで表示する」というのを作ってみることにします。あまりに多くアクセスするのはよくないので、一度アクセスしたらDBへ保存し、前回のアクセスから30分経っていない場合は、DBから取得するようになっています。
準備
$ npm install -g express-generator
$ express --view=pug somerset
とりあえず、express-generatorのサンプル通りです。
実装
以下、試行錯誤の結果だけを残しておきます。
エントリポイント
とりあえずエントリポイントは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と呼んでいるようです。
優先順位
リクエストされたパスを正規表現で検索し、マッチした場合のみ、その処理を行います。検索はファイルの上から行われ、順に処理されます。つまり、優先順位の高いものがある場合は、上に記述する必要があります。
.. 省略 ..
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
を参照します。
const dotenv = require('dotenv');
dotenv.load();
const node_env = process.env.NODE_ENV;
上記のコードのように、development
とproduction
環境で、環境変数を変える方法として、.env
ファイルを作成し、読み込む方法採用しました。ただ、evennodeではWebコンパネ上から環境変数が設定できるので、.env
は作成していません。(ローカルデバッグでのみ使っています)
リクエストクエリ
リクエストクエリは、routerのcallback関数の引数にセットされます。具体的にはapp.use('/', function(req, res, next){...});
と設定した場合、req.query
にセットされます。
const query_id = (req.query.id?req.query.id.split(','):[]);
/?id=12345,678901のようなクエリを,
で分割します。クエリがない場合、req.query.id
がundefined(falsy)
になるため、空配列を返すようにしています。
if (req.query.format && 'json' === req.query.format.toLowerCase()) {
}
こちらもreq.query.format
がundefined(falsy)
でない場合、json
であるかをチェックしています。(大文字小文字は判定しないので、無条件に小文字に変換しています)
レスポンス(JSON)
res.header('Content-Type', 'application/json; charset=utf-8');
res.json({ list:jsonData });
JSONを返す際にはヘッダーを適切に設定することと、charset=utf-8
をつけた方が良いようです。(ブラウザなどが誤爆しないように・・)
レスポンス(PUGでレンダリング)
res.render('index', {
title : req.html.title,
update: req.html.update,
list : req.html.list,
id : query_id,
});
引数であるindex
はviews/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にハンドラとメソッド群をまとめました。
'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
少し書いていれば慣れてきました。楽でいいですね。
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)の判定をしています。指定がなければ全件表示、指定があれば一致したものだけ表示しています。