75
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

rails + react + socket.iOによるリアルタイムチャットシステム

Last updated at Posted at 2015-12-05

##準備
まずは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.iosocket.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

各モデルの記述は以下のようになります。

student.rb

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
#ポリモーフィック関連
chat_message.rb

belongs_to :user, polymorphic: true
belongs_to :teacher_student
teacher_student.rb

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とか見ると分かるかと)

Chat.js
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

ChatForm.js

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>
    );
  }
}
CompanionMessage.js

export default class ChatMessage extends React.Component {
  render() {
    return(
      <div className="companion_message">
        { this.props.message }
      </div>
    );
  }
}

CurrentUserMessage.js

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の方が良いのかな?)

そこでは以下のように記述します。

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を一緒に送ってあげます。

Chat.js

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側で受け取りましょう。

server.js

io.sockets.on('connection', function (socket) {
  socket.on("sendMessage", function(messages){

  });



  socket.on('disconnect', function () {
    console.log("user disconnected");
  });

});

受け取る時はonを使えばできます。またreact側で送られてきた、messageの内容もここで受け取りましょう。では次にreact側にもう一度通知してあげます。ここでもmessageの内容を送るようにしましょう。

server.js
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を更新してあげましょう。

Chat.js

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 })
  }

///...省略
}


これで完成です!!!

##最後に

長くてすみません。。何かうまくいかなかったりやり方がおかしかったりしたら教えていただきたいです。(文献があまりなくて自分でなんとなくで考えてやったので構造が少し怪しいかもしれないです。。)

75
73
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
75
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?