socket.ioとchart.jsを使ってメモリ利用率をグラフ表示

まずはchart.jsを表示するHTML

[index.html]

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Server HealthCheck</title>
  <script src="https://[your socket server]/socket.io/socket.io.js"></script>
</head>
<body>
    <div style="position:absolute; top:60px; left:10px; width:500px; height:500px;">
  <canvas id="myChart" style="position: relative; height:100; width:150"></canvas>
    </div>

    <script src="/chartjs/moment/moment.js"></script>
    <script src="/chartjs/chart.js/dist/Chart.js"></script>
    <script src="/chartjs/chartjs-plugin-streaming.js"></script>
    <script src="/js/health.js"></script>
</body>
</html>

canvasを配置して、javascript部分をhealth.jsに作っていきます。

[health.js]

'use strict';
/*
 * Socket.io Setting
 */
let socket = io.connect('https://[your socket server]/');
let useMemPercent = null;

socket.on('connect', (evt) => {
  console.log('[SOCKET IO] socket.io connected.');
});

socket.on('health', (message) => {
        //受信したmessageにはメモリの利用率が数字で入っています。
  useMemPercent = message;

});

//あとはchart.jsのサンプルを少し変更
var chartColors = {
  red: 	'rgb(255, 99, 132)',
  orange: 'rgb(255, 159, 64)',
  yellow: 'rgb(255, 205, 86)',
  green: 	'rgb(75, 192, 192)',
  blue: 	'rgb(54, 162, 235)',
  purple: 'rgb(153, 102, 255)',
  grey: 	'rgb(201, 203, 207)'
};

function getUseMemPercent() {
  return useMemPercent;
}

function onRefresh(chart) {
  chart.config.data.datasets.forEach(function(dataset) {
    dataset.data.push({
      x: Date.now(),
      y: getUseMemPercent()
    });
  });
}

var color = Chart.helpers.color;
var config = {
  type: 'line',
  data: {
    datasets: [{
      label: 'Use Memory',
      backgroundColor: color(chartColors.orange).alpha(0.5).rgbString(),
      borderColor: chartColors.blue,
      fill: false,
      lineTension: 0,
      borderDash: [8, 4],
      data: []
    }]
  },
  options: {
    title: {
      display: true,
      text: 'Server Use Memory'
    },
    scales: {
      xAxes: [{
        type: 'realtime',
        realtime: {
          duration: 20000,
          refresh: 1000,
          delay: 2000,
          onRefresh: onRefresh
        }
      }],
      yAxes: [{
        scaleLabel: {
          display: true,
          labelString: 'percent'
        },
        ticks: {
          bigenAtZero: true,
          min: 0,
          max: 100
        }
      }]
    }
  }
};

window.onload = function() {
  var ctx = document.getElementById('myChart').getContext('2d');
  window.myChart = new Chart(ctx, config);
};

 

socket.ioサーバにnode.jsを利用します。

[server.js]

module.exports = io => {
  let os = require('os');
  
  io.on('connection', (socket) => {

    console.log('[SOCKET IO] Socket.io New Connection');

    setInterval(cb, 1000);

    function cb(){
      let useMem = os.totalmem() - os.freemem(); 
      let useMemPercent = Math.floor(useMem / os.totalmem() * 100);
      io.emit("health", useMemPercent);
    }

  });
}

 

1秒毎にメモリ使用率がチャートに表示されます。

ESLINTの設定ファイル

