Sails.jsでチャットアプリを作りながらPub/Subで柔軟に通知するテクニックを学ぶ
はじめに
まずは前回の記事を読んでいただけるとスムーズに理解出来るかと思います。
Sails.js(0.10.x)でSocket.IOを使った超シンプルなチャットサービスを作る
また、今回はPub/Subに関するテクニックですが、その肝心のテクニックはだいぶ後ろのほうにかかれています。この記事を元に初心者が簡単なチャットサービスを作れるところを目指していますので、有識者には退屈かと思います。適当に読み飛ばして下さい。
準備
基本的に前回の記事で作成したアプリケーションに修正を加えていきますので、まずはそちらを参考にしてください。
今回使ったアプリは以下のレポジトリに置いています。参考までに。
https://github.com/KeitaMoromizato/chatsample2
Controllerのメソッドを作成
前回sails generate api message
というコマンドを実行しているので、/api/controllers/MessageController.js
が生成されているはずです。まずはここにメソッドを追加し、Controllerの実装方法を学びましょう。
必要なのはGET /message
とPOST /message
の処理なので、以下のようにfind()
とcreate()
の2つのメソッドを書きます。
module.exports = {
find: function(req, res) {
console.log("GET /message");
Message.find().exec(function(err, messages) {
res.json(messages);
});
},
create: function(req, res){
console.log("POST /message");
var text = req.param('text');
Message.create({text: text}).exec(function(err, message) {
res.json(message);
});
}
};
これだけで動いてしまいます。ブラウザから見た動作は前回と変わりませんが、Sails.jsを起動したコンソールを見てみるとしっかりとログが出力されているはずです。
Sails.jsにはBlueprint
という機能が備わっており、GET /message
のリクエスト時にはfind()
が、POST /message
のリクエスト時にはcreate()
が自動的に実行されるようになっています。とりあえず動かしてみたいときには非常に楽です。
他にもあるので詳細は公式リファレンスを。
http://sailsjs.org/#/documentation/reference/blueprint-api
Pub/Subを実装する
前回ブラウザを2つ立ち上げてリアルタイム通信の動作確認までしましたが、上記実装をしてしまうとそれが動かなくなっています。メソッドを何も書いていない場合はsocket.get()
のリクエストが来た場合は自動的にSubscribe(予約)が実行され、socket.post()
のリクエストでPublish(通知)が実行されます。
なので自分でPublish/Subscribeする処理を追記しましょう。
module.exports = {
find: function(req, res) {
console.log("GET /message");
Message.find().exec(function(err, messages) {
Message.subscribe(req.socket);
res.json(messages);
});
},
create: function(req, res){
console.log("POST /message");
var text = req.param('text');
Message.create({text: text}).exec(function(err, message) {
Message.publishCreate(message);
res.json(message);
});
}
};
追加された行を見て行きましょう。まずはfind()内での以下の行。これは「req.socketに対してMessageに関する通知があったら送ります」という命令。フロントとではsocket.get()
でMessageモデルを取得しているので、以降Messageモデルに関する通知を受け取ることが出来ます。
Message.subscribe(req.socket);
次はcreate()
の中の一文。内容はメソッド名そのままで、「Messageモデルが作成されたので通知しますよ」ということ。これでPub/Subの実装は完了!
Message.publishCreate(message);
複数のスレッドに分割する
前置きが長くなってしまいましがここからが本題。前回は最速で作れるチャットサービスを目指してやっていましたが、正直こんなサービス使い物にならないですよね。なのでもう少し実用性を高めるため、トピックごとの会話が出来る「スレッド」という概念を入れます。SlackでいうところのChannelです。これを擬似的に創りましょう。
まずはController/Modelを作るところから。
$ sails generate api thread
ThreadControllerを実装します。今回は簡略化のため、POSTされたスレッドと同じタイトルのスレッドがすでに存在したらそれを返し、ない場合は新規作成しています。
module.exports = {
create: function(req, res) {
console.log("POST /thread");
var title = req.param('title');
Thread.findOne({title: title}).exec(function(err, thread){
// すでに同名のスレッドがあれば返す
if (thread) {
console.log("thread already exists");
return res.json(thread);
}
Thread.create({title: title}).exec(function(err, thread) {
res.json(thread);
});
});
}
};
Viewはこんな感じ。デザインセンスが無いわけではなくデザインを放棄しただけです。
(function() {
// Socket.IOに接続
var socket = window.io.connect();
var currentThread;
socket.on('connect', function() {
// 以下の処理はSocket.IOのconnectメッセージ受信後(接続確立後)
// に行わないと失敗する
socket.on('message', function(message) {
if (message.verb == "created") {
$("#chat-timeline").append('<li>' + message.data.text + '</li>');
}
});
});
$('#chat-send-button').on('click', function() {
if (!currentThread) return;
var $text = $('#chat-textarea');
var msg = $text.val();
// messageをthreadに紐付けるためThreadIDを追加
socket.post("/message", {
text: msg,
thread_id: currentThread.id
}, function(res) {
$text.val('');
});
});
$('#thread-create-button').on('click', function() {
var $text = $('#thread-form');
var title = $text.val();
// Thread作成or取得
socket.post("/thread", {
title: title
}, function(res, a) {
$('#current-thread').text("in " + res.title);
// 取得したThreadのメッセージを取ってくる
getMessages(res.id);
currentThread = res;
$text.val('');
});
});
var getMessages = function(threadId) {
socket.get("/message", {thread_id: threadId}, function(messages) {
for (var i = 0; i < messages.length; i++) {
$("#chat-timeline").append('<li>' + messages[i].text + '</li>');
}
});
}
})();
実行してみると、しっかりとThreadごとにメッセージが保存されていることが分かります。また、2画面で開いてもリアルタイム通知がもう一方の画面にも届いていることが分かります。
がしかし、これ実はうまくいってません。2つのウィンドウで__別々のThreadを開いていても、通知されてしまいます。__普通のチャットアプリで考えると、同じスレッドを開いている人にだけ通知してほしいものです。
ここまで来てやっとこの記事の目的、__いい感じにpublishするにはどうしたらいいのか?__というところです。
Publishを振り分ける
まず上の問題点はなんだったのか?それは__Messageモデルを取得した人に対し、全てのMessageの通知を投げている__ところ。そこにThreadの概念はなく、どれだけ人が増えようとも、Threadが増えようとも全員に通知してしまいます。
これを解決するにはどうするか?方法は結構シンプルで、__Threadに対してpublishを投げればいい__ということ。では実際に見て行きましょう。
まずはControllerから。ポイントは1箇所だけ。 Thread.subscribe(req.socket, thread);
の部分。Thradを読み込んだ(作成した)ユーザに対して通知をするために、subscribe()しています。第2引数を設定した場合、指定したモデルにのみpublishを送るようになります(というか第2引数指定しない場合deprectedって怒られてますね。動きはするけど...)
module.exports = {
create: function(req, res) {
console.log("POST /thread");
var title = req.param('title');
Thread.findOne({title: title}).exec(function(err, thread){
// すでに同名のスレッドがあれば返す
if (thread) {
console.log("thread already exists");
Thread.subscribe(req.socket, thread);
return res.json(thread);
}
Thread.create({title: title}).exec(function(err, thread) {
Thread.subscribe(req.socket, thread);
res.json(thread);
});
});
}
};
一方MessageControllerの方は、Messageモデルに対する通知をやめたので、find()
からsubscribeを消しました。そして、一番重要な点がcreate()
にて__Messageモデルが作成された時にThreadに対してpublishをしている__ところです。ここが今日のポイント!
また、先ほどはpublichCreate()を使っていましたが、今回はpublishUpdate()です。Threadモデル自体はもう存在するので、そのモデルに対する擬似的な変更通知としてMessageを投げます。
module.exports = {
find: function(req, res) {
console.log("GET /message");
var threadId = req.param('thread_id');
console.log(threadId);
Message.find({thread: threadId}).exec(function(err, messages) {
res.json(messages);
});
},
create: function(req, res){
console.log("POST /message");
var text = req.param('text');
var threadId = req.param('thread_id');
Message.create({thread: threadId, text: text}).exec(function(err, message) {
Thread.publishUpdate(message.thread, {
model: 'message',
body: message
});
res.json(message);
});
}
};
あとはクライアント側で受け取るだけ。socketでthreadモデルに対して待ち受け、update通知ならデータを取り出し表示。これで完了!
socket.on('thread', function(thread) {
if (thread.verb == "updated" && thread.data.model == 'message') {
$("#chat-timeline").append('<li>' + thread.data.body.text + '</li>');
}
});
動作確認
ここまで実装すると、こんな感じで(といっても静止画だとよくわからないので動かしてみて欲しいのですが)同じスレッドを開いている人に対してのみ通知が行くようになります。やったね!