エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Phoenix.LiveView 事始め(subscribeとbroadcast)

前回の続きです。

前回、「バージョン 0.1 が公開され…」と書いたばかりなのに、今回の記事を書くまでの十日足らずの間にバージョン 0.2 が公開されてしまいました。

それはそれとして。

今回は Phoenix.LiveView の app で subscribe と broadcast を使って、複数のユーザ(ブラウザ)で通信させようという試みです。

使うサンプルはクリックしたセルの色が同期して変化する Phoenix app です。

モジュールを追加する手前まで進む

LiveView を利用できるように設定するところまでは同じですので前回の記事を参照しながら、「準備」「パッケージを追加する」「Endpoint を編集する」「ソケットのクライアントを設定する」「salt を設定する」まで進みます。

ここでは live_click という app 名で作成しています。

$ mix phx.new live_click --no-ecto

まずは単独の app を作成する

live モジュール

ディレクトlib/live_click_web/live を作成し、そこに click_live.ex を作成してモジュール LiveClickWeb.ClickLive を定義します。

defmodule LiveClickWeb.ClickLive do
  use Phoenix.LiveView

  def mount(_session, socket) do
    new_socket =
      socket
      |> assign(:effects, %{})
    {:ok, new_socket}
  end

  def render(assigns) do
    Phoenix.View.render(LiveClickWeb.ClickView, "index.html", assigns)
  end

  def handle_event("cell", %{"row" => row, "col" => col}, socket) do
    new_socket =
      socket
      |> assign_cell_color(String.to_integer(row), String.to_integer(col))
    {:noreply, new_socket}
  end

  def assign_cell_color(socket, row, col) do
    effects = socket.assigns.effects

    new_effect =
      case Map.get(effects, {row, col}) do
        :red -> :blue
        :blue -> :green
        :green -> :white
        _ -> :red
      end

    socket
    |> assign(:effects, Map.put(effects, {row, col}, new_effect))
  end
end

view モジュール

lib/live_click_web/views/click_view.ex を作成してモジュール LiveClickWeb.ClickView を定義します。

defmodule LiveClickWeb.ClickView do
  use LiveClickWeb, :view
end

テンプレートとスタイル

lib/live_click_web/templates/click/index.html.leex を作成してテンプレートを定義します。

<div>
  <%= Enum.map (0..9), fn row -> %>
    <div>
      <%= Enum.map (0..9), fn col -> %>
        <div class="cell <%= Map.get(@effects, {row, col}) %>" phx-click="cell" phx-value-row="<%= row %>" phx-value-col="<%= col %>">
          <%= row %><%= col %>
        </div>
      <% end %>
    </div>
  <% end %>
</div>

また assets/css/app.css を編集してスタイルを追加します。

@import "./phoenix.css";

/* ここから下を追加 */

.cell {
  display: inline-block;
  height: 50px;
  width: 50px;
  border: solid thin #ccc;
  text-align: center;
  line-height: 50px;
  margin-bottom: 5px;
}

.red {
  background-color: red;
}

.green {
  background-color: green;
}

.blue {
  background-color: blue;
}

.white {
  background-color: white;
}

ルーティングを変更する

lib/live_click_web/router.ex を編集します。

   scope "/", LiveClickWeb do
     pipe_through :browser
 
-    get "/", PageController, :index
+    live "/", ClickLive
   end

動作を確認する

iex コマンドあるいは mix コマンドで Phoenix app をローカルに起動し、http://localhost:4000 にアクセスして動作を確認します。

$ iex -S mix phx.server

セルをクリックすると、クリックしたセルの色がクリックのたびに 白 → 赤 → 青 → 緑 → 白 と変化すれば成功です。

subscribe と broadcast と handler を追加する。

Phoenix.LiveView 自体が WebSocket を利用しているため、ここから少しコードを追加するだけで他のブラウザにイベントを送ることができるようになります。

subscribe

イベントを subscribe するために LiveClickWeb.ClickLive.mount/2LiveClickWeb.Endpoint.subscribe(@topic) の一行を追加します。 これだけで broadcast されたイベントを受け取ることができるようになります。

  @topic "live_click:cells"

  def mount(_session, socket) do
    LiveClickWeb.Endpoint.subscribe(@topic)
    new_socket =
      socket
      |> assign(:effects, %{})
    {:ok, new_socket}
  end

関数の詳細はドキュメントを参照してください。

broadcast

イベントを broadcast するために LiveClickWeb.Endpoint.broadcast_from/4 の一行を追加します。 今回は LiveClickWeb.ClickLive. assign_cell_color/3 でローカルで発生したイベントを処理をしているのでここに追加します。

関数の第二引数には subscribe したトピックを文字列で、第三引数にはイベントを文字列で、第四引数には送信したいパラメータをマップで設定します。

  def assign_cell_color(socket, row, col) do
    effects = socket.assigns.effects

    new_effect =
      case Map.get(effects, {row, col}) do
        :red -> :blue
        :blue -> :green
        :green -> :white
        _ -> :red
      end

    LiveClickWeb.Endpoint.broadcast_from(self(), @topic, "click", %{key: {row, col}, value: new_effect})

    socket
    |> assign(:effects, Map.put(effects, {row, col}, new_effect))
  end

broadcast 元以外にイベントを送るために broadcast_from/4 を利用しましたが、全体に送るには broadcast/3 を利用します。

関数の詳細はドキュメントを参照してください。

handler

broadcast されたイベントはモジュール LiveClickWeb.ClickLive へのメッセージとして届きます。メッセージをハンドリングするため LiveClickWeb.ClickLive.handle_info/2 を記述します。

第一引数は broadcast の引数の内容を格納したマップになります。 :topic にトピックが、:event にイベントが、:payload にその他のパラメータが格納されます。

  def handle_info(%{topic: @topic, event: "click", payload: %{key: key, value: value}}, socket) do
    effects = socket.assigns.effects
    new_socket =
      socket
      |> assign(:effects, Map.put(effects, key, value))

    {:noreply, new_socket}
  end

関数の詳細はドキュメントを参照してください。

実行

app を起動し、ブラウザのウィンドウを二つ以上開きます。 一つのブラウザでクリックした内容が他のブラウザにも反映されることが確認できます。

一つ注意点。 このサンプルではセルの状態を永続化しているわけではないので、表示の内容のすべてが同期されるわけではありません。 あるセルをクリックしたことによるそのセルの状態の変化のみが伝わるようになっています。

いつか読むはずっと読まない:THE LONG WAY TO A ...

銀河共同体の中で、様々な星系出身の容姿も文化も価値観も違うクルーが一つの船で目的地に向かう。古典的なスペオペがわくわく感を誘います。

とはいえ。「古典的」とはあえて書いてみました。設定だけを切り出すと懐かしいスペオペの雰囲気があるのですが、書かれた時代背景が作品に現れるのはこの作品にも違いはなく、やはり現代の作品なのだな、と感じます。

銀河核へ 上 (創元SF文庫)

銀河核へ 上 (創元SF文庫)

銀河核へ 下 (創元SF文庫)

銀河核へ 下 (創元SF文庫)