node.js + express + haml.js + mongoose で MongoDB を使った Web アプリ開発
以前(id:fits:20110306)に Sinatra で作成したサンプルの node.js 版を作成してみました。
環境は以下の通りで、Sinatra 版と同等の構成になっています。
- node.js 0.4.0
- Express 1.0.7
- Haml.js 0.5.1
- Mongoose 1.1.24
- MongoDB 1.8.1 rc1
サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20110409/
事前準備
まず、今回使用するパッケージを npm でインストールしておきます。
Mongoose によるモデル定義
Mongoose を使ったモデル定義は以下の通りです。
Ruby の MongoMapper とかに比べると多少面倒です。
- Schema インスタンスでスキーマを定義
- model メソッドに Schema インスタンスを渡してモデルを定義
- 型の指定で Schema インスタンスを使って組み込みドキュメント(コンポジッション)を定義
なお、MongoMapper とかの belongs_to に該当する機能が無さそうだったので、virtual を使った仮想的な属性定義と method を使ったメソッド追加でとりあえず代用してみました。(ただし、処理内容的には全くの別物です)
models/book_models.js (Mongoose モデル定義)
var mongoose = require('mongoose'); //Userスキーマ定義 var UserSchema = new mongoose.Schema({ name: String }); //Commentスキーマ定義 var CommentSchema = new mongoose.Schema({ content: String, created_date: Date, user_id: mongoose.Schema.ObjectId }); //user 仮想属性の定義 CommentSchema.virtual('user') .get(function() { return this['userobj']; }) .set(function(u) { this.set('user_id', u._id) }); //Book スキーマ定義 var BookSchema = new mongoose.Schema({ title: String, isbn: String, //Comment の組み込みドキュメント(コンポジション)定義 comments: [CommentSchema] }); //Book へのメソッド追加 BookSchema.method({ //Comment の関連 User を復元する処理 restoreUser: function(userList) { var ulist = {}; userList.forEach(function(u) { ulist[u._id] = u; }); this.get('comments').forEach(function(c) { c['userobj'] = ulist[c.user_id]; }); } }); //User モデルの定義 mongoose.model('User', UserSchema); //Book モデルの定義 mongoose.model('Book', BookSchema); //接続メソッドの定義 exports.createConnection = function(url) { return mongoose.createConnection(url); };
上記で定義したモデルを使うには、model('モデル名') を実行して変数に割り当てる必要があります。(今回は Express 側の処理で実施しています)
Express による Web サーバー処理
Express による Web サーバー側の処理は以下の通りです。
- POST データを request.body から取得するために app.use(express.bodyDecoder()) を実行
- Haml.js を使うために app.register('.haml', require('hamljs')) を実行
- render 時の拡張子指定を不要にするために app.set('view engine', 'haml') を実行
- Haml.js のテンプレート内で使用する変数は locals に設定する
bodyDecoder を設定しておかないと、POST データを request.body で取得できない点にやられました。
site.js (Express 処理定義)
var express = require('express'); var app = express.createServer(); /* * post でパラメータを取得するのに必要な設定 * bodyDecoder を設定しないと request.body が undefined になり、 * post された値が request.body や param() 等で取得できない */ app.use(express.bodyDecoder()); //haml.js の設定 app.register('.haml', require('hamljs')); //view engine に haml を設定することで render で index.haml のように //拡張子まで書かなくてもよくなります app.set('view engine', 'haml'); //Book, User モデルクラス定義の呼び出し var mongoModel = require('./models/book_models'); //MongoDB への接続設定 var db = mongoModel.createConnection('mongodb://127.0.0.1/book_review'); //Book モデルを取得 var Book = db.model('Book'); //User モデルを取得 var User = db.model('User'); //Top ページ表示 app.get('/', function(req, res) { User.find().asc('name').find(function(err, userList) { Book.find().asc('title').find(function(err, bookList) { bookList.forEach(function(b) { //Comment の関連 User を復元 b.restoreUser(userList); }); res.render('index', { locals: { books: bookList, users: userList, action: '/comments' } }); }); }); }); //Book ページ表示 app.get('/books', function(req, res) { Book.find().asc('title').find(function(err, list) { res.render('book', { locals: { books: list, action: '/books' } }); }); }); //Book 追加処理 app.post('/books', function(req, res) { new Book(req.body).save(function(err) { res.redirect('/books'); }); }); //Comment 追加処理 app.post('/comments', function(req, res) { User.findById(req.body.user_id, function(err, u) { Book.findById(req.body.book_id, function(err, b) { //Comment 追加 b.comments.push({ content: req.body.content, created_date: Date.now(), user: u }); //保存 b.save(function(err) { res.redirect('/'); }); }); }); }); ・・・ //ポート 8081 で Web サーバーを起動 app.listen(8081);
Book や User モデルで引数無しの find を実行していますが、これは asc() を使うためです。(ソート順を find で指定する方法もあります)
find は以下のようなちょっと分かり難い動作になっています。
- find に callback が指定されていれば検索が実行され callback が呼び出されるが、指定されていなければ検索せずに Query を返す
Haml.js によるページの定義
Haml.js の特徴は以下の通りです。
- = で HTML エスケープされる
- != で HTML エスケープされない
- - each で繰り返し処理を定義できる
レイアウト定義は以下のようになります。
body 変数に各々のページ内容が設定されます。(例えば index の場合、index.haml のレンダリング結果が body 変数に格納される事になります)
views/layout.haml (レイアウト定義)
!!! 5 %html %title Express + Haml.js Sample %body != body
body の内容は HTML エスケープさせずにそのまま出力させたいので != を使っています。
Top ページは以下の通りです。
views/index.haml (Topページ定義)
.menu Menu %ul %li %a{ href: "/books" } Books List %li %a{ href: "/users" } Users List .list Book Comments %form.post{ action: action, method: 'post' } %select{ name: 'user_id' } - each u in users %option{ value: u._id } = u.name %select{ name: 'book_id' } - each b in books %option{ value: b._id } = b.title %input{ name: 'content', type: 'text' } %input{ type: 'submit', value: 'Add' } - each b in books %ul %li= b.title %ul - each c in b.comments %li= c.content + ' : ' + c.user.name + " , " + c.created_date
実行
まず、MongoDB を実行しておきます。
MongoDB 実行例
> mongod -dbpath db
次に、node コマンドで site.js を実行します。
Express 実行例
$ node site.js
http://localhost:8081/ に接続すると Sinatra 版のサンプル(id:fits:20110306)とほとんど同じ画面が表示されます。
ちなみに、表示された HTML は以下のようになります。
Top ページの表示 HTML の例
<!DOCTYPE html> <html> <title>Express + Haml.js Sample</title> <body> <div class="menu">Menu</div> <ul> <li><a href="/books">Books List</a></li> <li><a href="/users">Users List</a></li> </ul> <div class="list">Book Comments</div> <form action="/comments" method="post" class="post"> <select name="user_id"> <option value="4d99c38946098c7409000001">fits</option> <option value="4d99c38e46098c7409000003">テスター</option> </select> <select name="book_id"> <option value="4d99c3dc46098c7409000008">プログラミングScala</option> <option value="4d99c40346098c740900000a">プログラミングWindowsAzure</option> </select> <input name="content" type="text"/> <input type="submit" value="Add"/> </form> <ul> <li>プログラミングScala</li> <ul> <li>アプリケーション開発の実践的な内容もあり : fits , Mon Apr 04 2011 13:16:00 GMT+0000 ( )</li> <li>テストコメント : テスター , Mon Apr 04 2011 13:21:31 GMT+0000 ( )</li> </ul> </ul> <ul> <li>プログラミングWindowsAzure</li> <ul> <li>cspack を使ったパッケージングなどIDE前提じゃない説明がなかなかよい : fits , Mon Apr 04 2011 13:20:45 GMT+0000 ( )</li> </ul> </ul> </body> </html>