/*
* ESLint設定ファイル
* PATH: /.eslintrc.js
* 参考:https://garafu.blogspot.com/2017/02/eslint-rules-jp.html
*/
module.exports = {
  env: {
    browser: true, // document や console にエラーが出ないようにする
    node: true, // document や console にエラーが出ないようにする
    es6: true // es6から使える let や const にエラーがでないようにする
  },
  extends: ["eslint:recommended"],
  "rules": {
    // off, warn, errorで設定
    "no-console":"off", // console.logの場所を確認(公開時はerrorを設定して不要なconsoleを削除)

    "strict":"error", // use strict を記述すること
    "quotes": ["error", "single"], // クォーテーションはシングル
    "indent": ["error", "tab"], // インデントはタブ
    "no-dupe-args":"error", // function 内で重複して変数宣言を行わないこと
    "no-extra-semi":"error", // 不要なセミコロンの記述しないこと
    "no-func-assign":"error", // function を再定義しないこと
    "no-unreachable":"error", // 到達不可能なコードを記述しないこと
    "valid-typeof":"error", // typeof利用時は正しい型名文字列と比較すること
    "no-eq-null":"error", // null または undefined と比較するときは厳密な比較演算子を利用すること
    "no-magic-numbers":"error", // マジックナンバーを使用しないこと
    "init-declarations":"error", // 変数定義時に初期化を行うこと
    "no-undef-init":"error", // 変数定義時に undefined で初期を行わないこと
    "no-undef":"error", // 未定義の変数は利用しないこと
    "block-spacing":"error", // 単一行ブロックを使うときはブロックのすぐ内側に空白を入れること
    "camelcase":"error", // 変数名はキャメルケースで記述すること
    "id-blacklist":"error", // ブラックリストとして定義された名前は利用しないこと
    "max-len":"error", // 1行80文字以内とすること
    "max-lines":"error", // 1ファイル300行以内とすること
    "newline-after-var":"error", // 変数宣言の直後には空行を入れること
    "newline-before-return":"error", // return の前には空行を入れること
    "space-before-blocks":"error", // ブロック開始前には空白を入れること
    "spaced-comment":"error", // コメントの先頭には空白を入れること
    "arrow-parens":"error", // アロー関数の引数部分には丸括弧を記述すること
    "arrow-spacing":"error", // アロー関数の矢印前後には空白を入れること
    "no-const-assign":"error", // const 定義された変数の再定義を行わないこと
    "no-var":"error", // var (メソットスコープ変数) は使わず let または const (ブロックスコープ変数) を使うこと
    "prefer-arrow-callback":"error", // コールバックにはアロー関数を利用すること
    "prefer-const":"error", // 再代入を行わない変数は const を利用すること
    "accessor-pairs":"error", // getter とsetter はペアで作成すること
  }
};


 

利用者画面と管理画面を別々のポートで待ち受けたい

アプリ利用者側画面と管理者画面のインターフェースを分けたい場合

ポート番号を別々にして、違うアプリケーションのように見せることで

URLを手打ちされても、ポートが違うのでNotFoundとなり安心

 

#main.js

// ========================================= 
// Express Settings
// =========================================

const express = require('express'),
  app = express(),
  adminConsole = express(),
  path = require('path');

// --------------------------------------------------
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(express.static(path.join(__dirname, 'public')));
// --------------------------------------------------

adminConsole.set('port', 12345);
adminConsole.set('views', path.join(__dirname, 'views'));
adminConsole.set('view engine', 'ejs');
adminConsole.use(express.static(path.join(__dirname, 'public')));

// =========================================
// Load MiddleWare
// =========================================

const router = require('./routes/route'),
  adminRouter = require('./routes/adminRoutes');

app.use('/', router);
adminConsole.use('/admin', adminRouter);

// =========================================
// Start Node.js
// =========================================

app.listen(app.get('port'), () => {
  console.log(`AppServer running at http://localhost:${app.get('port')}`);
});

adminConsole.listen(adminConsole.get('port'), () => {
  console.log(`AdminConsole running at http://localhost:${adminConsole.get('port')}`);
});

 

上記例だと一般利用者はport3000でアクセスしてルーティングされ

管理者はport12345の/admin/で利用すると操作ができる。

 

chatのコントローラーをmain.jsに書きたくない!

require時にsocket.ioを渡してあげれば実行される。

#main.js

const server = app.listen(app.get('port'), () => {
  log.console(
    log.types.Info,
    `AppServer running at http://localhost:${app.get('port')}`
  );
});

// WebSocket Setting
const io = require('socket.io')(server);
require('./controllers/chatController')(io);

module.exportsで指定した引数に渡されて処理される。

#chatController.js

module.exports = io => {
  io.on('connection', client => {
    console.log('[CHAT INFO] new connection');

    client.on('disconnect', () => {
      console.log('[CHAT INFO] user disconnected');
    });

    client.on('message', () => {
      io.emit('message', {
        content: 'Hello'
      });
    });
  });
};

 

