M.Hiroi's Home Page

JavaScript Programming

お気楽 Node.js 超入門

[ Home | Light | JavaScript | Node.js ]

テンプレートエンジンの利用

前回は静的コンテンツ用の簡易サーバーを作りました。今回は「テンプレートエンジン」を取り上げます。一般に、Web アプリケーションの開発で用いられるテンプレートエンジンは、テンプレート (template, 雛型) と呼ばれるファイルとデータを合成して、Web ページやその一部を出力するものです。Node.js にはいろいろなテンプレートエンジンがありますが、今回は EJS (Embedded JavaScript templates) を使ってみることにします。

●EJS のインストール

EJS を使うときは、最初に npm でモジュール ejs をインストールします。npm には便利なモジュールがたくさん登録されていて EJS もその一つです。インストール方法や使い方は ejs に書かれています。このページに書かれているように、シェルで次のコマンドを実行します。

npm install ejs

これでカレントディレクトリに node_modules というディレクトリが作成され、そこにモジュール ejs がインストールされます。これを「ローカルインストール」といいます。グローバルな環境にインストールしたい場合は次のように -g オプションを付けます。

npm install -g ejs

これを「グローバルインストール」といいます。一般的なモジュールであれば、ローカルインストールで大丈夫だと思います。インストールしたモジュールは require() で読み込むことができます。

モジュールのアンインストールも簡単です。次のコマンドを入力するだけです。

npm unistall moduleNmae

グローバルインストールしたモジュールをアンインストールする場合はオプション -g を付けてください。

●テンプレートファイルの作成

EJS のテンプレートファイルは、基本的には HTML 形式のテキストファイルです。そこに EJS 用の特別なタグを書き込むことでデータを埋め込んだり、JavaScript のプログラムを実行して HTML 形式のテキストを生成することができます。

EJS でよく使われるタグを以下に示します。

  1. <%= expr %>
  2. <%- expr %>
  3. <% script %>

タグは <% と %> で囲みます。1, 2 は JavaScript の式 expr を評価して、タグをその値に置き換えます。このとき、1 は値をエスケープ処理しますが、2 はエスケープ処理を行いません。3 は script を JavaScript のプログラムとして実行します。if, for, while などの制御構造を使うことができます。

簡単な例を示しましょう。

リスト : テンプレートファイル (index.ejs)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>お気楽 Node.js 超入門</title>
</head>
<body>
<h1><%= title %></h1>
<p><%- message %></p>
<% for (let i = 0; i < cnt; i++) { %>
<p>Good Job!</p>
<% } %>
</body>
</html>

テンプレートファイルの拡張子は .ejs としました。タグ <%= title %> は変数 title の値に、タグ <%- message %> は変数 message の値に置き換わります。タグ <% for (...) %> は JavaScript の for 文を実行します。左カッコ { でタグを閉じて、右カッコ } は別のタグに記述します。このタグの間の処理が cnt 回繰り返されます。つまり、<@>Good Job!</p> が cnt 回出力されます。変数 title, message, cnt はテンプレートファイルを HTML 形式のテキストに変換するメソッド render() で指定します。

●HTML テキストの生成

次はサーバー側のプログラムを作りましょう。基本的にはテンプレートファイル index.ejs を読み込み、それを EJS のメソッド render() で処理して、その結果をブラウザに送信します。プログラムは次のようになります。

リスト : EJS の使用例

const http = require('http'),
      fs = require('fs'),
      ejs = require('ejs'),
      template = fs.readFileSync(__dirname + '/public_html/index.ejs', 'utf-8'),
      server = http.createServer();

server.on('request', (req, res) => {
  if (req.url == '/') {
    let data = ejs.render(template, {
      title: "お気楽 Node.js 超入門",
      message: "<b>hello, EJS!!</b>",
      cnt: Math.floor(Math.random() * 5)
    });
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(data);
  } else {
    res.writeHead(404, {'Content-Type': 'text/plain'});
    res.write('not found');
  }
  res.end();
});
server.listen(1337, 'localhost');
console.log("server listening...");

