勉強用の題材としてブログを作りはじめた。
ブログの更新やコメントにも使用できるので、次はchannelを使用して簡単なチャットを作ってみる。Channelとは簡単にwebsocket通信ができる機能で、Node.jsにおけるSocket.io的なものと理解している。
動作環境
- Erlang 17.5
- Elixir 1.2.0
- Phoenix 1.1.4
- node.js 4.2.1
環境構築
前回同様。
プロジェクト名は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.ex
にsocket
用のエンドポイントが用意されている。
lib/chat_phoenix/endpoint.ex
defmodule PhoenixChannelSandbox.Endpoint do use Phoenix.Endpoint, otp_app: :phoenix_channel_sandbox socket "/socket", PhoenixChannelSandbox.UserSocket
これにより、エンドポイント/socket
にアクセスするとUserSocket
モジュール に接続されることになる。
UserSocket
はweb/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_channel
をweb/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/3
はchannel
への参加を承認/拒否する関数で、{: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成功のメッセージが出力されているはず。
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'));
ひとまずこれで最低限のチャットは可能となる。
今回の成果物
TODO
- データの永続化
- 認証
- 見た目
- スクロールとか 検索とか
- Herokuへのデブロイ
- かなり検索したがまだ失敗している
参考記事
2015/07/09/Phoenix FrameworkのChannelを使ってTwtter Streamingをbroadcastする - ヽ(´・肉・`)ノログ
所感
少しElixir書いた