Cygwinや仮想化なしでnode.jsをWindowsへインストール

このエントリはリアルタイムWebハッカソンのハンズオン資料その1補足です。

どうしてもCygwinが嫌いだ。だけどVirtualBoxとかVMwareとかも嫌だって人もいるかもしれません。そういう人もなんとかまあnode.jsを使えるようにできないわけではありません。

注)茨の道です。普通にCygwin使うかVirtualBoxUbuntu入れることを推奨します。

1. msysgitをインストールしてGit Bashを使えるようにします。
2. Git Bash上で

$ git clone git://github.com/ajaxorg/node-builds

をして、node-builds/win32/binにパスを通す。これで一応nodeコマンドは使えるようになります。
ただ、必要なライブラリをnpmを使って取得することができません。なので……
3. Git Bash上で

$ git clone git://github.com/robrighter/node-boilerplate.git hoge
$ cd hoge
$ ./bin/initproject.sh

とすれば、express, Socket.ioなどをhoge/lib内に取得し、hoge/server.jsの先頭に

require(__dirname + "/lib/setup").ext( __dirname + "/lib").ext( __dirname + "/lib/express/support");

を記述してくれるのでこれで普通にrequireして使うことができるようになります。
expressやSocket.io以外のライブラリが使いたければ自分でlib内にgit cloneすればいいんじゃないかな。とはいえやったことはないので出来るかどうか知りません!誰かやってみて教えてくださいっ

ExpressとWebSocketを使ったWebSocketのサンプルを作る準備

このエントリはリアルタイムWebハッカソンのハンズオン資料その2です。

前回の記事でnode.jsとnpmのインストールは完了しているものとします。

まずは必要なライブラリのインストール

$ npm install express jade less socket.io
npm info it worked if it ends with ok
npm info using npm@0.2.3-6
npm info fetch http://registry.npmjs.org/express/-/express@1.0.0rc4.tgz
npm info fetch http://registry.npmjs.org/less/-/less-1.0.36.tgz
npm info fetch http://registry.npmjs.org/socket.io/-/socket.io-0.5.3.tgz
npm info fetch http://registry.npmjs.org/jade/-/jade@0.5.3.tgz
npm info install express@1.0.0rc4
npm info install socket.io@0.5.3
npm info install less@1.0.36
npm info install jade@0.5.3
npm info activate socket.io@0.5.3
npm info activate less@1.0.36
npm info activate jade@0.5.3
npm info activate express@1.0.0rc4
npm info build Success: socket.io@0.5.3
npm info build Success: less@1.0.36
npm info build Success: jade@0.5.3
npm info build Success: express@1.0.0rc4
npm ok

ExpressはSinatraライクなnode.jsのWebアプリケーションフレームワーク
JadeHamlライクなnode.jsのテンプレートエンジンでlessCSS拡張lessのnode.js版。両方ともExpressが標準でサポートしている。
Expressがサポートしているテンプレートエンジンは他にもEJSHamlCoffeeKupがあり、CSS拡張もsassnode.js実装をサポートしてる。でまあ、好きなの使えばいいんだけど後述のジェネレータで作られたアプリでデフォルトで使われてるのがjadeとlessなので、ここではjadeとlessを使うことにする。

で、おもむろに以下を実行する

$ express sample
   create : sample
   create : sample/app.js
   create : sample/public/javascripts
   create : sample/logs
   create : sample/pids
   create : sample/public/images
   create : sample/public/stylesheets
   create : sample/public/stylesheets/style.less
   create : sample/test
   create : sample/test/app.test.js
   create : sample/views/partials
   create : sample/views/layout.jade
   create : sample/views/index.jade

すると、必要なファイルとディレクトリが作られアプリ基盤が完成する。
publicが静的ファイル置き場。(クライアントサイドの)JavaScriptCSSや画像なんかはここに置く。viewsはテンプレートファイル置き場。静的なHTMLならpublicに置けばいいんじゃないかな。testはテストコード置き場。logsはログファイルが作られる場所。pidsは複数プロセス立ち上げた時の管理用…だと思う。(ぶっちゃけ前まではpublicとviewsしかなくて、このエントリ書くために今実行してみたらその他のが出来てたので戸惑ってるところ)。
この状態ですでに実行可能になっているので実行してみます。

$ cd sample
$ node app.js
Express server listening on port 3000