require('ejs') でモジュール ejs を読み込みます。テンプレートファイルは最初に読み込んでおくと簡単です。この処理を readFileSync() で行います。このメソッドは同期処理 (ブロッキング) でファイルを読み込むので、コールバック関数は必要ありません。この段階ではサーバーとしてまだ動作していないので、同期処理でも問題ありません。

次に、リクエストに対応するコールバック関数の中で、メソッド ejs.render() を呼び出します。第 1 引数がテンプレートで、第 2 引数に変数と値を格納したオブジェクトを渡します。message のタグは <%- なのでエスケープ処理はされません。したがって、b タグで文字は太字で表示されるはずです。cnt は乱数で 0 から 4 までの値を生成してセットします。これで、アクセスするたびに Good Job! の表示を変化させることができます。興味のある方は実際に動かしてみてください。

●フォーム

HTML のフォーム (form) 要素は、ブラウザ上でユーザーがデータを入力し、それをサーバーに送るための基本的な仕組みです。フォームはタグ <form> と </form> の間に、テキスト入力ボックス、ボタン、チェックボタン、プルダウンメニューなど、データを入力するための HTML 要素を配置します。そして、'Submit' ボタンを押すと、ユーザーが入力したデータをサーバーに送信します。サーバーは送られてきたデータを処理して、その結果をブラウザに返します。

一般に、サーバー側の処理は CGI プログラムで行われますが、Node.js では処理を JavaScript で記述することができます。最初にフォームの基本を簡単に説明します。

フォームにはいろいろな属性が用意されていますが、基本的には次に示す 2 つの属性を設定します。

action を省略すると、送信先はフォームを表示している Web ページの URL になります。method には get と post の 2 種類があります。get は action で指定した URL の後ろに ? を付けて、その後ろに入力データを付加します。post は action で指定した URL のサーバーに接続し、そのあとで入力データを送信します。どちらの方法でも入力データはクエリ文字列に変換してから送信されます。

使い分けの基準ですが、簡単に言うと入力データが少ないときには get を、入力データが多いときには post を使うとよいようです。

データを入力する HTML 要素には input, textarea, select などがありますが、ここでは input を取り上げることにします。input には多くの機能があり、属性 type で指定します。type で指定できる主な種類を下表に示します。

表 : 属性 type の種類
機能
text1 行のテキストボックスを作る
passwordパスワード入力ボックスを作る
checkboxチェックボックスを作る
radioラジオボタンを作る
file送信するファイルを選択する
hidden隠しデータを定義する
button汎用のボタンを作る
submit送信ボタンを作る
resetリセットボタンを作る

入力データを区別するため、input などの入力フォームには属性 name で名前を付けます。サーバーには 名前=値 の形式でデータが送信されます。text は属性 size でテキストボックスに表示される文字数を、属性 maxlenght で入力文字数の最大長を指定することができます。また、属性 value 初期値を指定することもできます。

checkbox と radio は同じ名前のフォームをいくつても作ることができます。そして、checkbox はチェックされたボタンの値がサーバーに送信されます。ラジオボタンは同じ名前のボタンを一つだけ選択することができます。なお、これらのボタンは属性 value で値を設定しておく必要があります。

●簡単な掲示板

それでは簡単な例として、名前、性別、コメントを投稿する掲示板を作ってみましょう。最初に EJS 用のテンプレートファイルを作ります。次のリストを見てください。

リスト : テンプレートファイル (bbs.ejs)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>BBS</title>
</head>
<body>
<form method="post" action="/send">
<label>名前: <input type="text" name="name"></label><br>
<label>性別:
  <input type="radio" checked name="sex" value="男"> 男
  <input type="radio" name="sex" value="女"> 女 
</label><br>
<label>コメント: <input type="text" name="comment" size=60></label><br>
<input type="submit" value="投稿">
</form>
<hr>
<% for (let data of posts) { %>
日付: <%= data.date %><br>
名前: <%= data.name %><br>
性別: <%= data.sex %><br>
コメント: <%= data.comment %><br>
<hr>
<% } %></body>
</html>

