ここにある
http://chat.shokai.org

herokuでも動いている
http://rocketio-chat.herokuapp.com

ソースコード
shokai/rocketio-tiqav-chat · GitHub


このように1文字入力するごとにTiqav.comで画像検索する。
レス画像検索No.1/画像会話なら ちくわぶ

画像をマウスクリックすれば画像が投稿される。
enterキーを押せばそのままテキストが投稿される。


http://chat.shokai.org/shokai
http://chat.shokai.org/test
のように部屋も無限に作れる。


作った経緯

RubyHirobaでLTして気づいたのだが、もともとchatはRocketIOの最も簡単なサンプルとして作っていた。
サンプルなので、難しい事するとわからなくなるからシンプルに留めていた。

でももっと単純なhello worldサンプルができたので、チャットは手加減する必要なくなった。
shokai/rocketio-hello-world · GitHub
http://hello.shokai.org

むしろ、「それなりに込み入った操作をするUIと通信の連携方法」や「RocketIOのチャンネル機能」の例が必要かと思ったので、
それなりに複雑なアプリを作った。


実装

チャットログはサーバーのオンメモリに保存している。

画像検索は、HTML上のinput要素を監視して変化があれば都度RocketIOでサーバーに文字列を送る。
サーバー(Sinatra)がTiqavに検索をリクエストする。(tiqav gemを使っている)
[ブラウザ]<--(RocketIO)-->[Sinatra]<--(HTTP)-->[Tiqav.com]


同じ文字列を何度も検索するのは無駄なので、ブラウザ上とサーバー上でそれぞれJSのオブジェクトとmemcachedでキャッシュを行なっている。
[ブラウザ/JS Cacheオブジェクト]<--(RocketIO)-->[Sinatra/memcached]<--(HTTP)-->[Tiqav.com]


Tiqav.comにはあまりリクエストが飛ばないようになるので、かなりリアルタイム気味にチャットと画像検索できる。


サーバーで検索する時はEM::defer内でリクエストを飛ばす。
以前HerokuのSinatraにバックグラウンドワーカーを詰め込んで節約で説明したが、Sinatraはシングルスレッドだけど、IO待ちする処理をEM::deferで包むと他のクライアントを待たせずにレスポンスを返せるようになる。


EventEmitterの活用

RocketIOに付属しているEventEmitterを使うと、複雑なUIと通信をうまく連携させられる(ブラウザ用EventEmitterを作った
EventEmitterはあらゆるクラスやインスタンスにイベント機能をmixinできるライブラリです


画像chatのjavascriptは超シンプル。
input要素が変更されたら画像検索して結果をHTMLに表示している。
var io = new RocketIO().connect();
var img_search = new ImageSearch(io);

$(function(){
var chat_input = new InputWatcher("#message");

chat_input.on("change", function(val){
img_search.search(val);
});
});

img_search.on("result", function(res){
$("#img_select").html(""); // clear DOM
for(var i = 0; i < res.imgs.length; i++){
(function(){
var img_url = res.imgs[i];
var img_tag = $("<img>").attr("src", img_url);
$("#img_select").append( $("<li>").html(img_tag) ); // display each Images
})();
}
});


まずInputWatcherというinputタグの中身を監視して”change”イベントを発行するクラスを作る。
中身が変わった時だけchangeイベントが発行される。
// watch text-input and emit event on "change"
var InputWatcher = function(target){
var self = this;
new EventEmitter().apply(this);
this.target = (target instanceof jQuery) ? target : $(target);
var last_val = null;
var watch = function(){
var val = self.target.val();
if(!!last_val && last_val !== val) self.emit("change", val);
last_val = val;
};
setInterval(watch, 100);
this.target.keyup(watch);
};


changeイベントから画像検索する
$(function(){
var chat_input = new InputWatcher("#message");

chat_input.on("change", function(val){ // regist "change" event
img_search.search(val);
});
});


画像検索をRocketIOでリクエストして、結果が返ってきたら”result”イベントを発行するImageSearchクラスを作る。
内部にcacheを持っていて、一度検索したことのある文字列はサーバーに送らずに即”result”イベントを発行する。
コンストラクタにRocketIOインスタンスを渡して、通信にはそれを使うようにする。
// request image-search to server with RocketIO
// emit "result" event on receive image-url-array
var ImageSearch = function(io){
var self = this;
if(!(io instanceof RocketIO)) throw new Error("Argument must be instance of RocketIO");
var cache = {};
new EventEmitter().apply(this);
io.on("img_search", function(data){
if(!data || typeof data.word !== "string" || !(data.imgs instanceof Array)) return;
cache[data.word] = data.imgs;
self.emit("result", data); // "result" event
});
var eid = null;
var last_word = null;
this.search = function(word){
if(!!eid) clearTimeout(eid);
if(typeof word !== "string") return;
if(word.length < 1){
self.emit("result", {imgs: [], word: ""}); // "result" event with Empty images
return;
}
// check Cache
if(cache[word] instanceof Array && cache[word].length > 0){
self.emit("result", {imgs: cache[word], word: ""}); // "result" event with Cached images
return;
}
// if NOT Cached
eid = setTimeout(function(){
eid = null;
cache[word] = [];
io.push("img_search", word); // request to server
}, 300);
};
};


最終形。
さっきのInputWatcherと合わせて使うとこうなる。
var io = new RocketIO().connect();
var img_search = new ImageSearch(io);

$(function(){
var chat_input = new InputWatcher("#message");

chat_input.on("change", function(val){
img_search.search(val);
});
});

img_search.on("result", function(res){
$("#img_select").html(""); // clear DOM
for(var i = 0; i < res.imgs.length; i++){
(function(){
var img_url = res.imgs[i];
var img_tag = $("<img>").attr("src", img_url);
$("#img_select").append( $("<li>").html(img_tag) ); // display each Images
})();
}
});