目次


IBM Cloud Node.js アプリケーション入門, 第 1 回

Node.js を使用して受付用訪問者記録アプリケーションを作成する

サーバー・サイドのコードを開発する

Comments

コンテンツシリーズ

このコンテンツは全3シリーズのパート#です: IBM Cloud Node.js アプリケーション入門, 第 1 回

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:IBM Cloud Node.js アプリケーション入門, 第 1 回

このシリーズの続きに乞うご期待。

この記事では、組織が受付で訪問者の出入りを記録するために使用できる Node.js アプリケーションを、IBM Cloud を利用して作成する方法を説明します。手順に沿って、Node.js、Express HTTP サーバー・ライブラリー、Cloudant データベースを使用する方法、そして可用性に優れた IBM Cloud 内で一連のタスクを行う方法を学んでください。この記事は、IBM Cloud プラットフォーム上で Node.js プログラミングを行うための基礎入門です。

アプリケーションを作成するために必要なもの

  • 無料の IBM Cloud アカウント
  • HTML の基礎知識
  • JavaScript の基礎知識

アプリを実行するコードを入手する

はじめに

まず、アプリケーションを作成します。続いて、開発環境を作成します。

アプリケーションを作成する

  1. IBM Cloud コンソールにログオンします。IBM アカウントをお持ちでない場合は、このリンク先のページでアカウントを作成してください)。
  2. ハンバーガー・アイコンを展開して、「Cloud Foundry Apps (Cloud Foundry アプリケーション)」を選択します。
  3. 「Create resource (リソースを作成)」をクリックします。
  4. カテゴリーとして「Cloud Foundry Apps (Cloud Foundry アプリケーション)」を選択します。このカテゴリーに含まれている「SDK for Node.js」をクリックします。
  5. 「SDK for Node.js」 をクリックします。
  6. 以下のパラメーターを入力します。
    App name (アプリケーション名)使用されていない値にします。
    Host name (ホスト名)デフォルト値を受け入れます。
    Domain (ドメイン)mybluemix.net
    Region/location (地域/場所)最も近い場所を選びます。
    Organization and space (組織とスペース)デフォルト値のままにします。
  7. 「Create (作成)」をクリックします。
  8. 左側のサイドバーにある「Overview (概要)」をクリックします。
  9. 「Visit App URL (アプリケーションの URL にアクセス)」をクリックします。
  10. アプリケーションが作成されて起動されるまでに、リロードしなければならない場合があります。
  11. 図 1 に、初期状態のアプリケーションを示します。アプリケーションが表示されたら、そのタブを閉じないでください。後で必要になります。

注: これは初期状態のアプリケーションです。サンプル・アプリケーションの URL にアクセスすると表示されるアプリケーションは、これとは異なっていることでしょう。この記事の後半で、そのユーザー・インターフェースを作成します。

図 1. 初期状態のアプリケーション
初期のアプリケーション
初期のアプリケーション

開発環境を作成する

IBM Cloud 上の Cloud Foundry では、複数の開発環境を使用できるようになっています。この記事では楽な方法を取り、Web 開発環境を使用します。

  1. IBM Cloud コンソールで、アプリケーションのページに戻ります。ログアウトしている場合は、もう一度ログインしてから、ダッシュボード内でアプリケーションの名前をクリックします。
  2. 「Continuous Delivery (継続的デリバリー)」までスクロールダウンして、「Enable (有効化)」をクリックします。
  3. デフォルト値のまま「Create (作成)」をクリックします。
  4. ツールチェーンが作成されたら、「Eclipse Orion Web IDE」をクリックします。
  5. 「Create new launch configuration (新しい起動構成を作成)」 > 「+」の順にクリックします。
  6. 「Save (保存)」をクリックしてデフォルトの起動構成を作成します。必要なものは、デフォルトの起動構成にすべて揃っています。
  7. 「Visitor-Log (訪問者記録)」 > 「app.js」の順にクリックします。
  8. ファイルの内容を、このリンク先のコードで置き換えます。
  9. 再生アイコンをクリックして変更を適用します。
  10. 緑色の円が再び表示されるまで待ちます。緑色の円は、アプリケーションが新しいコードを使用して再起動したことを意味します。
  11. アプリケーションのタブに戻り、URL の末尾に「/hello」を追加します。「Hello, world」というメッセージが表示されることを確認します。

このプログラムを理解するために、ソース・コードを 1 行ずつ見ていきます。ソース・コードは、このリンク先のページにあります。

最初の行はコメントになっているので、JavaScript インタープリターに無視されます。このコメントを入れている理由は、IBM Cloud コンソールのエディターには Eslint ユーティリティーが組み込まれているためです。Eslint ユーティリティーはコードの潜在的エラーを識別することから、このディレクティブを使用して、JavaScript を Node.js (サーバー・サイドの JavaScript) コードとして扱うようユーティリティーに指示しています。Eslint ユーティリティーについて詳しくは、このリンク先のページを参照してください。