フォームの属性 method に post を、action に /send を指定します。サーバーの URL が localhost:1337 とすると、データの送信先は localhost:1337/send になります。あとは input タグで、名前、性別、コメントを入力するフォームを作ります。その後ろに投稿されたデータを表示します。データはサーバー内の配列 posts に格納します。データを保存していないので、サーバーを再起動すると今まで投稿されたデータは消失します。実用的な掲示板を作る場合、データの保存 (永続化) の処理が不可欠になります。ご注意くださいませ。

次はサーバー側のプログラムを作ります。フォームから送信されたデータを受け取る処理がポイントになります。次のリストを見てください。

リスト : 一行簡易掲示板

const http = require('http'),
      fs = require('fs'),
      ejs = require('ejs'),
      url = require('url'),
      qs = require('querystring'),
      template = fs.readFileSync(__dirname + '/public_html/bbs.ejs', 'utf-8'),
      posts = [],
      server = http.createServer();

function renderForm(posts, res) {
  const data = ejs.render(template, {
      posts: posts
  });
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.write(data);
  res.end();
}

function insertData(query, posts, res) {
  query.date = (new Date()).toLocaleString();
  posts.push(query);
  renderForm(posts, res);
}

server.on('request', (req, res) => {
  const u = url.parse(req.url, true);
  if (u.pathname == '/send') {
    if (req.method == 'POST') {
      let data = "";
      req.on('data', x => data += x);
      req.on('end', () => insertData(qs.parse(data), posts, res));
    } else {
      insertData(u.query, posts, res);
    }
  } else {
    renderForm(posts, res);
  }
});
server.listen(1337, 'localhost');
console.log("server listening...");

'request' のコールバック関数で、URL を url.parse() で解析して、u.pathname が /send ならばフォームからの送信です。この処理はあとで説明します。そうでなければ、関数 renderForm() を呼び出して、bbs.ejs を HTML 形式のページに変換してブラウザに返します。引数 posts が投稿データを格納した配列です。ejs.render() を呼び出すときは { posts: posts } を渡します。これで投稿されたデータを Web ページに表示することができます。

URL が /send の場合、プロパティ method で HTTP メソッドの種別をチェックします。'POST' のときはデータの受信処理を行います。データを受信したとき、イベント 'readable' または 'data' が発生するので、req.on() でコールバック関数を登録します。イベントが 'data' の場合、受信したデータはコールバック関数の引数 x に渡されます。一度にすべてのデータを受信できるとは限らないので、データを格納する変数 data を用意しておいて、data += x のようにデータを追加していきます。

データの受信が完了したときに発生するイベントが 'end' です。qs.parse() でクエリ文字列を解析し、その結果を関数 insertData() に渡します。ここで現在の日付を query に追加してから、それを posts の末尾に追加します。あとは、renderForm() を呼び出して Web ページを生成します。HTTP メソッドが POST でなければ GET の処理を行います。これは簡単で、送信されたデータは url.parse() で解析済みなので、u.query を insertData() に渡して実行するだけです。

これでプログラムは完成です。興味のある方は実際に動かしてみてください。

●参考文献, URL

  1. 初めての HTML フォーム - ウェブ開発を学ぶ | MDN
  2. フォームデータを送信する - ウェブ開発を学ぶ | MDN

Node.js DE SQLite

SQLite は D. Richard Hipp 氏が開発しているパブリックドメインな軽量のリレーショナルデータベース管理システム (RDBMS) です。一般的な RDBMS (たとえば MySQL など) はサーバとして動作させるのですが、SQLite はアプリケーションに組み込んで使用することができます。中小規模なシステムであれば、SQLite でも十分なパフォーマンスが得られるようです。SQLite の基本は拙作のページ Linux Programming お気楽 SQLite 超入門 で簡単に説明しています。よろしければお読みくださいませ。

SQLite は Perl, Python, Ruby などのスクリプト言語からでも簡単に利用することができます。もちろん、Node.js でも SQLite を利用することができます。本稿では Node.js から SQLite にアクセスする基本的な方法について簡単に説明します。

●SQLite のインストール

