##準備
まずはsocket.ioはnpmでインストールして使うので、nodeのセットアップをしなければなりません。node関係のセットアップは
http://qiita.com/oreo3@github/items/622fd6a09d5c1593fee4
こことかを参考にして各自セットアップしてみてください。(ここは主題ではないので省きます。)
##railsにおけるreact + npmの環境設定
まずはrailsのディレクトリの中にpackage.jsonとかを用意しないといけないので、ターミナル上で以下のようなコマンドを打ちます。
npm init
そしてpackage.jsonを作成したら必要なモジュールをインストールしていく必要があります。package.jsonは以下のようにしました。reactが使える環境と、es6をcompileする環境を設定するためのモジュールなどを使いました。
#package.json
"devDependencies": {
"babelify": "^6.4.0",
"browserify": "^12.0.1",
"browserify-incremental": "^3.0.1",
"del": "^2.0.2",
"express": "^4.13.3",
"glob": "^5.0.15",
"gulp": "^3.9.0",
"gulp-babel": "^5.3.0",
"gulp-notify": "^2.2.0",
"react-websocket": "^1.0.0",
"reactify": "^1.1.1",
"redis": "0.8.x",
"socket.io": "^1.0.6",
"socket.io-client": "^1.0.6",
"vinyl-source-stream": "^1.1.0"
},
}
ここも細かいモジュールについての説明は省きます。
重要なのは、socket.io
とsocket.io-client
です。
一応下にgulpfileの内容を載せます。
var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var glob = require('glob');
var del = require('del');
var notify = require('gulp-notify');
gulp.task(
'compile',
[
'clean',
'compile-es6'
]
);
gulp.task('compile-es6', function() {
var files = glob.sync('./app/frontend/javascripts/**/*.{js, jsx}')
browserify({ entries: files, debug: true })
.transform('babelify')
.bundle()
.on("error", function(err) {
return notify().write(err)
})
.pipe(source('bundle.js'))
.pipe(gulp.dest('./app/assets/javascripts/components'))
});
gulp.task('clean', function() {
del('./app/javascripts/components/*.js')
});
gulp.task('watch', function() {
gulp.watch('./app/frontend/javascripts/**/*.js', ['compile'])
});
compileの形としてはrailsのappディレクトリの中にfrontend/javascripts/components/
の下にes6で書いたreactjsのファイルを入れていき、compileした時にapp/assets/components
以下に出力されるようにしました。
gemfileには'react-rails'を入れました。
##railsのセットアップ
ここではTeacherモデルとStudentモデルの二つのuserモデルがあることを想定してDBなどを用意していきます。user周りはdevise
を使いました。
rails g devise teacher
rails g devise student
この二つは多対多にするべきなので、以下のように中間モデルを作りました。
rails g model teacher_student teacher_id:integer student_id: integer
またchatのメッセージを保存する必要があるので、ChatMessage
モデルを作成しました。
rails g model chat_message
chat_messagesテーブルのカラムは以下のようなものを用意します。
カラム名 | 型 | 用途 |
---|---|---|
text | text | メッセージを保存する |
group_id | integer | teacherとstudentの組み合わせを保存(teacher_studentsのidを保存) |
user_id | integer | ポリモーフィック用のid |
user_type | string | ポリモーフィックのtype |
各モデルの記述は以下のようになります。
has_many :teacher_students
has_many :teachers, through: :teacher_students
has_many :chat_messages, as: :user, dependent: :destroy
#teacher model
has_many :teacher_students
has_many :students, through: :teacher_students
has_many :chat_messages, as: :user, dependent: :destroy
#ポリモーフィック関連
belongs_to :user, polymorphic: true
belongs_to :teacher_student
belongs_to :teacher
belongs_to :student
has_many :chat_messages, foreign_key: "group_id"
これでひとまずは設定終了です。
では次にreactjsのファイルの中身についてです。
##React js セッティング
fileとしては4つ、chatのmain componentとchatのフォームのcomponent、自分が送ったメッセージのcomponent、相手が送ったメッセージのcomponent。 メッセージのcomponentを分ける理由としては、自分が送ったメッセージと相手のメッセージで色を変えたいからです。(Lineとか見ると分かるかと)
import CompanionMessage from './CompanionMessage'
import CurrentUserMessage from './CurrentUserMessage'
import ChatForm from './ChatForm'
class Chat extends React.Component {
constructor(props){
super(props)
this.state = {
messages: this.props.messages,
group_id: this.props.group,
current_user: this.props.current_user,
type: this.props.type
}
}
saveMessage(message){
$.ajax({
type: "POST",
url: "/api/chats",
data: { content: message, group_id: this.state.group_id },
dataType: "JSON",
})
.done( (data) => {
this.setState({ messages: data.chat_messages })
})
.fail( (xhr, status, error) => {
console.log(this.props.url, status, error.toString());
});
}
render() {
var messages = this.state.messages.map((message) => {
if ((message.user_id === this.state.current_user.id)&&(message.user_type === this.state.type)) {
return <CurrentUserMessage message={message.content}/>
} else {
return <CompanionMessage message={message.content} />
}
});
return(
<div className="chats">
<div className="chat_message_area">
{ messages }
</div>
<div className="chat_form">
<ChatForm onSaveMessage={this.saveMessage.bind(this)} />
</div>
</div>
);
}
}
window.Chat = Chat
export default class ChatForm extends React.Component{
setHandler(){
var $input = $("input[name='chat_message']")
this.props.onSaveMessage($input.val())
$input.val("")
}
render() {
return(
<div>
<input type="textarea" name="chat_message" />
<button className="btn btn-primary" onClick={ this.setHandler.bind(this) } >送信</button>
</div>
);
}
}
export default class ChatMessage extends React.Component {
render() {
return(
<div className="companion_message">
{ this.props.message }
</div>
);
}
}
export default class CurrentUserMessage extends React.Component{
render() {
return(
<div className="current_message">
{ this.props.message }
</div>
);
}
}
reactで何やっているかは省かせてもらいます。...
重要なのはここからで、socket.ioの設定をしていきます。
##Socket.ioのserver設定
まずはsocket.ioのサーバーを作らないといけないので、railsルートディレクトリにserver.jsというフォルダを作りましょう。(server/server.jsの方が良いのかな?)
そこでは以下のように記述します。
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io').listen(server);
server.listen(3001);
console.log('Server listening on port 3001');
app.use(function(req, res, next){
console.log("error")
console.log('%s %s', req.method, req.url);
next();
});
io.sockets.on('connection', function (socket) {
socket.on('disconnect', function () {
console.log("user disconnected");
});
});
これでサーバーは立つようになります。コマンドは以下のコマンドを打ちましょう。
node server.js
socket.io でリアルタイムチャットの実装
ようやく本題に入れます。。
まずどのようにして実装していくかなのですが、react.jsのところでメッセージが新しく作成された瞬間、つまりAjax処理をおこなわれた瞬間にemitをしてserver側に伝えます。server側はそれを受け取ったあと、react側に通知をするためにまたemitをします。これでsocket.ioによる監視を行うことができます。
では実際にコードを書いていいましょう。
まずはreact.jsでserver側に通知しましょう。
まずはreact側でsocket.ioのサーバーに接続できるようにしなければならないので、socket.jsというファイルを作り以下のように記述をします。
import io from 'socket.io-client'
let socket = io.connect("http://localhost:3001")
export default socket
これをchat.jsでimportしてserver側に対してemitをします。emitするところは、saveMessageメソッドの中でajax処理が成功した時に行います。serverに通知する時にstateのmessagesを一緒に送ってあげます。
import CompanionMessage from './CompanionMessage'
import CurrentUserMessage from './CurrentUserMessage'
import ChatForm from './ChatForm'
import socket from './socket'
class Chat extends React.Component {
constructor(props){
super(props)
this.state = {
messages: this.props.messages,
group_id: this.props.group,
current_user: this.props.current_user,
type: this.props.type
}
}
saveMessage(message){
$.ajax({
type: "POST",
url: "/api/chats",
data: { content: message, group_id: this.state.group_id },
dataType: "JSON",
})
.done( (data) => {
this.setState({ messages: data.chat_messages })
//ここです!!!!!!!
socket.emit("sendMessage", data.chat_messages)
})
.fail( (xhr, status, error) => {
console.log(this.props.url, status, error.toString());
});
}
}
ではこの通知をserver側で受け取りましょう。
io.sockets.on('connection', function (socket) {
socket.on("sendMessage", function(messages){
});
socket.on('disconnect', function () {
console.log("user disconnected");
});
});
受け取る時はon
を使えばできます。またreact側で送られてきた、messageの内容もここで受け取りましょう。では次にreact側にもう一度通知してあげます。ここでもmessageの内容を送るようにしましょう。
io.sockets.on('connection', function (socket) {
socket.on("sendMessage", function(messages){
socket.broadcast.json.emit("receiveMessage", messages)
});
socket.on('disconnect', function () {
console.log("user disconnected");
});
});
broadcast
は送信者の人以外に送る方法で、emit
でも問題ないかと思いますが今回はこれでやりました。
では今度はreact側でserver側の通知を受信できるようにしましょう。
受け取る時は、componentが表示されてからの話なので、componentDidMount
の中に記述します。
受け取ったら、messageReceive
メソッド(名前が少し気持ち悪いですが..)を呼びstateを更新してあげましょう。
import CompanionMessage from './CompanionMessage'
import CurrentUserMessage from './CurrentUserMessage'
import ChatForm from './ChatForm'
import socket from './socket'
class Chat extends React.Component {
constructor(props){
super(props)
this.state = {
messages: this.props.messages,
group_id: this.props.group,
current_user: this.props.current_user,
type: this.props.type
}
}
saveMessage(message){
$.ajax({
type: "POST",
url: "/api/chats",
data: { content: message, group_id: this.state.group_id },
dataType: "JSON",
})
.done( (data) => {
this.setState({ messages: data.chat_messages })
socket.emit("sendMessage", data.chat_messages)
})
.fail( (xhr, status, error) => {
console.log(this.props.url, status, error.toString());
});
}
componentDidMount(){
socket.on("receiveMessage", (messages) => {
this.messageReceive(messages)
})
}
messageReceive(messages) {
this.setState({ messages })
}
///...省略
}
これで完成です!!!
##最後に
長くてすみません。。何かうまくいかなかったりやり方がおかしかったりしたら教えていただきたいです。(文献があまりなくて自分でなんとなくで考えてやったので構造が少し怪しいかもしれないです。。)