🧊

PhoenixのChannels/Presenceについて

を、絶賛Elixirかじり中のフロントエンドなエンジニアが使ってみての学びや気付きなどのメモ。

まずはPhoenixのアプリを

星の数ほどある記事を参考にして作る。
ただ純API・WebSocketサーバーとしてPhoenixを使う場合は、`phx.new`する時にオプションでアレコレを捨てておく。

mix phx.new ch_example --no-brunch --no-ecto --no-html

Channels

さてそんなElixirのRails的ポジションであるPhoenixには、WebSocketをいい感じにラップしたサーバーサイド実装が機能として入ってて、それをChannelsっていう。

Channels – Phoenix v1.3.0

サーバーサイドでちょろっとコードを書くだけで、簡単にルームが作れてしまうという優れもの。

クライアントサイドのSDKも用意があって、公式はJavaScriptのみやけど、3rdからSwiftとかJavaとかC#とかのがあるっぽい。

サーバーでやること

  • Endpointに`socket`のモジュールをリンク
  • Socketでルームとしての`channel`やら`transport`をリンク
  • Channelでやり取りするメッセージやらJOIN時のふるまいを定義

このあたりはサンプルも山ほどあるしすぐできる。

クライアントでやること

JavaScriptのたったこれだけでできちゃう!

import { Socket } from './phoenix.js';

// サーバー側で指定すればLongPollingにもできる
const socket = new Socket('ws://localhost:4000/socket');
socket.connect();

// `room:`から始まる名前じゃないとダメらしい
const channel = socket.channel('room:xxx');

// 見ての通り受け
channel.on('msg', payload => {
  console.log(payload);
});

// 送りたい時はこれ
channel.push('msg', { body: msg });

// 何はともあれ部屋に入る
channel.join()
  .receive('ok', resp => {
    console.log('Joined successfully', resp);
  })
  .receive('error', resp => {
    console.log('Unable to join', resp);
  });

まあそうですよねという感じ。

Presence

日本語記事はまったく見つからなかったけども、Channelsと並んで使えそうなPresenceという機能もある。

これが何かというと、その名の通り接続中のクライアントの情報を取得するためのもの。オンラインなのは何人?とかそういう。

サーバーでやること

mix phx.gen.presence

これで`/channels`の下にファイルができる。
そしてこれを、ApplicationのSupervisorにぶら下げて起動するように。

って、↑のコマンドを叩いたら標準出力で教えてくれる。

あとはさっきのChannelsのサンプルに少し足す。

誰が接続してるのかを識別しないといけないので、それを見るように。

# user_socket.ex
def connect(params, socket) do
  {:ok, assign(socket, :user_id, params["user_id"])}
end

ちなみにトークンで接続時に認証かけたいときとかもココに書く。

あとはルーム側。

def join("room:xxx", _params, socket) do
  # これが↓のハンドラに飛ぶ
  send(self(), :after_join)
  {:ok, socket}
end

# ルームに接続した!タイミングのフック
def handle_info(:after_join, socket) do
  push(socket, "presence_state", Presence.list(socket))
  {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
    online_at: inspect(System.system_time(:seconds))
  })
  {:noreply, socket}
end

サーバーサイドはこれで以上。

クライアントでやること

import { Socket, Presence } from './phoenix.js';

const socket = new Socket('ws://localhost:4000/socket', {
  // 本来はちゃんとした一意に識別するなにか
  params: { user_id: window.location.hash, },
});
socket.connect();

const channel = socket.channel('room:xxx');

// Elixirが関数型な影響で、JavaScriptでもそう書いちゃうらしい
let presences = {};
// 初期の状態はこのイベント(一度きり)
channel.on('presence_state', state => {
  presences = Presence.syncState(presences, state);
  console.log('state', presences);
});
// 以降、なにか差分が出たらこっちのイベント
channel.on('presence_diff', diff => {
  presences = Presence.syncDiff(presences, diff);
  console.log('diff', presences);
});

channel.join()
  .receive('ok', resp => { console.log('Joined successfully', resp); })
  .receive('error', resp => { console.log('Unable to join', resp); });

`presence_diff`では、差分が`{ leaves: {}, joins: {} }`ってな感じで送られてくる。
なので`syncDiff()`に現状とあわせて渡すことで、`presence_state`時と同じようにたたんでくれる。

ちなみに、`Presence.list()`なるメソッドも用意されてる。

// ただfnでイテレートできるだけの関数
Presence.list(presences, fn);

自分たちで差分を管理するぜ!って場合は、`Precense`自体必要ない。
JavaScript側のコードが別に関数型を指向してないときは、なんかココ気持ち悪いな・・ってなる。

おわりに

フロントエンドとしては、別にElixirでPhoenixだろうがNode.jsでSocket.IOだろうが、どちらにせよただのWebSocketなので大差ない感じ。

PhoenixのJSは大した長さじゃないので、自分で書いてもいいとは思う。
もうこのご時世なのでLongPollingは捨てる!って場合、SDKのコードを半分くらい減らせる(˘ω˘ )

phoenix/phoenix.js at master · phoenixframework/phoenix · GitHub

なんでもいいけどこのSDKのコード、ファイル分けてくれんかな・・。