Node.js から SQLite にアクセスする場合、モジュール sqlite3 が必要になります。npm を使って簡単にインストールすることができます。

npm install sqlite3

これで SQLite にアクセスすることができます。

●接続と切断

Node.js で SQLite を使用する場合、最初に require('sqlite3') でモジュール sqlite3 をロードしてください。データベースの接続は new で sqlite3.Database クラスのオブジェクトを生成します。切断はメソッド close() を使います。次の例を見てください。

リスト : 接続と切断 (test01.js)

const sqlite3 = require('sqlite3'),
      db = new sqlite3.Database('sample.sqlite');

// SQL 文を逐次処理する
db.serialize();

db.close();
console.log("OK");

Database のコンストラクタにはデータベース名を指定します。データベース名と一致するファイルが見つからない場合、新しいファイルが生成されます。同名のファイルがある場合はそれをデータベースとして使用します。コンストラクタの返り値 (データベースオブジェクト) はデータベースの操作に必要なので、変数 db に格納しておきます。

メソッド db.serialize() は、SQL 文を逐次処理するための命令です。これ以降の SQL 文は並行に実行されることはありません。逆に、メソッド db.parallel() は SQL 文を並行処理するための命令です。これらのメソッドは引数に関数を渡して、その処理を実行することもできます。切断はメソッド close() を呼び出すだけです。

●テーブルの作成

Node.js で結果を返さない SQL 文を実行するには、データベースオブジェクトのメソッド run() または exec() を使います。

db.run(sql, [parm, ...], [callback])
db.exec(sql, [callback])

run() と exec() は、第 1 引数に渡された文字列 sql を SQL 文として実行します。run() は SQL 文にパラメータ ? を含めることができます。これを「プレスホルダー」といいます。パラメータに対応する値は run() の第 2 引数以降に渡します。複数の値を配列に格納して渡すこともできます。

Node.js の SQLite では、? のかわりに $名前 を使うこともできます。この場合、run() の第 2 引数に $名前 と 値 を格納したオブジェクト { $名前: 値, ... } を渡します。run() と exec() の最後の引数はエラーが発生したときに実行するコールバック関数です。第 1 引数にエラー情報が渡されます。コールバック関数は省略してもかまいません。

なお、同じようなクエリ (問い合わせ) を何度も繰り返すと、RDBMS では同じような解析処理を繰り返すというオーバーヘッドが発生します。メソッド prepare() を使うと、このようなオーバーヘッドを避けることができます。

db.prepare(sql, [param, ...], [callback]) => statement
statement.run([param, ...], [callback])

prepare() はパラメータを含んだ SQL 文をプリコンパイルします。prepare() の返り値をステートメントハンドルと呼びます。プリコンパイルした SQL 文を実行するにはメソッド run() を使います。パラメータ parm の与え方は db.run() と同じです。

それでは簡単な例題として、次に示すテーブルを作成してみましょう。

テーブル名 : person
idnameagesexemail
1Foo50malefoo@yahoo.co.jp
2Bar35femalebar@yahoo.co.jp
3Baz40malebaz@yahoo.co.jp
4Oops30femaleoops@yahoo.co.jp
リスト : テーブルの作成 (test02.js)

const sqlite3 = require('sqlite3'),
      db = new sqlite3.Database('sample.sqlite'),
      data = [[1, 'Foo', 50, 'male', 'foo@yahoo.co.jp'],
              [2, 'Bar', 35, 'female', 'bar@yahoo.co.jp'],
              [3, 'Baz', 40, 'male', 'baz@yahoo.co.jp'],
              [4, 'Oops', 30, 'female', 'oops@yahoo.co.jp']];

db.serialize();
db.run("create table person (id integer, name text, age integer, sex text, email text)");

const sth = db.prepare("insert into person (id, name, age, sex, email) values (?,?,?,?,?)");
for (let xs of data) {
  sth.run(xs);
}
sth.finalize();

db.close();
console.log("OK");

ステートメントハンドル sth の使用が終了したらメソッド finalize() で sth を廃棄します。これでデータベース sample.sqlite にデータを追加することができます。

