Phoenixは、Elixirでつくられたwebフレームワークです。Channelの機能を使えば、リアルタイム通信ができます。昨年のgumi Inc. Advent Calendar 2018の「Elixir/PhoenixとReactをWebsocketでつないでみる」
では、Websocketをつないだ簡単なチャットアプリケーションが紹介されました。本稿はその前半部分、Phoenixプロジェクトによるチャットルームのつくり方について少し説明を加えます。今回は、Elixir 1.8.0とPhoenix 1.4.2を使いました。
Phoenixプロジェクトをつくる
Phoenixのインストールについては、公式サイトの「Installation」をお読みください。まずは、mix phx.new
コマンドでPhoenixプロジェクトをつくります(プロジェクト名chat
)。--database
オプションはEctoに用いるデータベースアダプタの定めです。今回はmysql
としました(デフォルト値はpostgres
)。なお、このチャットアプリケーションでは、データベースの機能は使いません。
mix phx.new chat --database mysql
アプリケーションがつくられ始めると、Fetch and install dependencies? [Yn]
と尋ねられます。Y
を入力すると、依存関係もインストールされるのでお手軽です。
Fetch and install dependencies? [Yn] y
* running mix deps.get
* running mix deps.compile
* running cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development
イントールの終わりに、ローカルサーバーでアプリケーションを立ち上げる手順が示されます。指示にしたがってコマンドを入力したあとhttp://localhost:4000
を開けば、ひな形のページが表示されるでしょう(図001)。
cd chat
mix ecto.create
mix phx.server
図001■Phoenixプロジェクトのひな形ページ
channel
を使う
Phoenixプロジェクトのlib/chat_web/endpoint.ex
には、つぎのようにPhoenix.Endpoint
ビヘイビアを用いてすでにエンドポイントが加えられています。
defmodule ChatWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :chat
socket "/socket", ChatWeb.UserSocket,
websocket: true,
longpoll: false
end
そして、エンドポイントのモジュールChatWeb.UserSocket
を定めているのが、lib/chat_web/channels/user_socket.ex
です。Phoenix.Socket
ビヘイビアが使われています。このモジュールで、メッセージを適切なChannelにルーティングしなければなりません。そのために、channel/3
が呼び出されるコードのコメントアウトを外してください。
defmodule ChatWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", ChatWeb.RoomChannel # コメントアウトを外す
end
これで、クライアントから"room:"ではじまるトピックでメッセージを送れば、ChatWeb.RoomChannel
モジュールにルーティングされます。
クライアントの接続を認証する
チャットルームのメッセージを管理するChatWeb.RoomChannel
は、lib/chat_web/channels/room_channel.ex
として新たに定めます。モジュールはPhoenix.Channel
ビヘイビアを用い、クライアントがトピックに接続することを認証しなければなりません。認証のために実装するのが、join/3
です。
チャットルーム"room:lobby"には、誰でも接続できるようにします。そのほかのチャットルームは、プライベートです。実際には、データベースなどによる認証を行うことになるでしょう。ここでは、エラーを返すだけにします。接続を認証する戻り値は、{:ok, socket}
です。拒否する場合には、{:error, reply}
を返します。これで、Channelの準備は整いました。
defmodule ChatWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "authorized"}}
end
end
Phoenixプロジェクトは、assets/js/socket.js
にソケットを実装した簡単なクライアントが定められています。接続するには、正しいルーム名("room:lobby"
)に書き替えるだけです。ソケットの接続について詳しくは「Socket Connection」をご参照ください。
socket.connect()
// Now that you are connected, you can join channels with a topic:
// let channel = socket.channel("topic:subtopic", {}) // 以下に修正
const channel = socket.channel("room:lobby", {});
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket
-
connect()
: ソケットに接続します。 -
channel()
: 第1引数のトピックのChannelを初期化します。第2引数はChannelに渡されるパラメータです。 -
join()
: Channelに接続します。戻り値はPush
です。 -
receive()
: 第1引数のステータスに対して、第2引数のコールバックを呼び出します。
ソケットへの接続準備が整いましたので、アプリケーションのassets/js/app.js
にimport
します。コメントアウトされているつぎのコードを有効にしてください。
import socket from "./socket" // コメントアウトを外す
これで、クライアントとサーバーが接続されます。ブラウザのコンソールには、"Joined successfully"と示されているはずです。channel()
の第1引数に渡すトピックを違う名前に書き替えれば、"Unable to join"というエラーが表示されるでしょう。
チャットメッセージを送る
チャットができるように、ページにテキスト入力フィールドを加えます(図002)。テンプレートは、lib/chat_web/templates/page/index.html.eex
です。もとの中身はすべて消してしまって構いません。つぎのコード001の2行に置き替えます。<ul>
要素は、送られてきたメッセージをあとで加える場所です。
コード001■lib/chat_web/templates/page/index.html.eex
<ul id="messages"></ul>
<input id="chat-input" type="text">
図002■テキスト入力フィールドが加わったページ
assets/js/socket.js
には、サーバーとやりとりするためのコードを以下のように書き加えます。テキスト入力フィールドで[return]/[Enter]キーが押されたら、push()
メソッドでメッセージを送ります。第1引数はイベント名で、第2引数のオブジェクトが入力フィールドのテキストを納めたメッセージ本文です。
on()
メソッドは、メッセージを受け取ります。第1引数にやはり待ち受けるイベント名を与え、第2引数のリスナー関数で受け取ったメッセージの処理を定めます。とりあえず、console.log()
メソッドで中身を確かめることにしました。
const channel = socket.channel('room:lobby', {});
// 追加↓
const chatInput = document.querySelector('#chat-input');
const messagesContainer = document.querySelector('#messages');
chatInput.addEventListener('keypress', (event) => {
if (event.keyCode === 13) {
channel.push('new_msg', { body: chatInput.value });
chatInput.value = '';
}
});
channel.on('new_msg', (payload) => {
console.log(payload.body); // 確認用
});
Channel経由で送られてきたメッセージを扱うのはコールバックhandle_in/3
です。第1引数のイベントでパターンマッチングし、第2引数にマップでメッセージ本文を受け取ります。第3引数はPhoenix.Socket
です。コールバックはlib/chat_web/channels/room_channel.ex
に以下のように定めます。
handle_in/3
から呼び出しているbroadcast!/3
は、リスナーにイベントを送ります。第1引数はソケット、第2引数がイベントで、第3引数は送るメッセージです。
defmodule ChatWeb.RoomChannel do
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
ページのフィールドに入力したテキストをクライアントから送ると、handle_in/3
が呼び出すbroadcast!/3
によりイベントとして配信されます。すると、assets/js/socket.js
でchannel.on()
により定められたリスナー関数が呼び出されるのです。メッセージとして送られた入力フィールドのテキストがコンソールに出力されるでしょう。lib/chat_web/channels/room_channel.ex
の中身は、つぎのコード002にまとめました。
コード002■lib/chat_web/channels/room_channel.ex
defmodule ChatWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "authorized"}}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
assets/js/socket.js
のリスナー関数を完成させましょう。channel.on()
の第2引数に渡すアロー関数式を、確認用のステートメントからつぎのコード003のように書き替えます。動的につくられた要素にメッセージ本文のテキストが加わって、ページに差し込まれ、簡単なチャットアプリケーションの動きができ上がりました(図003)。
コード003■assets/js/socket.js
// Now that you are connected, you can join channels with a topic:
// let channel = socket.channel("topic:subtopic", {}) // 以下に修正
const channel = socket.channel('room:lobby', {});
// 追加↓
const chatInput = document.querySelector('#chat-input');
const messagesContainer = document.querySelector('#messages');
chatInput.addEventListener('keypress', (event) => {
if (event.keyCode === 13) {
channel.push('new_msg', { body: chatInput.value });
chatInput.value = '';
}
})
channel.on('new_msg', (payload) => {
const messageItem = document.createElement('li');
messageItem.innerText = [</span><span class="p">${</span><span class="nc">Date</span><span class="p">()}</span><span class="s2">] </span><span class="p">${</span><span class="nx">payload</span><span class="p">.</span><span class="nx">body</span><span class="p">}</span><span class="s2">
;
messagesContainer.appendChild(messageItem);
})
// 追加↑
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
Top comments (0)