今回は簡単な例題として、名前とコメントを投稿する簡易掲示板を作ってみましょう。
プログラムの構成は次のようになります。
bbs.js public_html/ bbs1.ejs bbs.sqlite
カレントディレクトリにサーバー本体のプログラム bbs.js とディレクトリ public_html を用意します。public_html の中にはテンプレートファイル bbs1.ejs とデータベース bbs.sqlite を格納します。bbs.sqlite はあらかじめ用意しておく必要はありません。あとは、node bbs.js でサーバーを起動し、ブラウザで URL localhost:1337 を入力します。
テンプレートファイルは次のようになります。
リスト : 簡易掲示板 (bbs1.ejs) <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>簡易掲示板</title> <style> body { margin-left: 2em; margin-right: 2em; } .bbs { font-family: sans-serif; font-size: medium; } </style> </head> <body> <% for (let data of posts) { %> <%= data.id %>. <%= data.name %> - <%= data.date %><br> <pre class="bbs"> <%= data.comment %> </pre> <hr> <% } %> <% if (msg) { %> <p><b><%= msg %></b></p> <% } %> <form method="post"> <label>名前: <input type="text" name="name"></label><br> <label>コメント:<br> <textarea name="comment" rows="6" cols="60"></textarea></label><br> <input type="submit" value="送信"> </form> </body> </html>
名前は input タグで、コメントは textarea タグで入力します。textarea はテキストを改行することができますが、表示するときに p タグを使うとテキストは改行されません。そこで、コメントを表示するときは pre タグで囲むことにします。これで改行されたテキストを表示することができます。
サーバー側のプログラムは次のようになります。
// // bbs.js : 簡易掲示板 (サーバー) // // Copyright (C) 2017 Makoto Hiroi // const http = require('http'), fs = require('fs'), ejs = require('ejs'), qs = require('querystring'), sqlite = require('sqlite3'), template = fs.readFileSync(__dirname + '/public_html/bbs1.ejs', 'utf-8'), db = new sqlite.Database(__dirname + '/public_html/bbs.sqlite'), server = http.createServer(); function renderForm(db, res, msg = null) { db.all('select * from bbs', (err, posts) => { const data = ejs.render(template, { posts: posts, msg: msg }); res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); res.end(); }); } function insertData(query, db, res) { if (query.name != '' && query.comment != '') { db.run('insert into bbs (name, comment, date) values (?, ?, ?)', [query.name, query.comment, (new Date()).toLocaleString()]); renderForm(db, res); } else { renderForm(db, res, "名前とコメントを入力してください"); } } db.serialize(); db.run('create table if not exists bbs (id integer primary key autoincrement, name text, comment text, date text)'); server.on('request', (req, res) => { if (req.method == 'POST') { let data = ""; req.on('data', x => data += x); req.on('end', () => insertData(qs.parse(data), db, res)); } else { renderForm(db, res); } }); server.listen(1337, 'localhost'); console.log("server listening...");
サーバー側では POST のみを受け付けて、関数 insertDate() で名前とコメントが入力されていることをチェックします。どちらも空文字列であればデータベースには登録しません。そして、テンプレートファイル (bbs1.ejs) にメッセージ msg を渡して表示します。
あとは特に難しいところはないと思います。興味のある方は実際に動かしてみてください。
フォームの入力チェックはクライアント側 (ブラウザ) で行うこともできます。データ入力の有無だけでよければ、HTML5 で導入された required 属性を使うと簡単です。
簡単な使用例を示しますので、実際に試してみてください。
リスト : required 属性の使用例 <form method="post" onsubmit="return false"> <label>本文 <input type="text" name="content" required></label> <input type="submit" value="Submit"> </form>
form タグで onsubmit 属性にプログラムを登録すると、submit ボタンを押したときに登録されたプログラムを実行します。ここで false を返すとデータの送信は行われません。JavaScript (DOM) を使って入力データをチェックすることも可能です。これはあとで試してみましょう。
テンプレートファイルは次のようになります。
リスト : テンプレートファイル (bbs2.ejs) <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>簡易掲示板</title> <style> body { margin-left: 2em; margin-right: 2em; } .bbs { font-family: sans-serif; font-size: medium; } </style> </head> <body> <% for (let data of posts) { %> <%= data.id %>. <%= data.name %> - <%= data.date %><br> <pre class="bbs"> <%= data.comment %> </pre> <hr> <% } %> <% if (msg) { %> <p><b><%= msg %></b></p> <% } %> <form method="post"> <label>名前: <input type="text" name="name" required></label><br> <label>コメント:<br> <textarea name="comment" rows="6" cols="60" required></textarea></label><br> <input type="submit" value="送信"> </form> </body> </html>
input タグと textarea タグに属性 required を追加します。これでこのフォームの入力は必須になり、空のまま submit すると、ブラウザ側で入力を促すエラーメッセージが表示されます。
このほかにも HTML5 には便利な機能がたくさん追加されています。興味のある方は 参考 URL をお読みくださいませ。
次は、JavaScript (DOM) を使って入力データをチェックしてみましょう。プログラムは次のようになります。
リスト : テンプレートファイル (bbs3.ejs) <!DOCTYPE html> <html lang="ja"> ・・・ 省略 ・・・ <form method="post" onsubmit="return check(this)"> <label>名前: <input type="text" name="name"></label><br> <label>コメント:<br> <textarea name="comment" rows="6" cols="60"></textarea></label><br> <input type="submit" value="送信"> </form> <script> function check(e) { if (!e.name.value || !e.comment.value) { alert("名前とコメントを入力してください"); return false; } return true; } </script> </body> </html>
form タグの onsubmit に "return check(this)" を登録します。submit ボタンを押すと関数 check() が呼び出されて、その返り値が false ならばデータの送信を行いません。引数 this には form 要素 (HTMLFormElement のオブジェクト) が渡されます。
form に含まれる要素はその name 属性の値で求めることができます。その入力データは value プロパティで取得することができます。ラジオボタンなど同じ名前を持つ要素が複数ある場合、それらの要素は配列に格納されていることに注意してください。JavaScript の場合、空文字列は偽として扱われるので、e.name.value または e.comment.value が偽であれば alert() で警告メッセージを表示して false を返します。そうでなければ true を返します。
form タグの onsubmit 属性のかわりに、form 要素のプロパティ onsubmit にコールバック関数を登録する方法もあります。プログラムは次のようになります。
リスト : テンプレートファイル (bbs4.ejs) <!DOCTYPE html> <html lang="ja"> ・・・ 省略 ・・・ <form id="form" method="post"> <label>名前: <input type="text" name="name"></label><br> <label>コメント:<br> <textarea name="comment" rows="6" cols="60"></textarea></label><br> <input type="submit" value="送信"> </form> <script> const form = document.getElementById("form"); form.onsubmit = () => { if (!form.name.value || !form.comment.value) { alert("名前とコメントを入力してください"); return false; } return true; }; </script> </body> </html>
form タグに id="form" をセットし、getElementById() で form 要素を取得します。コールバック関数はプロパティ onsubmit に登録します。これでも入力データをチェックすることができます。
最後に、掲示板からキーワードを検索する機能を追加してみましょう。SQLite の場合、デフォルトでは正規表現を使用した検索をサポートしていませんが、like 句または glob 句を使ってデータを検索することができます。
select カラム名 form テーブル名 where 検索カラム like 'キーワード' select カラム名 form テーブル名 where 検索カラム glob 'キーワード'
like 句で文字列を比較するとき、ワイルドカードに % と _ を使用することができます。
% : 任意の 0 文字以上の文字列 _ : 任意の 1 文字
glob 句の場合、bash の Globbing と同様のメタ文字を使用することができます。Globbing については、拙作のページ お気楽 bash 超入門 : グロブ (glob) をお読みくださいませ。今回は like 句を使うことにします。
まずは最初に、検索キーワードを入力するフォームをテンプレートファイルに追加します。プログラムは次のようになります。
リスト : 検索フォームの追加 (bbs5.ejs) <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>簡易掲示板</title> <style> body { margin-left: 2em; margin-right: 2em; } .bbs { font-family: sans-serif; font-size: medium; } </style> </head> <body> <% for (let data of posts) { %> <%= data.id %>. <%= data.name %> - <%= data.date %><br> <pre class="bbs"> <%= data.comment %> </pre> <hr> <% } %> <% if (msg) { %> <p><b><%= msg %></b></p> <% } %> <form method="post"> <input type="hidden" name="cmd" value="send"> <label>名前: <input type="text" name="name" required></label><br> <label>コメント:<br> <textarea name="comment" rows="6" cols="60" required></textarea></label><br> <input type="submit" value="送信"> </form> <hr> <form method="post"> <input type="hidden" name="cmd" value="search"> <label>検索: <input type="radio" name="column" value="name"> 名前 <input type="radio" checked name="column" value="comment"> コメント </label><br> <label>キーワード: <input type="text" name="keyword" required></label><br> <ul> <li>% : 任意の 0 文字以上の文字列 <li>_ : 任意の 1 文字 </ul> <input type="submit" value="検索"> </form> <hr> </body> </html>
投稿と検索を区別するため、input タグの type="hidden" で隠しデータ cmd を設定します。投稿の時は cmd の値を send に、検索のときは search にします。submit ボタンを押すと他のデータと一緒に隠しデータも送信されます。検索は名前とコメントを区別して行います。これをラジオボタンで指定します。あとはキーワードを入力するテキストボックスを用意するだけです。
サーバー側のプログラムは次のようになります。
// // bbs1.js : 簡易掲示板 (検索機能の追加) // // Copyright (C) 2017 Makoto Hiroi // const http = require('http'), fs = require('fs'), ejs = require('ejs'), qs = require('querystring'), sqlite = require('sqlite3'), template = fs.readFileSync(__dirname + '/public_html/bbs5.ejs', 'utf-8'), db = new sqlite.Database(__dirname + '/public_html/bbs.sqlite'), server = http.createServer(); function renderForm(db, res, msg = null) { db.all('select * from bbs', (err, posts) => { const data = ejs.render(template, { posts: posts, msg: msg }); res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); res.end(); }); } function insertData(query, db, res) { if (query.name != '' && query.comment != '') { db.run('insert into bbs (name, comment, date) values (?, ?, ?)', [query.name, query.comment, (new Date()).toLocaleString()]); renderForm(db, res); } else { renderForm(db, res, "名前とコメントを入力してください"); } } function searchData(query, db, res) { db.all(`select * from bbs where ${query.column} like '${query.keyword}'`, (err, posts) => { if (err) { res.writeHead(404, {'Content-Type': 'text/plain'}); res.write(`${err}`); res.end(); return; } const data = ejs.render(template, { posts: posts, msg: "" }); res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); res.end(); }); } db.serialize(); db.run('create table if not exists bbs (id integer primary key autoincrement, name text, comment text, date text)'); server.on('request', (req, res) => { if (req.method == 'POST') { let data = ""; req.on('data', x => data += x); req.on('end', () => { const query = qs.parse(data); if (query.cmd == 'send') { insertData(query, db, res); } else { searchData(query, db, res); } }); } else { renderForm(db, res); } }); server.listen(1337, 'localhost'); console.log("server listening...");
server.on() のコールバック関数でデータの受信が終了したとき、クエリ文字列を解析して cmd が send ならば投稿処理を、そうでなければ検索処理を関数 searchData() で行います。searchData() では SQLite のコマンドをテンプレート文字列 `...` で組み立てます。テンプレート文字列は ECMAScript2015 (ES2015) から導入された機能です。詳しい説明は拙作のページ JavaScript 超入門: テンプレート文字列 をお読みください。
あとは db.all() でコマンドを実行して検索結果を取得します。エラーが発生した場合は、エラーメッセージをブラウザに返します。そうでなければ、取得したデータを ejs.render() で HTML 形式のテキストに変換してブラウザに返すだけです。
これでプログラムは完成です。興味のある方は実際に動かしてみてください。最低限の機能しかないので、いろいろ改造したり、見栄えを良くしてみるのも面白いと思います。