ブラウザでlocalhost:3000にアクセスし、Welcome to Expressと表示されたら成功。

app.jsの中身はこんな感じ。

/**
 * Module dependencies.
 */

var express = require('express');

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.use(express.bodyDecoder());
  app.use(express.methodOverride());
  app.use(express.compiler({ src: __dirname + '/public', enable: ['less'] }));
  app.use(app.router);
  app.use(express.staticProvider(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
});

app.configure('production', function(){
  app.use(express.errorHandler()); 
});

// Routes

app.get('/', function(req, res){
  res.render('index.jade', {
    locals: {
        title: 'Express'
    }
  });
});

// Only listen on $ node app.js

if (!module.parent) {
  app.listen(3000);
  console.log("Express server listening on port %d", app.address().port)
}

まあ、見たら大体分かると思うけどちょっと補足。

  • 16行目でCSS拡張としてlessを使うことを指定している。
  • 21,25行目でdevelopmentモードのときとproductionモードのときのエラー処理を設定している。基本はdevelopmentモードなんだけど、以下のようにNODE_ENVにproductionを指定して動かせばproductionモードになる。
$ NODE_ENV=production node app.js
  • 31行目で'/'以下にアクセスされたときの動作を設定している。見てのとおりjadeのテンプレートファイルのindex.jadeを変数titleに'Express'を束縛して描画する。テンプレートファイルの拡張子を見てどのエンジンを使うかを自動的に判断するので複数のテンプレートエンジンを共存させることも可能。だけどまあ1つのエンジンしか使わないなら、12行目のconfigureの中で次のように指定してやって拡張子を省略することもできる。
app.set('view engine', 'jade');

ExpressとSocket.ioを使ったWebSocketのサンプルを作る

このエントリはリアルタイムWebハッカソンのハンズオン資料その3です。

前回の続きです。まずはSocket.ioを使えるようにしましょう。

app.jsの先頭を

var express = require('express');

から

var express = require('express'),
    io = require('socket.io');

にする。
また、ファイルの最後に

var socket = io.listen(app);
socket.on('connection', function(client) {
  // connect
  client.on('message', function(message) {
    // message
  });
  client.on('disconnect', function() {
    // disconnect
  });
});

を追加する。

クライアントから接続がきたらconnectに書かれた処理が、メッセージがきたらmessageに書かれた処理が、切断されたらdisconnectにかかれた処理が実行される。

次にクライアント側の処理。
sample/views/layout.jadeを開いて

!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript', src='http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js')
    script(type='text/javascript', src='http://cdn.socket.io/stable/socket.io.js')
    script(type='text/javascript', src='/javascripts/client.js')
  body!= body

と記述。jQueryを使い、socket.ioのクライアントライブラリは公式サイトCDNを使い、自分で書くクライアント側のJavaScriptファイルはclient.jsという名前にする。

sample/public/javascripts/client.jsを以下の内容で作る。

var socket = new io.Socket('localhost');
socket.connect();
socket.on('message', function(message) {
  //message
});

見てのとおり2行目で接続し、サーバからメッセージがきたらmessageに書かれた処理が実行される。

んじゃとりあえず、現在接続している人数をリアルタイムに表示するプログラムでも書いてみましょう。
まずはapp.js。最後のsocket.ioのところを以下のようにする

var socket = io.listen(app);
var count = 0;
socket.on('connection', function(client) {
  count++;
  client.broadcast(count);
  client.send(count);
  client.on('message', function(message) {
    // message
  });
  client.on('disconnect', function() {
    // disconnect
    count--;
    client.broadcast(count);
  });
});
  1. countの初期値を0にしておく
  2. 接続がきたらcountを+1して、その値をbroadcast(現在のクライアント以外にメッセージを送信)とsend(現在のクライアントにメッセージを送信)
  3. 切断したらcountを-1して、その値をbroadcast

わかりやすいですね。

次にsample/views/index.jadeに以下のように追記します。

h1= title
p Welcome to #{title}
p 現在接続している人は
  span#count
  人います

そしてsample/public/javascripts/client.jsを以下のように修正します。

var socket = new io.Socket('localhost');
socket.connect();
socket.on('message', function(message) {
  $('#count').text(message);
});

以上で終了です。
それでは

$ node app.js

で起動してみましょう。
localhost:3000にアクセスすると「現在接続している人は1人います」と出るはずです。コンソールには

