読者です 読者をやめる 読者になる 読者になる

undefined

bokuweb.me

Phoenix + ReactでChannelを使用した簡易チャットを作る(1)

React.js Phoenix Elixir

勉強用の題材としてブログを作りはじめた。

blog.bokuweb.me

ブログの更新やコメントにも使用できるので、次はchannelを使用して簡単なチャットを作ってみる。Channelとは簡単にwebsocket通信ができる機能で、Node.jsにおけるSocket.io的なものと理解している。

動作環境

  • Erlang 17.5
  • Elixir 1.2.0
  • Phoenix 1.1.4
  • node.js 4.2.1

環境構築

前回同様。

blog.bokuweb.me

プロジェクト名はphoenix_channel_sandboxとする。

mix phoenix.new phoenix_channel_sandbox

また今回、package.jsonのdependenciesを削除する際以下の2つは削除しない。

  "dependencies": {
   "phoenix": "file:deps/phoenix",
   "phoenix_html": "file:deps/phoenix_html"
  },

Channelへのjoinまでを実装

バックエンド

lib/phoenix_channel_sandbox/endpoint.exsocket用のエンドポイントが用意されている。

  • lib/chat_phoenix/endpoint.ex
defmodule PhoenixChannelSandbox.Endpoint do
  use Phoenix.Endpoint, otp_app: :phoenix_channel_sandbox

  socket "/socket", PhoenixChannelSandbox.UserSocket

これにより、エンドポイント/socketにアクセスするとUserSocketモジュール に接続されることになる。 UserSocketweb/channels以下に雛形が生成されており、以下の行のコメントアウトを解除する。

  • web/channels/user_socket.ex
  channel "rooms:*", PhoenixChannelSandbox.RoomChannel

メッセージはPhoenix.Socket.Messageの形式でやりとりされる。上記の"rooms:*"はtopic名で、topic名はtopic:subtopic、またはtopicの形式で指定される。*はワイルドカードなのでトピックroomsに来たすべてのサブトピックをRoomChannelモジュールに送ることになる。

Phoenix.Socket.Message – Phoenix v1.1.4

メッセージの受け先となるモジュールroom_channelweb/channelsに作成する。

  • web/channels/room_channel.ex
defmodule PhoenixChannelSandbox.RoomChannel do
  use PhoenixChannelSandbox.Web, :channel

  def join("rooms:join", message, socket), do: {:ok, socket}
end

join/3channelへの参加を承認/拒否する関数で、{:ok, socket}{:ok, reply, socket}を返すことで、承認されchannelに参加できる。 拒否するには、{:error, reply} を返す。今回はtopic名rooms、subtopic名joinにアクセス接続が合った場合無条件に参加を承認している。

バックエンドはこれでOKで、JS側を実装する。

フロント

  • web/static/js/app.js
import "../../../deps/phoenix_html/web/static/js/phoenix_html";
import Socket from "./socket";
  • web/static/js/socket.js
import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()

const channel = socket.channel("rooms:join", {})
channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

余談だけど、サンプルのJSには;がついていない。;付けない一派なんだろうか。

joinの確認

$ mix phoenix.server
$ open http://localhost:4000/ 

問題なければ下記のようにjoin成功のメッセージが出力されているはず。

f:id:bokuweb:20160322223048p:plain

Reactを使った簡易チャットの実装

バックエンド

room_channel.exに以下の関数を追加する。

  • web/channels/room_channel.ex
  def handle_in("new:message", message, socket) do
    broadcast! socket, "new:reply", %{msg: message["msg"]}
    {:noreply, socket}
  end

handle_in/3は受信メッセージをハンドルする関数で、今回はnew:messageというイベントを受け取った場合、メッセージ内容をブロードキャスト配信している。

https://hexdocs.pm/phoenix/Phoenix.Channel.html#c:handle_in/3

次にReactでマウントできるようweb/template/layout/app.tml.eexを掃除しておく。

  • web/template/layout/app.tml.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello PhoenixChannelSandbox!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
  </head>

  <body>
      <div id="root"></div>
      <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

フロント

  • Reactのインストール
$ npm i  -S react react-dom

送受信が行えるよう、socket.jsを変更しておく。

  • web/static/js/socket.js
import { Socket } from "phoenix";

export default class SocketTest {
  constructor(endpoint, token) {
    this.socket = new Socket(endpoint, { params: { token }});
    this.socket.connect();
  }

  connect(topic, msg = {}) {
    this.channel = this.socket.channel(topic, msg)
    this.channel.join()
      .receive("ok", resp => console.log("Joined successfully", resp))
      .receive("error", resp =>  console.log("Unable to join", resp));
  }

  send(msg) {
    this.channel.push("new:message", { msg }); // TODO: 
  }

  addListener(key, fn) {
    this.channel.on(key, fn);
  }
}

app.jsではフォームの描画とメッセージ受信時の更新、ボタン押下時のメッセージ送信を行う。 今回はnew:replyというイベントをListenして、コールバックでメッセージ内容をstateに詰めてる。

  • web/static/js/app.js
import "../../../deps/phoenix_html/web/static/js/phoenix_html";
import Socket from "./socket";
import React, { Component } from 'react';
import { render } from 'react-dom';

class Chat extends Component {
  constructor(props) {
    super(props);
    this.state = { messages: [] };
    this.socket = new Socket('/socket');
    this.socket.connect('rooms:join', {});
    this.socket.addListener("new:reply", messages => {
      this.setState({ messages: this.state.messages.concat(messages) });
    })
  }

  handleSubmit(e) {
    e.preventDefault();
    const message = this.refs.message.value.trim();
    if (!message) return;
    this.socket.send(message);
  }

  renderMessages() {
    return this.state.messages.map(message => <p>{message.msg}</p>);
  }

  render() {
    return (
      <div>
        <h1>Chat sample!!</h1>
        <form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
          <input type="text" ref="message" placeholder="message" />
          <input type="submit" value="Post" />
        </form>
        {this.renderMessages()}
      </div>

    );
  }
}

render(<Chat />, document.querySelector('#root'));

ひとまずこれで最低限のチャットは可能となる。

f:id:bokuweb:20160322210259g:plain

今回の成果物

github.com

TODO

  • データの永続化
  • 認証
  • 見た目
  • スクロールとか 検索とか
  • Herokuへのデブロイ
    • かなり検索したがまだ失敗している

参考記事

2015/07/09/Phoenix FrameworkのChannelを使ってTwtter Streamingをbroadcastする - ヽ(´・肉・`)ノログ

ruby-rails.hatenadiary.com

所感

少しElixir書いた