特にmain.jsからの呼び出しは無いので、main.jsではconstを利用して代入していない。

mysqlを使いやすくする

データベース接続用ファイルを作る。

// db.js(models/db.js)
// npm install mysql --save
const mysql = require("mysql");
const connection = mysql.createConnection({
    host : [MYSQL HostIP],
    port : [MySQL PortNo], 
    user : [Your User],
    password: [Your Password],
    database: [MySQL DatabaseName]
});

connection.connect(function(err) {
    if (err) {
        console.error("MYSQL connection Error: " + err.stack);
        return;
    }

    console.log('MYSQL connected as ThreadID: ' + connection.threadId);
});

module.exports = connection;

 

exportsしているので、あとは利用するファイルから読み込むだけ

//適当なファイル

const db = require("../models/db");

var sql = "select * from user;";

db.query(sql, (err,results,fields) => {
     if( err !== null ){
           console.log(err);
     }
});

 

MVCっぽく

//log.js(models/log.js)

const db = require("../models/db");

exports.getAllLog = (req, res, next) => {
    db.query("select * from log", (err, results, fields) => {
        req.data = results;
        next();
    });
};
//main.js
const log = require("./models/log");

// Routing設定
app.get("/test", log.getAllLog, (req, res, next) => {
    res.render("test", { contents: req.data });
});

 

shellを実行する

サーバの状態を確認したり、他のサーバの情報を連携したいときにshellコマンドを利用したい場合

//node.jsの標準モジュール child_processを読み込み
const exec = require('child_process').exec;

//shellのdateコマンドを実行
exec('date', (err, stdout, stderr) => {
   if(err !== null){
        console.log(err);
   } else {
        console.log(stdout);
   }
});

 

requireするモジュールにデータを渡して挙動を変える

下記のような引数があるモジュールを定義

// test.js
module.exports = msg => {
    console.log(msg);
};

引数付きで呼び出せば実行される。

require("test.js")("welcome message");

 

関数として使いたい場合は下記のように定義

// test.js
module.exports = msg => {
    module.showMessage = () => {
        console.log(msg);
    };

    return module;
};

requireして実行してやれば良い

const test = require("test.js")("welcome message");
test.showMessage();

 

 

POSTを受け取る

ExpressでPostを受け取るにはmain.jsに下記コードが必要

app.use(
     express.urlencoded({
          extended: false
     })
);
app.use(express.json());

 

POSTするHTMLを作成

<form method="post" action="/post_test">
    <label for="memo">Memo</label>
    <input type="text" name="memo">
    <button type="submit">Post!</button>
</form>

 

あとは普通にapp.postを利用して受け取る

app.post("/post_test", (req, res) => {
    console.log(req.body.memo);
});

 

(注)req.body.memoはHTMLのname属性を指定する。

Node.jsの自動再起動

Node.jsはビュー以外のファイルを修正すると、プロセスの再起動しなくては修正が反映されない。

 

ファイルの修正 → プロセス終了 → プロセス起動

 

この一連の操作が大変なのでnodemonをインストールして.jsファイルが書き換わると自動的にプロセスの再起動するように設定する。

 

インストール方法

npm install nodemon --save-dev

 

あとはnode main.jsとプロセスを起動させる代わりにnodemon main.jsとしてやれば良い

nodemon main.js

 

もう少し楽をしたい場合はpackage.jsonにstartスクリプトを追加する

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon main.js"
  },

 

スクリプトを追加したらnpmで起動できるようになるので起動させるjsファイル名を意識しなくて良くなる

npm start

 

EJSで部品を作成して読み込む

ヘッダーやフッター、サイドメニューなどはEJSで部品化しておくと便利。

header.ejsなどの別ファイルを作成してinclude関数で読み込んでインライン展開される。

※.ejsは省略可能です。

<%- include("header"); %>

 

パスも有効なのでcomponent(部品)フォルダを作成すると以外とファイルもコードもスッキリ。

<%- include("component/footer"); %>

 

include関数の第2引数でパラメータを渡すことも可能

<%- include("component/header", title: "PageTitle"); %>