Express server listening on port 3000
18 Oct 18:44:52 - socket.io ready - accepting connections
xxx.xxx.xxx.xxx - - [Mon, 18 Oct 2010 09:44:56 GMT] "GET /favicon.ico HTTP/1.1" 404 - "" "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3"
18 Oct 18:44:56 - Initializing client with transport "websocket"
18 Oct 18:44:56 - Client 14417221560142934 connected

こんな感じでログが出てるはずです。ちなみにfaviconがないよっていう404はウザいので適当にfavicon.icoを作ってpublic直下に置いとけばいいと思います。
では、現在のタブを開いたまま新しいタブでもlocalhost:3000にアクセスしてみましょう。「現在接続している人は2人います」と表示され、コンソールにも

18 Oct 18:50:14 - Initializing client with transport "websocket"
18 Oct 18:50:14 - Client 2343847642187029 connected

みたいに接続が増えたログが出てるはずです。
さらに、最初の「1人」と表示されていたタブを見てください。リロードしなくてもすでに「2人」になっているはずです!
タブを増やすたびに人数が増えていき、また閉じるたびに減っていくことを確認してください。
これでnode.jsを使った基本的なWebSocketの使い方は終了です。

ExpressとSocket.ioを使ったチャットサンプル

このエントリはリアルタイムWebハッカソンのハンズオン資料その4です。

前回の続きです。それでは次に簡単なチャットアプリのコードを見てみましょう。かなりの部分(特にデザイン面)をSocket.ioのチャットサンプルをパクって参考にしています。

サーバ側であるapp.jsはこんな感じです。

var express = require('express'),
    io = require('socket.io'),
    json = JSON.stringify;

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.use(express.bodyDecoder());
  app.use(express.methodOverride());
  app.use(express.compiler({ src: __dirname + '/public', enable: ['less'] }));
  app.use(app.router);
  app.use(express.staticProvider(__dirname + '/public'));
  app.use(express.logger());
});

app.configure('development', function(){
  express.logger("development mode");
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  express.logger("production mode");
  app.use(express.errorHandler());
});

// Routes

app.get('/', function(req, res){
  res.render('index.jade', {
    locals: {
        title: 'リアルタイムWebハッカソン Chat Room'
    }
  });
});

// Only listen on $ node app.js

if (!module.parent) {
  app.listen(3000);
  console.log("Express server listening on port %d", app.address().port)
}

var socket = io.listen(app);
var count = 0;
socket.on('connection', function(client) {
  count++;
  client.broadcast(json({count: count}));
  client.send(json({count: count}));

  client.on('message', function(message) {
    // message
    client.broadcast(message);
    client.send(message);
  });
  client.on('disconnect', function() {
    // disconnect
    count--;
    client.broadcast(json({count: count}));
  });
});

やりとりする内容をJSON形式に変えてます。人数のカウント情報だけでなく、チャット内容もやりとりするため、ただの文字列ではどちらが来てるのかを判断しづらいからです。
messageを受け取るところの処理では受け取った内容をそのまま配信してます。

次にクライアント側を見ましょう。views/layout.jadeはこんな感じです。

!!! 5
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript', src='http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js')
    script(type='text/javascript', src='http://cdn.socket.io/stable/socket.io.js')
    script(type='text/javascript', src='/javascripts/client.js')
  body!= body

先頭に5が加わってHTML5の宣言にした事以外前回から変わりありません。HTML5にしてるのは下にあるindex.jadeのテキストボックスにplaceholder属性の設定をしているからです。
views/index.jadeは次のようになっています。

h1= title
p 現在接続している人は
  span#count
  人います
#chat
form#form(onsubmit='send(); return false;')
  input#name(type='text', placeholder='Twitter ID')
  input#text(type='text', autocomplete='off')
  input(type='submit', value='送信')

チャット表示欄#chatと入力フォーム#formが追加されています。#chatのように要素を省略すると自動的にdivだと判断されます。
public/stylesheets/style.lessは次のようになっています。