●データの抽出

select 文のように結果を返す SQL 文は、次に示すメソッドを使って実行します。

db.get(sql, [parm, ...], [callback])
db.all(sql, [parm, ...], [callback])
db.each(sql, [parm, ...], [callback])

callback の第 1 引数にはエラー情報、第 2 引数に結果が渡されます。get() は SQL 文を実行して、その結果の先頭行だけを取得します。all() はすべての結果を格納した配列を受け取ります。each() は結果を 1 行ずつ取り出して callback に渡します。なお、prepare() で SQL 文をプリコンパイルした場合は、ステートメントハンドルのメソッド get(), all(), each() を使ってください。

簡単な例を示しましょう。

リスト : データの抽出 (test03.js)

const sqlite3 = require('sqlite3'),
      db = new sqlite3.Database('sample.sqlite');

db.serialize();

db.get("select * from person", (err, data) => {
  console.log(data);
});    

db.all("select * from person", (err, data) => {
  console.log(data);
});    

db.each("select * from person", (err, data) => {
  console.log(data);
});    

db.close();
console.log("OK");
C>node test03.js
OK
{ id: 1,
  name: 'Foo',
  age: 50,
  sex: 'male',
  email: 'foo@yahoo.co.jp' }
[ { id: 1,
    name: 'Foo',
    age: 50,
    sex: 'male',
    email: 'foo@yahoo.co.jp' },
  { id: 2,
    name: 'Bar',
    age: 35,
    sex: 'female',
    email: 'bar@yahoo.co.jp' },
  { id: 3,
    name: 'Baz',
    age: 40,
    sex: 'male',
    email: 'baz@yahoo.co.jp' },   
  { id: 4,
    name: 'Oops',
    age: 30,
    sex: 'female',
    email: 'oops@yahoo.co.jp' } ]
{ id: 1,
  name: 'Foo',
  age: 50,
  sex: 'male',
  email: 'foo@yahoo.co.jp' }
{ id: 2,
  name: 'Bar',
  age: 35,
  sex: 'female',
  email: 'bar@yahoo.co.jp' }
{ id: 3,
  name: 'Baz',
  age: 40,
  sex: 'male',
  email: 'baz@yahoo.co.jp' }
{ id: 4,
  name: 'Oops',
  age: 30,
  sex: 'female',
  email: 'oops@yahoo.co.jp' }   

●トランザクションとオートコミット

「トランザクション (transaction)」は処理とか取引という意味ですが、SQL では「関連した複数の処理を一つの処理にまとめたもの」をトランザクションといいます。SQL 文では、BEGIN でトランザクションを開始します。この場合、データベースの変更を伴う行う作業 (insert, update など) では、そのつどデータベースに変更が反映されるのではありません。トランザクションを終了して実際にデータベースの変更を行う SQL 文が COMMIT (コミット) です。

Node.js で SQLite を操作する場合、オートコミット (AutoCommit) モードが設定されていると、トランザクションやコミットを明示的に指定しなくても、データベースを操作することができます。insert や update などの SQL 文を実行するとき、SQLite は暗黙のうちにトランザクションを開始します。さらに、オートコミットモードが有効だと、SQL 文が終了したとき、SQLite は自動的にコミットしてくれます。

これはとても便利な機能なのですが、コミットはけっこう時間がかかる処理なので、オートコミットモードのままでたくさんのデータをいっきに挿入しようとすると、時間がとてもかかるのです。次の例を見てください。

リスト : オートコミットモードでの挿入 (test04.js)

const sqlite3 = require('sqlite3'),
      db = new sqlite3.Database('sample02.sqlite');

db.serialize();

// テーブル作成
db.run('create table test (name text, val real)');
db.run('create index name_idx on test(name)');

// 時間計測
console.time('insert');
process.on('exit', code => console.timeEnd('insert'));

// データ挿入
const sth = db.prepare('insert into test (name, val) values (?, ?)');
for (let n = 1; n <= 1000; n++) {
  sth.run("test" + n, Math.random());
}
sth.finalize();

db.close();
console.log("OK");