/*Eslint-env node*/

JavaScript にコメントを書き込むには、以下の方法もあります。ダブル・スラッシュ (//) の後に続く行は、コメントとして無視されます。

//------------------------------------------------------------------------------
// node.js starter application for Bluemix
//------------------------------------------------------------------------------

Node.js はランタイム環境です。Web サーバー・パッケージは Express です。Express について詳しくは、このリンク先のページを参照してください。Node.js 内でパッケージを使用するには、パッケージ名を指定した require 関数を使用します (そのパッケージが package.json ファイル内で宣言されていることが前提となります)。require 関数は通常、該当するモジュールを使用するための関数を返します。

// This application uses express as its web server
// for more info, see: http://expressjs.com
var express = require('express');

cfenv は、Cloudy Foundry 情報を提供するモジュールです。後でこのモジュールを使用して、Web サーバーが listen しなければならないポート番号と、サービスを提供している URL を取得します。

// cfenv provides access to your Cloud Foundry environment
// for more info, see: https://www.npmjs.com/package/cfenv
var cfenv = require('cfenv');

次のステップは、HTTP サーバーを作成することです。

// create a new express server
var app = express();

以下のコードでは、Express に対して特定のディレクトリーから静的ファイルを提供するよう指示しています。エディター内で public ディレクトリーを開くと、index.html ファイルがあることがわかります。このファイルは、デフォルト・アプリケーション (図 1 に示されているアプリケーション) の一部です。

// serve the files out of ./public as our main files
app.use(express.static(__dirname + '/public'));

app.get の呼び出しにより、特定のパスに対するリクエストの処理方法を指定します。この関数の最初のパラメーターで、パスを指定します。2 番目のパラメーターで、そのパスがリクエストされたときに呼び出す関数を指定します。呼び出される関数のパラメーターには、HTTP リクエストが含まれるリクエスト・オブジェクト、レスポンスを返すために使用するレスポンス・オブジェクトの 2 つがあります。JavaScript では、関数リテラル (例えば自身を関数のパラメーターにすることができる、匿名関数) に 2 通りの構文を使用できるようになっています。そのうち、古くから使われているほうの構文は、ここで使用している、function(<パラメーター>) {本文} です。

コールバック関数は単に、res.send 関数を使用して一定の文字列 (お馴染みの「hello world」) を送信するに過ぎません。

/* @callback */ コメントは、Eslint 用です。関数に未使用のパラメーターがあると、Eslint は通常、そのパラメーターを削除するようアドバイスします。けれども、このコールバック関数のパラメーターは呼び出し側のコード (ここでは Express) によって設定されます。この場合、パスのコールバックによっては両方のパラメーターが必要になります。

app.get("/hello", /* @callback */ function(req, res) {
	res.send("Hello, world");
});

次のステップでは、Cloud Foundry 環境を取得します。IBM Cloud インフラストラクチャーは、ここに示されている方法を使用してアプリケーションと通信します。詳細については、このリンク先のページを参照してください。

// get the app environment from Cloud Foundry
var appEnv = cfenv.getAppEnv();

// start server on the specified port and binding host
app.listen(appEnv.port, '0.0.0.0', function() {
  // print a message when the server starts listening
  console.log("server starting on " + appEnv.url);
});

訪問者記録

このアプリケーションの主要なデータ構造は、訪問者記録です。このデータ構造を宣言して処理するコードが含まれるソース・コードは、このリンク先のページで確認できます。現在の app.js をこのコードで置き換えた上で、再生ボタンをクリックしてアプリケーションを再起動してください。https://<アプリケーション名>/test/visitors にアクセスすると、現在の訪問者記録が表示されます。訪問者記録は JSON で作成されているため、このリンク先にあるような JSON フォーマッターを使用すると、わかりやすくなるかもしれません。私のアプリケーション・インスタンスの訪問者記録を見るには、ここをクリックしてください。

訪問者記録は、訪問者名をキーとして持つ連想配列 (ハッシュ・テーブルまたはオブジェクトとも呼ばれます) です。各訪問者名の値はそれ自体が連想配列であり、以下の 2 つの属性を持つことができます。

  • arrived – 訪問者が前回オフィスを訪れた時刻。訪問者が現在オフィス内にいない場合、この属性は存在しません。
  • history – 訪問者の履歴。これまでの訪問の到着時刻と退出時刻を格納する連想配列からなる配列です。訪問者が初めてオフィスを訪れている間は、この属性は存在しません。

デフォルトの訪問者記録

デバッグ用に、サンプル・データを使用したデフォルトの訪問者記録を用意しておきました。図 2 に、このデフォルトの訪問者記録を記載します。

図 2. デフォルトの訪問者記録
デフォルトの訪問者記録
デフォルトの訪問者記録

このデフォルトの訪問者記録の定義については、このリンク先の GitHub 上にあるコードで、23 行目から 48 行目までを参照してください。その大部分は JavaScript の標準的なオブジェクト定義ですが、日付は固定されているのではなく、アプリケーションの起動時を基準として、一定時間遡った値です。Date.now() メソッドは、一定の時点 (1970 年 1 月 1 日の真夜中 (GMT)) から現在までの経過時間をミリ秒単位で返します。したがって、Bill Hamm の到着時刻は、アプリケーションが起動してこのコードが実行された 2 時間前ということになります。Date オブジェクトについて詳しくは、このリンク先のページを参照してください。

	"Bill Hamm": {
		arrived: new Date(Date.now() - 1000*3600*2) 
	},

訪問者記録を理解する

訪問者記録を表示するときに、すべての訪問者の記録を表示するか、あるいは現在オフィス内にいる訪問者の記録だけを表示するかを選べると便利です。また、完全な情報を取得せずに、訪問者全員の名前、現在オフィス内にいる訪問者の名前、または現在オフィス内にいない訪問者の名前だけを表示できる機能があれば役立ちます。こうした要件を実装する関数は次のとおりです。

訪問者の名前のリストを取得するには、visitors オブジェクトのキーを取得するだけです。オブジェクトのキーを取得する関数は、Object.keys です。以下に示すように、この関数には、(パラメーター) => {式;} という新しい構文を使用していることに注意してください。この特定の例では、パラメーターは使用していません。

var visitorNames = () => {
	return Object.keys(visitors);	
};

サブセット (オフィス内にいる訪問者またはオフィス内にいない訪問者) を取得する場合は、filter 関数を使用します。この関数は配列メソッドです (したがって、例えば visitorNames から返されたリストをはじめ、あらゆるリストに対して呼び出すことができます。filter 関数は唯一のパラメーターとして関数を取ります。その関数はパラメーターとしてのリストからアイテムを 1 つ取得し、そのアイテムがフィルタリングされたリストのメンバーであるかどうかを返します。

以下の関数の定義は単純化されています。関数リテラルが単一の式である場合、関数を波括弧 ({}) で囲まずに、return ステートメントを使用できます。したがって、以下のように式を入力できます。

var currentVisitorNames = () => {
	return visitorNames().filter((name) => visitors[name].arrived !== undefined);
};

現在の訪問者とその情報のリストを、名前をキーとして取得するとなると、もう少し複雑になってきます。最初のステップは currentVisitorList 内で行われます。この関数は map 関数を使用して、現在の訪問者のリストに含まれる各ユーザーの情報を取得します。currentVisitorList 関数は filter と同じように関数をパラメーターとして受け取りますが、値を出力に含めるかどうかを決定するのではなく、map 内の関数を使用して値を変換します。こうして変換された値が、元の値と同じ順序で並べられてリストになるというわけです。この関数については、このリンク先のページを参照してください。

この例の場合、名前ごとに、ユーザー情報が含まれる構造が返されます。例えば、デフォルトのデータを使用した場合は、図 3 に示すような構造が返されます。

var currentVisitorNames = () => {
	return visitorNames().filter((name) => visitors[name].arrived !== undefined);
};
図 3. 現在の訪問者のリスト
現在の訪問者のリスト
現在の訪問者のリスト

必要な情報は含まれているものの、リスト内では各ユーザーが個別のオブジェクトに分かれています。それよりも、visitors 変数内で使用しているフォーマットと同じように、ユーザーを 1 つのオブジェクト内にまとめて、ユーザーごとに個別の属性を持たせたほうが適切です。そのようにするために、reduce を使用してリストを単一のオブジェクトにしています。reduce 関数は、受け取ったリストに 1 つのアイテムしかない場合、そのアイテムを返します。2 つのアイテムがある場合、それらのアイテムに対して parameter 関数を実行し、その結果を返します。3 つ以上のアイテムがある場合は、そのうちの 2 つに対して parameter 関数を実行し、その結果に対してさらに parameter 関数を実行するといった具合に、値が 1 つになるまでこの処理を繰り返します。この関数について詳しくは、このリンク先のページを参照してください。

var currentVisitors = () => {
	return currentVisitorList().reduce((a, b) => {

結合オブジェクトを作成するには、まず、作成する新しいオブジェクト (ユーザーごとに 1 つの属性しかないリストに含まれるオブジェクト) のキーが必要です。

		var bKey = Object.keys(b)[0];

この値を元のオブジェクト (すべてのユーザーを集めたオブジェクト) に追加して返します。

		a[bKey] = b[bKey];
		
		return a;
	});
};

訪問者記録を変更する

訪問者記録に変更が加えられるのは、ユーザーがオフィスに入ってきた訪問者をログインする場合、あるいはオフィスから出ていく訪問者をログアウトする場合の 2 つです。

こうした変更に対処するために、get 関数と set 関数を使用して visitors 変数を操作します。この 2 つの関数に意図されている目的は、将来異なるデータ・ストレージ (データベースなど) を使用する際に、アプリケーションを変更しやすくすることです。

var getVisitor = (name) => visitors[name];
var setVisitor = (name, values) => visitors[name] = values;

ログアウト関数は比較的単純なものです。この関数はまず、ユーザーに arrived 属性が設定されているかどうかをチェックします。この属性がなければ、そのユーザーは現在オフィス内にいないことになるので、ログアウトすることはできません。その場合、エラーを報告します。arrived 属性が存在する場合はそれを削除して、たった今終了した訪問を履歴に追加します。

var logOut = (name) => {
	var oldRecord = getVisitor(name);

文字列が逆ティック (`) で囲まれている場合、それは、その文字列がテンプレート・リテラルであることを意味します。テンプレート・リテラルは何行にもわたって続けることができます。また、テンプレート・リテラルでは、例えば以下に示すように name 変数に式の値ではなく ${<式>} という構文を使用できます。ユーザーが 1 人もいない場合は、次のエラーを返します。

	if (oldRecord === undefined)
		return `Error, ${name} is unknown`;

ユーザーは存在する一方、その時点でログインしていない場合は、次のエラーを返します。

	if (oldRecord.arrived === undefined)
		return `Error, ${name} is not logged in`;

これまでの訪問者記録に含まれる属性のうち、引き続き必要になるものは history だけです。この属性については、常に保持して新しい値を追加する必要があります。ただし、初めてオフィスを訪れた訪問者には history 属性がありません。その場合、history は空になります。

var history = oldRecord.history;

	// If this is the first visit
	if (history === undefined) 
		history = [];

unshift 関数は、配列の先頭に新しい値を追加します。この動作が必要になる理由は、関心の高い訪問は、最近の訪問である場合が多いためです。したがって、配列を新しい順でソートすることが最善の策となります。この関数について詳しくは、このリンク先のページを参照してください。

	history.unshift({
		arrived: oldRecord.arrived,
		left: new Date(Date.now())
	});

現在ログインしていない訪問者には、history 属性しかありません。

	setVisitor(name, {history: history});
	
	return `OK, ${name} is logged out now`;
};

ログイン関数は、既存の history 属性と現在の時刻を設定した arrived 属性を使用して新しい記録を作成します。

var logIn = (name) => {
	var oldRecord = getVisitor(name);
	var history;

複数の条件があり、true となった最初の条件だけを処理したい場合は、以下の構文を使用できます。

if <condition A> <action A> else if <condition B> <action B> else if <condition C> <action C> … else <default action>

else if ステートメントにより、いずれかの条件が true として評価されると、その条件に対応するアクションが行われ、他の条件はスキップされます。

ここで、エントリーに arrived 属性があるかどうかを調べる前に、そのエントリーがすでに存在しているかどうかを調べなければなりません。定義されていない値の属性の有無を調べるのは間違いです。

	// First time we see this person
	if (oldRecord === undefined)    
		history = [];   // No history
		
	// Already logged in	
	else if (oldRecord.arrived !== undefined) 
		return `Error, ${name} is already logged in`;
		

	// Not logged in, already exists
	else history = oldRecord.history;
		
	setVisitor(name, {
		arrived: new Date(Date.now()),
		history: history
	});	
	
	return `OK, ${name} is logged in now`;	
};

テストする

上記のコードは機能するように見えますが、作業を進める前にコードをテストすることが最善策です。テストを目的として、/test の下にあるさまざまなパスは、上述の関数の結果を返します。これを行うために必要な testFunctions は、パスとそれぞれのパスに関連付けられた関数がリストアップされたテーブルです。このテーブル内の関数はパラメーターを 1 つも取らずに、ユーザーに送信する情報を返します。パラメーターを必要とする関数 (logIn など) のテストを可能にするために、テーブル内では該当する関数が、必要なパラメーターを提供する定義の中にラップされています。このようにすることで、このテーブルを使用するコードではパラメーター値を指定する必要がなくなります。

var testFunctions = [

最初の数行は、パラメーターを受け取らない関数の関数名です。これらの関数をラップする必要はありません。

	{path: "visitorNames", func: visitorNames},	
	{path: "currentVisitorNames", func: currentVisitorNames},	
	{path: "nonCurrentVisitorNames", func: nonCurrentVisitorNames},		
	{path: "currentVisitorList", func: currentVisitorList},		
	{path: "currentVisitors", func: currentVisitors},

/test/visitors パスは、実際に関数の出力を表示するのではなく、変数を表示します。ただし、このテーブルを使用するコードで期待するのは関数であるため、関数内にこのパスをラップします。

{path: "visitors", func: () => visitors},

logIn 関数と logOut 関数はパラメーターとしてユーザー名を取ります。ここで必要な単純なテストを実行するために、一定のユーザー名を使用します。

	{path: "logIn", func: () => logIn("Avimelech ben-Gideon")},
	{path: "logOut", func: () => logOut("Avimelech ben-Gideon")}	
];

以下のコードを使用してハンドラーを登録します。このコードはパスの先頭に /test/ を追加し、テーブル内の関数を呼び出す関数を作成した後、そのレスポンスをブラウザーに送信します。

testFunctions.map((item) => 
	app.get(
		`/test/${item.path}`, 
		/* @callback */ function(req, res) {
			res.send(item.func());
		}
	)
);

実際にアプリケーションをテストするには、/test/logOut にアクセスし、実在しない Avimelech ben-Gideon アカウントをログアウトして、エラー・コードを確認します。次に、/test/logIn に 2 回アクセスします。最初のログイン試行は成功し、2 回目の試行は失敗するはずです。続いて /test/logOut に 2 回アクセスして、最初のログアウト試行は成功し、2 回目の試行は失敗することを確認します。以上の手順を行った後、さらに何回かログインログアウトを繰り返し、/test/visitors にアクセスして訪問者記録の JSON データを確認してください。JSON データを確認するのに最も簡単な方法は、このリンク先にある JSON フォーマッターにアクセスすることです。この JSON フォーマッターは URL を受け取るので、そこで訪問者記録の URL を指定します。最後に、/test/currentVisitorNames など、他の関数の URL にアクセスして、正しい情報が表示されることを確認します。

サンプル・アプリケーション上でこれらのパスにアクセスする場合、この記事を通じた他の読者の操作による情報が大量に表示される可能性があることに注意してください。

ユーザー・インターフェース

app.js 内のコードを、このリンク先のコードで置き換えてください。その上でアプリケーション (またはサンプル・アプリケーション) 上で /visitors にアクセスすると、完全な訪問者リストが表示されます。現在の訪問者のリストを表示するには、/currentVisitors にアクセスします。ユーザーをログインするには /login、ログアウトするには /logout にアクセスします。/ または /index.html にアクセスすると、ユーザー・インターフェース全体を確認できます。

以前と違う点は、まず、以下のように Eslint に対し、未使用のパラメーターを無視するよう指示していることです。その理由は、/* callback */ ディレクティブにあります (この関数呼び出しについてのみ、未使用のパラメーターを無視します)。

/*eslint-disable no-unused-params */

訪問者がオフィス内に滞在した時間がわかると便利ですが、5000 秒といった数値では簡単に解釈することができません。そこで、以下の関数によってミリ秒単位の時間間隔を読みやすい文字列に変換します。入力がミリ秒単位となっているのは、時間に関連する JavaScript 関数の大部分では、標準的な単位としてミリ秒を使用するためです。

// Given a time difference in miliseconds, return a string 
// with the approximate value
var tdiffToString = (msec) => {
	var sec = msec/1000;

数値が 60 秒未満の場合は、適切なラベルを付けた秒数を返します。複数であるかどうかによって「s」を追加するには、3 項演算子を使用します。この 3 項演算子は 3 つのパラメーターを取ります。最初のパラメーター (2 秒未満) が true であれば、2 番目のパラメーター (空の文字列) を返します。最初のパラメーターが false の場合、複数形を使用する必要があることを意味するので、演算子は複数形を表す 3 番目のパラメーター「s」を返します。

	if (sec < 60)
		return sec + " second" + (sec < 2 ? "" : "s");

秒数が 60 秒未満であれば、関数はすでに戻っています。数値が 60 秒以上 3600 秒 (1 時間) 以下の場合は、その時間間隔を分単位で返す必要があります。そのために Math.floor を使用していることに注目してください。5.23544 分のような値を報告しないようにするためには、こうする必要があります。

	if (sec < 3600)
		return Math.floor(sec/60) + " minute" + (sec < 60*2 ? "" : "s");

関数の残りの部分 (時間数と日数) も同じように動作します。

…
};

以下に、HTML を生成する最初の関数を示します。ユーザー・インターフェースには HTML テーブル形式で履歴 (到着時間、退出時間、滞在時間) が表示されます。この関数が、このテーブルの行を生成します。

// Given a history entry (arrived and left times), create a table row with 
// that information
var histEntryToRow = (entry) => {
	return `<tr>
		<td>${entry.arrived}</td>
		<td>${entry.left}</td>
		<td>${tdiffToString(entry.left-entry.arrived)}</td>
		</tr>`;
};

以下に示す関数は、テーブル全体を生成します (テーブルに生成するアイテムがない場合は、空の文字列を返すだけです)。

// Given a history, create a table with it
var histToTable = (history) => {
	if (history === undefined)
		return "";
		
	if (history.length === 0)
		return "";

すべてのユーザーが履歴を確認したいとは限りません。そのため、details タグで履歴を囲むことによって、ユーザーが自分で開くまでは履歴を非表示にします。また、style 属性を使用して履歴テーブルの背景色を指定し、訪問者情報の残りの部分 (同じくテーブル形式で表示されます) と区別されるようにしています。

	return `<details>
		<table border style="background-color: yellow">
			<tr>
				<th>
					Arrived
				</th>
				<th>
					Left
				</th>
				<th>
					Time here
				</th>
			</tr>
			${history.map(histEntryToRow).reduce((a, b) => a+b)}
		</table>
	</details>`;
};

次の userToRow 関数と usersToTable 関数の 2 つは、履歴テーブルを作成した方法とほとんど同じように動作するため、特に説明する点はありません。続いて、visitorsHTMLcurrentVisitorsHTML が見出しを追加して、適切な user 名リストを使用してテーブルを作成します。これら 2 つの関数も非常によく似ているため、どちらか一方を確認すれば、それで十分です。

var visitorsHTML = () => {
	return `
				<h2>Current Visitor List</h2>
					${usersToTable(visitorNames())}
		`;
};

以下に示す関数で、ログイン・フォームを作成します。この関数は HTML フォームと GET メソッドを使用します。つまり、パラメーター (この場合、パラメーターは user だけです) は URL にエンコードされることになります。login 属性はハンドラーのパスを指定します。このハンドラーについては、記事の後のほうで取り上げます。

var loginForm = () => {
	return `<h2>Log in a visitor</h2>
			<form method="get" action="login">
				Visitor to log in: <input type="text" name="user">
			</form>`;
};

ログイン・フォームとログアウト・フォームの間には、大きな違いが 1 つあります。どの訪問者がログインされるかを事前に知る手段はありません。さらに、まったく知らない人物が訪ねてくる可能性もあります。一方、ログアウトできる訪問者は、現在ログインしている訪問者だけです。したがって、ユーザーにログイン中の訪問者のリストを表示して、そこからログアウトする訪問者を選択できるようにすることができます。

var logoutForm = () => {
	if (currentVisitorNames().length === 0) 
		return "No users to log out";
	
	return `
		<h2>Log out a visitor</h2>
		<ul>
			${currentVisitorNames()

ここでは入力フィールドを表示するフォームを作成するのではなく、リンクを使用することにしました。リンクを使用して GET フォームをシミュレーションできるわけは、GET フォームがそのパラメーターを返す仕組みにあります。GET フォームからは、通常の URL (http://<ホスト名>/<パス>) の後に疑問符が続き、その後にアンパサンド (&) で区切った <パラメーター>=<値> のペアが続くという形でパラメーターが返されます。値には、URL で特別な意味を持つ文字 (アンパサンドなど) が含まれている可能性があるため、値はエスケープされます。その役目を果たすのが、encodeURI 関数です。

				.map(name => `<li> 
					<a href="logout?user=${encodeURI(name)}">${name}</a> 
					</li>`)
				.reduce((a,b) => a + b)}
		</ul>`;	
};

以下に示す関数は、テキスト (上記の関数によって生成された HTML など) を取り、そのテキストを完全な Web ページに変換します。

var embedInHTML = (str) => {
	return `<html><body>${str}</body></html>`;	
};

以下の 2 つの呼び出しで、見出しとテーブルを作成してページ内に組み込んで返す、適切な関数を呼び出します。

app.get("/visitors", (req, res) => {
	res.send(embedInHTML(visitorsHTML()));
});

app.get("/currentVisitors", (req, res) => {
	res.send(embedInHTML(currentVisitorsHTML()));
});

以下の 2 つの呼び出しで、フォーム・ハンドラーを呼び出します。GET メソッドを使用してフォームに送信されるパラメーターは、req.query 内にあります。ユーザーがいない場合は、フォームを送信します。ユーザーがいる場合は、そのユーザーに対してコマンドを処理します。

app.get("/login", (req, res) => {
	if (req.query.user === undefined)
		res.send(embedInHTML(loginForm()));
	else 
		res.send(logIn(req.query.user));
});
app.get("/logout", (req, res) => {
	if (req.query.user === undefined)
		res.send(embedInHTML(logoutForm()));
	else
		res.send(logOut(req.query.user));
});

以下の app.get 呼び出しによって、アプリケーション内のすべてのものが表示されます。この呼び出しでは、1 つのパスではなく、パスのリストが使用されています。この場合、Express は「このハンドラーをリスト内のすべてのものに適用する」と解釈します。

app.get(["/index.html", "/"], (req, res) => {

app.get(["/index.html", "/"], (req, res) => {
	res.send(embedInHTML(`
		${loginForm()}
		<hr />
		${logoutForm()}
		<hr />
		${currentVisitorsHTML()}
		<hr />
		${visitorsHTML()}
	`));	
});

以下の呼び出しをそのまま維持すると登録済みのハンドラーがオーバーライドされるため、コメントアウトしました。

/*
// serve the files out of ./public as our main files
app.use(express.static(__dirname + '/public'));
*/

データベース

このアプリケーションには「わずかな」問題が 1 つあります。それは、アプリケーションを再起動するたびに、訪問者記録が失われてしまうことです。この問題を解決するために、訪問者記録を Cloudant データベース内に保管します。キーは訪問者名、その値はデータ (履歴と到着日時) です。

データベースを作成する

以下の手順に従って、データベースを作成します。

  1. IBM Cloud コンソールで、ハンバーガー・メニューにある「Data & Analytics (データとアナリティクス)」をクリックします。
  2. 「Create resource (リソースを作成)」をクリックし、「Cloudant NoSQL DB」を選択します。
  3. サービスに「visitor-log」という名前を付けて、「Create (作成)」をクリックします。
  4. サービスが作成されたら、左側のサイドバーにある「Service credentials (サービス資格情報)」をクリックします。
  5. 「New credential (新しい資格情報)」ボタンをクリックします。
  6. 新しい資格情報に「visitor-log」という名前を付けて、「Add (追加)」をクリックします。
  7. 資格情報のリスト内にある「View credentials (資格情報を表示)」をクリックします。「copy/duplicate (コピー/複製)」アイコンをクリックして資格情報をコピーし、テキスト・ファイルに貼り付けます。
  8. 左側のサイドバーで、「Manage (管理)」をクリックします。次に、「LAUNCH (起動)」ボタンをクリックします。
  9. 左側のサイドバーで、「Databases (データベース)」アイコンをクリックします。
    DB アイコン
  10. 右上にある「Create Database (データベースの作成)」をクリックします。
  11. データベースに「visitor-log」という名前を付けます。
  12. アプリケーション開発環境に戻り、package.json ファイルを開きます。
  13. 依存関係のセクションに、Cloudant パッケージ (バージョン 1.7.x) の行を追加します。以上の手順を完了すると、ファイルの内容はこのリンク先のコードのようになります。

データベースを使用する

上記の手順で作成したデータベースを使用するには、app.js 内のコードを、このリンク先のコードで置き換える必要があります。このコードで使用しているデータ定義とユーザー・アプリケーションは前とほとんど同じなので、変わったところはないように見えるでしょう。けれどもこのコードであれば、アプリケーションを再起動しても情報は失われません。

まず、Cloudant 資格情報を設定した変数を作成します。当然、GitHub 内の値は使用できません。資格情報はユーザーごとに異なるためです。また、私の資格情報も公開していません。

var cloudantCred = {
<<
	
	redacted
	
>>
};

Cloudant 資格情報を使用してデータベース管理システムに接続した後、使用するデータベースを指定します。

// Connect to the database
var cloudant = require("cloudant")(cloudantCred.url);
var mydb = cloudant.db.use("visitor-log");

データベース内に情報を保管するには、2 つの方法があります。1 つは、visitors データ構造全体を 1 つのデータベース・エントリー (Cloudant ではドキュメントと呼ばれます) に格納するという方法です。もう 1 つの方法では、訪問者名ごとに個別のドキュメントを使用します。この入門記事では、最初の方法を使用することにします。この方法は明らかに効率性に劣りますが、最大 1 MB のデータに対応できます。この方法の主な利点は、前に開発したアプリケーション・ロジックをほとんど変更せずに使用できることです。したがって、引き続き visitors データ構造を変数として維持できます。

同じ Cloudant データベースが複数のアプリケーションで同時に使用される可能性があるため、アプリケーションが互いに干渉しないよう、 Cloudant データベースではリビジョン・システムを使用しています。すべてのドキュメントには、現在のリビジョンが値として設定された _rev 属性があります。ドキュメントを更新するときは、更新するリビジョンを指定します。ドキュメントを読み取った後に、そのドキュメントに変更が加えられている場合、リビジョンが異なることから更新が失敗します。このことから、現在のリビジョンを格納する変数が必要となります。

var dbRev = "";

以下の関数は、データベース上の訪問者記録を更新します。

var updateVisitors = () => {

更新するデータには常に、_id、キー、実際の値が含まれます。また、通常は _rev (リビジョン) も含まれます。ただし、新しいエントリーを作成する場合は例外です。

	var updateData = {
	    	"_id": "visitors",
        	value: visitors    	
	};
	
	if (dbRev !== "")
		updateData["_rev"] = dbRev;

ハッシュ・テーブルを更新できるようになったら、最新バージョンをデータベースに挿入できます。それには、<データベース>.insert 関数を使用します。

	mydb.insert(updateData,
    	(err, body) => {

挿入が正常に完了すると、レスポンスの本文に新しいリビジョン値が組み込まれます。

    		if (err === undefined)
    			dbRev = body["rev"];

以下の行では、データベースで発生したエラーをログに記録します。ここで使用しているメカニズムについては、この記事で追って説明します。

    		else
    			log += "updateVisitors: " + err + "
"; } ); };

コード内の次の数行では、アプリケーションの起動時に、<データベース>.get 関数を使用して visitors を読み取ります。

// Read the visitors data structure. 
mydb.get("visitors", (err, body) => {

404 エラーを受け取った場合、それはデータベース内に visitors データ構造がまだないことを意味します。その場合は、新しいデータ構造を作成して updateVisitors という名前を付けます。リビジョンを指定せずに database.insert 関数を呼び出した場合は、このエラーを受け取ります。

	// No visitors yet, create it.
    if (err !== null && err.statusCode === 404) {
    	visitors = {"Test user": {arrived: Date.now().valueOf()}};
    	updateVisitors();
	} else {

値がある場合は、その値を使用し、updateVisitors で必要となった場合に備えてリビジョンを保持します。

		visitors = body.value;
		dbRev = body["_rev"];
	}

});

アプリケーションの起動後に訪問者情報を直接変更する唯一の関数は、setVisitor です。この関数は変更をデータベース内に保存するために、updateVisitors を呼び出す必要があります。

var setVisitor = (name, values) => {
	visitors[name] = values;
	updateVisitors();		
};

時刻と日付の表現

もう 1 つ問題があります。Cloudant との通信はテキスト・ベースで行われるため、Cloudant に Date オブジェクトを書き込むと、Cloudant のテキスト表現 (日付、時刻、タイムゾーンからなる文字列) に変換されてしまうことです。その後、情報を再び読み取る際は、Date オブジェクトが文字列として扱われます。日付だけを表示するのであれば、これでも問題はありませんが、ユーザーには各訪問の滞在時間も表示する必要があります。

この問題を解決するには、Date オブジェクトを visitors データ構造内に格納してデータベースに保管するのではなく、エポック (任意の時点。UNIX 系システム上では通常、1970 年 1 月 1 日の真夜中 (GMT) に設定されます) からの経過時間をミリ秒数で保管します。ミリ秒数を保管するには、プログラム内にいくつかの変更を加える必要があります。

まず、現在の日付が出現するすべての場所を Date.now().valueOf() で置き換える必要があります。valueOf() 呼び出しによって、Date オブジェクトをエポックからの経過時間 (ミリ秒数) に変換します。例えば、logout 内では次のように変更します。

	history.unshift({
		arrived: oldRecord.arrived,
		left: Date.now().valueOf()
	});

日付スタンプと時刻スタンプを表示する関数にも変更が必要です。ミリ秒単位の時刻を文字列に変換するには、新しい Date オブジェクトを作成し、ミリ秒単位の時刻を histEntryToRow という 1 つの関数の中に含めます。

// Given a history entry (arrived and left times), create a table row 
// with that information
var histEntryToRow = (entry) => {
	return `<tr>
		<td>${new Date(entry.arrived)}</td>
		<td>${new Date(entry.left)}</td>
		<td>${tdiffToString(entry.left-entry.arrived)}</td>
		</tr>`;
		
		// The Date need to be new, otherwise we are just 
// modifying the same object and all dates in
		// the history table are the same.
};

デバッグ用のロギング

このアプリケーションの開発中にデータベースの更新が失敗することがあったので、その理由を調べる必要に迫られました。そこで、log 文字列を作成し、更新が失敗するたびに、この文字列の後にログを追加しました。この手法は、Web アプリケーション内でも役立ちます (本番データを使用する前に必ず無効にするか、シークレットが要求されるようにしてください)。

need to be new, otherwise we are just 
var log = "";


var updateVisitors = () => {
	
...
		
	mydb.insert(updateData,
    	(err, body) => {
    		if (err === undefined)
    			dbRev = body["rev"];
    		else

ログに追加する際は、末尾に <br/> を付加します。この文字列はブラウザーによって取得され、デフォルトで HTML として解釈されます。

log += "updateVisitors: " + err + "
"; } ); };

ログにアクセスするには、/log パスにアクセスします。

app.get("/log", (req, res) => {
	res.send(log);
});

まとめ

これで皆さんは、IBM Cloud 上で単純な Node.js Web アプリケーションを作成し、データを Cloudant 内に保管できるようになったはずです。この Web アプリケーションには、視覚的に魅力的でないという主な問題があります。また、変更するたびにページ全体を更新しなければならず、しかも、プレーンな HTML です。

このシリーズの次回の記事で、Bootstrap のテーマを使用して見栄えの良い Web アプリケーションに変身させる方法、そして AngularJS ライブラリーを使用して応答性を向上させる方法を説明します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing, セキュリティ
ArticleID=1012673
ArticleTitle=IBM Cloud Node.js アプリケーション入門, 第 1 回: Node.js を使用して受付用訪問者記録アプリケーションを作成する
publish-date=10112018