はじめに
「IBM i 上でNode.jsのWebアプリを動かす」3つ目の記事です。
- 1つ目の記事はこちら
IBM i 上での Node.js の動かし方を簡単に紹介しています。
- 2つ目の記事はこちら
環境情報や作成したWebアプリの概要はこちらをご覧ください。
- 3つ目の記事はこちら
Express, EJSについてまとめています。
https://qiita.com/tom_m_m/items/8bc53ae27a396946c614
作成したコードはこちらにおいています。
この記事の中では、こちらのアプリを「QRコードアプリ」と記載しています。
Node.jsでのWebアプリ制作に関しては手探りで行っていますので、荒いところがあるかもしれません。
JavaScriptでのQRコード読み取り
QRコードの読み取りには、こちらで公開されている jQR を利用しています。
また、こちらの記事を参考にさせていただいております。
jsQRの使い方については、上記2つの記事を参考にしていただければと思います。
ここでは、QRコードアプリを作成した際に詰まった部分について記述します。
当初は、QRコード読み取り用のJavaScriptを別のファイルとして用意していました。
ただ、express, ejs の組み合わせで、うまくデータの受け渡しができず、ejsファイル内に JavaScript をベタがきしています。
良くないとは思いつつ、自分の技術力不足のためこの形になっています。
httpsでのアクセス
カメラをブラウザ上に表示する際、環境によっては https
でのアクセスが必須になるようです。
今回はこれに該当したため、自己署名証明書を作って簡易的にhttpsアクセスが出来るよう設定しています。
httpsでアクセスができない場合、カメラが起動しない状態となります。
httpsアクセスに関しては、こちらを参考にしています。
まずは、自己署名証明書を作成します。
QRコードアプリでは、key ディレクトリを作成して、そこに証明書のファイルを配置しています。
下記のように実行すると、証明書が作成できます。
key # openssl genrsa -out server_key.pem 2048
key # openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Tokyo/L=Musashino-shi/O=Foo/OU=Bar/CN=foo.bar.com"
key # openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Certificate request self-signature ok
subject=C = JP, ST = Tokyo, L = Musashino-shi, O = Foo, OU = Bar, CN = foo.bar.com
これを前回の記事で作成したExpress + EJSの簡易的なWebアプリを変更して https でアクセス出来るようにします。
前回作成したコードは、こちらでした。
express-test # cat index.js
const express = require('express');
const app = express();
app.set("view engine", "ejs");
const hostname = '<サーバーのIPアドレス>';
const port = 9443;
app.get('/', (req, res) => {
res.render('index', {data: 'jsから渡したデータ'});
});
app.listen(port, () => console.log(`Listening on port ${port}!`));
このコードを以下のように書き換えています。
- パッケージ
fs
の追加 - 証明書の読み込みとhttpsサーバーの作成
- appではなくserverでの起動に変更
express-test # cat index.js
const express = require('express');
var fs = require('fs'); //追加 1
const app = express();
app.set("view engine", "ejs");
//追加 2
const server = require('https').createServer(
{
key: fs.readFileSync('./key/server_key.pem'),
cert: fs.readFileSync('./key/server_crt.pem'),
},
app
);
const hostname = '<サーバーのIPアドレス>';
const port = 9443;
app.get('/', (req, res) => {
res.render('index', {data: 'jsから渡したデータ'});
});
server.listen(port, () => console.log(`Listening on port ${port}!`)); //変更 3
この変更を行うことで、httpsでのアクセスが可能になります。
ブラウザでアクセスすると、最初は警告画面が出るので、詳細設定を開いた上でページにアクセスします。
アクセスすると、httpsで表示されていることがわかります。
IBM i 上でjsQRを動かすときには、このhttpsの設定が必要でした。
QRコードアプリ詳細
ここまで、QRコードアプリを構成しているパーツについて書いてきました。
それでは、実際に作成したアプリの動きを見ていきます。
画面については、あまりカスタマイズしてないためシンプルな見た目となってます。
アクセスすると、このような画面が表示されます。
index.ejs が表示されている状態となり、jqQRがブラウザ側で動いている状態です。
黒色で表示されているのが、カメラ画面になります。
index.ejs
<!DOCTYPE html>
<html lang="jp">
<head>
<%- include('./partials/head'); %>
<link rel="stylesheet" href="css/index.css">
</head>
<body class="container">
<header>
<%- include('./partials/header'); %>
</header>
<main>
<div id="loadingMessage"> カメラが有効になっていません</div>
<canvas id="canvas" hidden></canvas>
<div id="outputs" hidden>
<div id="outputMessage">QRコード検出なし</div>
<div hidden><b>登録番号:</b> <span id="outputData"></span></div>
<div hidden><b></b> <span id="result"></span></div>
</div>
<div id="outputs" hidden>
<button type="button" class="btn btn-primary" onclick="yes()">Yes</button>
<button type="button" class="btn btn-secondary" onclick="no()">No</button>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script type="text/javascript">
var video = document.createElement("video");
var canvasElement = document.getElementById("canvas");
var canvas = canvasElement.getContext("2d");
var loadingMessage = document.getElementById("loadingMessage");
var outputContainer = document.getElementById("outputs");
var outputMessage = document.getElementById("outputMessage");
var outputData = document.getElementById("outputData");
var result = document.getElementById("result");
var requestID;
function drawLine(begin, end, color) {
canvas.beginPath();
canvas.moveTo(begin.x, begin.y);
canvas.lineTo(end.x, end.y);
canvas.lineWidth = 4;
canvas.strokeStyle = color;
canvas.stroke();
}
// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then(function(stream) {
video.srcObject = stream;
video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
video.play();
requestAnimationFrame(tick);
});
var qr_flag = JSON.parse("<%= flag %>".replace(/"/g,'"'));
var update_flag = JSON.parse("<%= update_flag %>".replace(/"/g,'"'));
if (update_flag == 1){
outputMessage.innerText = '入室処理が完了しました。';
} else if (update_flag == 2){
outputMessage.innerText = '退室処理が完了しました。';
} else if (update_flag == 3) {
outputMessage.innerText = 'すでに処理が完了しているため、変更されませんでした。';
}
function tick() {
loadingMessage.innerText = "Loading video..."
if (video.readyState === video.HAVE_ENOUGH_DATA) {
loadingMessage.hidden = true;
canvasElement.hidden = false;
outputContainer.hidden = false;
canvasElement.height = video.videoHeight;
canvasElement.width = video.videoWidth;
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
var imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
var code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
if (code) {
drawLine(code.location.topLeftCorner, code.location.topRightCorner, "#FF3B58");
drawLine(code.location.topRightCorner, code.location.bottomRightCorner, "#FF3B58");
drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, "#FF3B58");
drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, "#FF3B58");
outputMessage.hidden = true;
outputData.parentElement.hidden = false;
outputData.innerText = code.data;
if (code.data) {
post("/qrid", {id:code.data});
sleep(300);
} else {
result.parentElement.hidden = false;
}
} else {
outputMessage.hidden = false;
outputData.parentElement.hidden = true;
result.parentElement.hidden = true;
}
}
requstID = requestAnimationFrame(tick);
}
function post(path, params, method='post') {
const form = document.createElement('form');
form.method = method;
form.action = path;
for (const key in params) {
if (params.hasOwnProperty(key)) {
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = key;
hiddenField.value = params[key];
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
function sleep(waitMsec) {
var startMsec = new Date();
while (new Date() - startMsec < waitMsec);
}
</script>
</body>
</html>
QRコードを写すと、一瞬QRコードの部分が赤枠で囲われ、次の画面に移ります。
読み取りが完了すると、読み取ったIDをPOSTリクエストでサーバー側に送信します。
QRコードを読み取り、IBM i のDBに一致するIDが存在する場合は、処理を行うかどうかの選択肢が表示されます。
POSTリクエストを受け付けると、サーバー側では、idb-connect を介してデータが存在するかどうか確認するためのSQLが実行されています。
app.post('/qrid', (req, res) => {
if (req.body.id != null){
const sql = `SELECT * FROM ACCESS.ACCESSTABLE WHERE QRID = ${req.body.id} LIMIT 1`;
const connection = new dbconn();
connection.conn('*LOCAL');
const statement = new dbstmt(connection);
const result = statement.execSync(sql);
let qrid = JSON.stringify(result[0].QRID);
let name = JSON.stringify(result[0].NAME);
let status = JSON.stringify(result[0].STATUS);
statement.close();
connection.disconn();
connection.close();
res.render('submitqr.ejs', {qrid : qrid, name : name, status : status, flag : 1});
}
});
はい
を選択すると、POSTリクエストが送信され、サーバー側ではDBのデータ更新が行われます。
今回のアプリだと、予約状態から入室状態に、入室状態から退出状態に STATUS を変更します。
入退出のDB更新部分
app.post("/submit",(req,res)=>{
let update = 0;
const connection = new dbconn();
connection.conn('*LOCAL');
const statement = new dbstmt(connection);
const sql = `SELECT * FROM ACCESS.ACCESSTABLE WHERE QRID = ${req.body.id} LIMIT 1`;
const result = statement.execSync(sql);
let qrid = JSON.stringify(result[0].QRID).slice( 1, -1 );
let status = JSON.stringify(result[0].STATUS).slice( 1, -1 );
const statement_update = new dbstmt(connection);
if (status == 'schedule'){
const sql_up = `UPDATE ACCESS.ACCESSTABLE SET STATUS = 'stay' WHERE QRID = ${qrid}`;
const result = statement_update.execSync(sql_up);
update = 1;
} else if (status == 'stay'){
const sql_up = `UPDATE ACCESS.ACCESSTABLE SET STATUS = 'leave' WHERE QRID = + ${qrid}`;
const result = statement_update.execSync(sql_up);
update = 2;
} else {
update = 3;
}
statement.close();
statement_update.close();
connection.disconn();
connection.close();
res.render('index.ejs', {update_flag : update, flag : 0});
})
一連の流れは以上になります。
アプリ全体のコードはこちらにおいてありますので、コピーすれば試すことができます。
パッケージのインストールとと証明書の作成、DBの作成を行うことで動かすことができます。
動かすまでの手順は以下になります。
- Githubからのソースダウンロード
work # git clone https://github.com/tom-m-m/ibmi-nodejs-qr-detection.git
Cloning into 'ibmi-nodejs-qr-detection'...
remote: Enumerating objects: 40, done.
remote: Counting objects: 100% (40/40), done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 40 (delta 15), reused 35 (delta 10), pack-reused 0
Receiving objects: 100% (40/40), 3.93 MiB | 3.15 MiB/s, done.
Resolving deltas: 100% (15/15), done.
- パッケージのインストール
Githubにはモジュールをアップロードしていないため、インストールが必要となっています。
package.json があると、npm install
で必要なパッケージがインストールできます。
work # cd ibmi-nodejs-qr-detection/
ibmi-nodejs-qr-detection # ls
app.js core odbc.ini package-lock.json package.json views
ibmi-nodejs-qr-detection # npm install
added 216 packages, and audited 217 packages in 9s
24 packages are looking for funding
run `npm fund` for details
8 moderate severity vulnerabilities
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
- 証明書の作成
証明書は、この記事の上部で記載している内容と同様になります。
ibmi-nodejs-qr-detection # mkdir key
ibmi-nodejs-qr-detection # cd key/
ibmi-nodejs-qr-detection/key # openssl genrsa -out server_key.pem 2048
ibmi-nodejs-qr-detection/key # openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Tokyo/L=Musashino-shi/O=Foo/OU=Bar/CN=foo.bar.com"
ibmi-nodejs-qr-detection/key # openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Certificate request self-signature ok
subject=C = JP, ST = Tokyo, L = Musashino-shi, O = Foo, OU = Bar, CN = foo.bar.com
ibmi-nodejs-qr-detection/key # cd ..
ibmi-nodejs-qr-detection # node app.js
Listening on port 9443!
あとは、DBの作成を行うと完了です。
IDを含むQRコードは、Webのサービスを使うと簡単に作成できます。
Node.js にもQRコード作成用のパッケージもあります。
4編に渡る記事になりましたが、このシリーズは終了となります。
ありがとうございました。