Node.js で時間を計測する場合、console.time(label) と console.timeEnd(label) を使うと簡単です。console.time(label) はタイマーを生成し、console.timeEnd(label) は console.time(label) からの経過時間を計測します。ただし、データベースの処理は非同期で行われるため、メインプログラムはすぐに終了します。このため、プログラムの最後に console.timeEnd() を実行しても時間を計測することはできません。

そこで、Node.js が終了するときに発生するイベント 'exit' で、console.timeEnd() を実行することにします。Node.js にはプロセスを表すオブジェクトが用意されていて、グローバル変数 process に格納されています。process.on('exit', code => ...) でコールバック関数を登録することができます。引数 code には終了コードが渡されます。これで、データ挿入処理の時間を計測 [*1] することができます。

C>node test04.js
OK
insert: 93477.554ms

実行環境 : Windows 10, Intel Core i5-6200U 2.3 GHz

sample02.sqlite に TEXT と REAL を 1000 件挿入したところ、実行時間は 1 分 33 秒もかかりました。トランザクションを明示的に指定すると、もっと高速にデータを挿入することができます。

リスト : トランザクションの利用 (test05.js)

const sqlite3 = require('sqlite3'),
      db = new sqlite3.Database('sample03.sqlite');

db.serialize();

// テーブル作成
db.run('create table test (name text, val real)');
db.run('create index name_idx on test(name)');

// 時間計測
console.time('insert');
process.on('exit', code => console.timeEnd('insert'));

// トランザクション開始
db.exec('begin transaction');

// データ挿入
const sth = db.prepare('insert into test (name, val) values (?, ?)');
for (let n = 1; n <= 1000; n++) {
  sth.run("test" + n, Math.random());
}
sth.finalize();

// トランザクション終了
db.exec('commit');

db.close();
console.log("OK");

db.exec('begin transaction') でトランザクションを開始し、db.exec('commit') でトランザクションを終了します。実行結果は次のようになりました。

C>node test05.js
OK
insert: 352.986ms

実行環境 : Windows 10, Intel Core i5-6200U 2.3 GHz

1000 件のデータを 0.35 秒で挿入することができました。

-- note --------
[*1] Windows の場合、プログラムの時間計測は PowerShell のコマンド Measure-Command {...} を使うと簡単です。
PS C> Measure-Command {node test05.js}


Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 531
Ticks             : 5314073
TotalDays         : 6.1505474537037E-06
TotalHours        : 0.000147613138888889
TotalMinutes      : 0.00885678833333333
TotalSeconds      : 0.5314073
TotalMilliseconds : 531.4073

●メモリ上にデータベースを作成する

SQLite はデータベース名に :memory: を指定すると、メモリ上にデータベースを作成することができます。簡単な実行例として、test04.js のデータベースをメモリに変更してみましょう。プログラムは次のようになります。

リスト : インメモリデータベース (test06.js)

const sqlite3 = require('sqlite3'),
      db = new sqlite3.Database(':memory:');

db.serialize();

// テーブル作成
db.run('create table test (name text, val real)');
db.run('create index name_idx on test(name)');

// 時間計測
console.time('insert');
process.on('exit', code => console.timeEnd('insert'));

// データ挿入
const sth = db.prepare('insert into test (name, val) values (?, ?)');
for (let n = 1; n <= 1000; n++) {
  sth.run("test" + n, Math.random());
}
sth.finalize();

db.close();
console.log("OK");

ファイル名を :memory: に変更しただけです。実行結果は次のようになりました。

C>node test06.js
OK
insert: 23.345ms

実行環境 : Windows 10, Intel Core i5-6200U 2.3 GHz

オートコミットしているはずですが、とても高速ですね。インメモリで利用する場合、オートコミットのままでもよいかもしれません。興味のある方はいろいろ試してみてください。

●参考 URL

  1. sqlite3 - npm, (本家)
  2. API documentation, (リファレンス)
  3. Node.jsでSQLite3を使用する, (情報アイランドさん)

Copyright (C) 2017 Makoto Hiroi
All rights reserved.

[ Home | Light | JavaScript | Node.js ]