body {
  padding: 10px 50px 5px 50px;
  font: 14px "Lucida Grande", "Helvetica Nueue", Arial, sans-serif;
}
#chat {
  height: 500px;
  overflow: auto;
  width: 800px;
  border: 1px solid #eee;
  p {
    padding: 0px;
    margin: 0px;
  }
  div {
    padding: 5px;
    margin: 0;
  }
  div:nth-child(odd) {
    background: #F6F6F6
  }
  .permalink {
    font: 3px;
    color: #AEAEAE;
  }
}
#form {
  width: 782px;
  background: #333;
  padding: 5px 10px;
  input[type=text] {
    padding: 5px;
    background: #fff;
    border: 1px solid #eee;
  }
  #name {
    width: 85px;
  }
  #text {
    width: 620px;
  }
  input[type=checkbox] {
  }
  input[type=submit] {
    cursor: pointer;
    background: #999;
    border: none;
    padding: 6px 8px;
    -moz-border-radius: 8px;
    -webkit-border-radius: 8px;
    margin-left: 5px;
    text-shadow: 0 1px 0 #fff;
  }
  input[type=submit]:hover {
      background: #A2A2A2;
  }
  input[type=submit]:active {
      position: relative;
      top: 2px;
  }
}

最後にclient.jsを見てみましょう。

var socket = new io.Socket('localhost'),
    json = JSON.stringify;
socket.connect();
socket.on('message', function(message) {
  message = JSON.parse(message);
  if (message.count) {
    $('#count').text(message.count);
  }
  if (message.message) {
    var data = message.message;
    var date = new Date();
    date.setTime(data.time);
    $('#chat').append('<div class="chatlog"><p><a name=' + data.time + '></a><a href="http://twitter.com/' + data.name + '"><img src="http://api.dan.co.jp/twicon/' + data.name + '/mini" /></a> ' + data.text + '</p><a class="permalink" href="#' + data.time + '">' + date.toString() + '</a></div>')
    $('#chat').scrollTop(1000000);
  }
});

function send() {
  var name = $('#name').val();
  var text = $('#text').val();
  if (text && name && name != "Twitter ID") {
    var time = new Date().getTime();
    socket.send(json({message: {name: name, text: text, time: time}}));
    $('#text').val('');
  }
}

特に難しいところはないと思います!

ExpressとSocket.ioを使ったチャットサンプルを永続化

このエントリはリアルタイムWebハッカソンのハンズオン資料その5です。

今回は、前回のチャットサンプルのチャットログを永続化してみましょう。
node.jsは様々なデータストアと連携することができます。
MySQL, PostgreSQLなどのRDBMSももちろん可能ですが、やはりCouchDB, MongoDB, RedisなどのいわゆるNoSQLと呼ばれるものと連携させるのが人気のようです。
特にJSON形式でそのまま格納することのできるデータストアは、JavaScriptから扱うのに都合がいいというのは言うまでもないと思います。

もちろん、node.js製のデータストアエンジンもいくつかあります。有名なのはnStoreでしょう。
今回はdirtyというライブラリを使います。理由は僕が使ってみたかったからです。

インストールは

$ npm install dirty

で終わり。
……のはずなんだけど、現状(dirty v0.9.0)だと最新のnode v0.2.3で使おうとしたら'constrants'ってモジュールが見つからないよってエラーが出てしまう。
もうパッチはあたっていて、npmにその最新のが登録されていないだけの状態なのでgithubから直接とってくりゃいい。なので

$ cd tmp
$ git clone git://github.com/felixge/node-dirty.git
$ cd node-dirty
$ npm install

でOK

んじゃさっそくdirtyを使ったapp.jsを見てみましょう。変わったのは先頭のrequireの部分

var express = require('express'),
    io = require('socket.io'),
    db = require('dirty')('log.db'),
    json = JSON.stringify;

とsocket.onの引数のfunctionの部分

socket.on('connection', function(client) {
  count++;
  client.broadcast(json({count: count}));
  client.send(json({count: count}));
  db.forEach(function(key, val) {
    client.send(val);
  });

  client.on('message', function(message) {
    // message
    db.set(new Date().getTime(), message);
    client.broadcast(message);
    client.send(message);
  });
  client.on('disconnect', function() {
    // disconnect
    count--;
    client.broadcast(json({count: count}));
  });
});

こんだけです。見ての通り、メッセージを受け取ったらそれを現在時刻のミリ秒をキーにsetしています。
で、新規接続したらそいつに全てのログをforEachで回しながらsendしています。単純ですね。
果たしてこれでどんだけのパフォーマンスが出るかわかりませんが、それはこのチャットを実際に使うハッカソンの会場で判明することでしょう。

リポジトリhttp://github.com/meso/chatsample