Phoenix LiveView でゲージを動かしてみた

## はじめに

Phoenix LiveView を使ったことがないので、やってみました

https://elixirschool.com/blog/phoenix-live-view/

せっかくなので、リアルタイムに動いてる感が出るものを作ってみます

完成品は以下のイメージです

一定間隔でカウントアップし、それに応じてゲージが伸びます

![liveview.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/11ec8b42-a781-356d-3816-7337789c32fa.gif)

実装したコードの全量はこちら

https://github.com/RyoWakabayashi/phoenix-liveview-example

## Phoenix LiveView とは

ざっくり言うと、 Elixir でフロントエンドを書いてしまおう、というもの

- バックエンドもフロントエンドも全て Elixir で書くことができるため、  __JS を書く必要がない__

- バックエンドとフロントエンドが統合されており、 __API を作る必要がないため、開発が速い__

- バックエンドで HTML を生成するため、 __フロントエンドのサイズが小さくなる__

- データが変更されたとき、自動的に差分だけを抽出して送信するため、 __通信量が少なくなる__

## 開発環境

OS: macOS Monterey 12.2.1
Elixir: 1.13.3
Erlang: 23.3.4

## カウンターの実装

まずは以下のサイトを参考に単純なカウンターを実装しました

https://github.com/dwyl/phoenix-liveview-counter-tutorial

README の通りに実装し、問題なく動作しました

二つウィンドウを開き、片方で更新した内容がリアルタイムにもう片方に反映されています

詳しくは上記のサイトを参照してください

![counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/46367327-966a-41b5-4bb0-74e403d44063.gif)

WebSocket の通信内容を見てみましょう

- Chrome のデベロッパーツールを開く
- Network タブを開く
- フィルターアイコンをクリックする
- Filter の中から WS を選択する
- 再読込したあとカウンターを操作すると、更新時の WebSocket の内容が見える

![スクリーンショット 2022-03-08 13.38.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f1bb4191-04bc-f48c-fcb3-e3459b35494b.png)

```json
[
  "4",
  "9",
  "lv:phx-FtpNuytuwcs7iLOI",
  "phx_reply",
  {
    "response": {
      "diff": {
        "0": "9"
      }
    },
    "status": "ok"
  }
]
```

`diff` の項目が `"0": "9"` になっており、ここが差分であろうと思われます

## ゲージの実装

まず、以下のような形に実装してみました（一部を抜粋）

lib/live_view_example/gauge.ex

```elixir
defmodule LiveViewExample.Gauge do

  ...

  def init(start_num_blocks) do
    # 一定間隔で :incr を呼び出す
    :timer.send_interval(1000, :incr)
    {:ok, start_num_blocks}
  end

  ...

  def handle_info(:incr, num_blocks) do
    # カウントアップし、 99 を超えたら 0 にする
    new_num_blocks =
      if num_blocks > 99 do
        0
      else
        num_blocks + 1
      end

    # カウント結果をフロントエンドに送信
    PubSub.broadcast(LiveViewExample.PubSub, topic(), {:num_blocks, new_num_blocks})
    {:noreply, new_num_blocks}
  end
end
```

lib/live_view_example_web/live/gauge_live.ex

```elixir
defmodule LiveViewExampleWeb.GaugeLive do

  ...

  defmodule Block do
    defstruct [:id, :class_name]
  end

  def mount(_params, _session, socket) do
    PubSub.subscribe(LiveViewExample.PubSub, @topic)

    # 現在の値を取得
    num_blocks = Gauge.current()

    # ブロックを生成
    blocks =
      num_blocks
      |> generate_blocks

    {:ok, assign(socket, num_blocks: num_blocks, blocks: blocks)}
  end

  def handle_info({:num_blocks, num_blocks}, socket) do
    # ブロックを生成
    blocks =
      num_blocks
      |> generate_blocks

    {:noreply, assign(socket, num_blocks: num_blocks, blocks: blocks)}
  end

  def generate_blocks(num_blocks) do
    # 100個のブロックをゲージの値に応じて生成
    Enum.map(0..99, fn index ->
      %Block{id: "block-#{index}", class_name: class_name(index, num_blocks)}
    end)
  end

  def class_name(index, num_blocks) do
    cond do
      index > num_blocks ->
        # ゲージの値未満は非表示
        "gauge-block none"

      index >= 79 ->
        # 79以上は色を変える
        "gauge-block high"

      true ->
        # それ以外は通常のブロック
        "gauge-block"
    end
  end
end
```

lib/live_view_example_web/templates/gauge/index.html.heex

```html
<div class="gauge-main">
  <div class="gauge-outer">
    <div class="gauge-text">
      <%= @num_blocks %>
    </div>
    <div class="gauge-container">
      <!-- ブロックを繰り返し表示 -->
      <%= for block <- @blocks do %>
        <div class={block.class_name}></div>
      <% end %>
    </div>
  </div>
</div>
```

結果、以下のように動きました

![gauge.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d00ca854-e192-b78c-3962-05b43c5646ed.gif)

WebSocket の中身は以下のようになっていて、長さは 1,800 程度になっています

```json
[
  "4",
  null,
  "lv:phx-FtpOuSyAcUisSwBC",
  "diff",
  {
    "0": "12",
    "1": {
      "d": [
        ["gauge-block"],
        ["gauge-block"],
        ...
        ["gauge-block none"],
        ["gauge-block none"]
      ]
    }
  }
]
```

生成された div 毎（ 100 個）にクラス名が送られてきていることがわかります

実際に変わっているのは毎秒 1 つの div だけのはずですが、思ったより多くの変更が来てしまっています

## LiveComponent

より差分を小さくできないかと探ったところ、 LiveComponent を使うと良い、という情報がありました

https://elixirforum.com/t/liveview-re-renders-all-assigns-inside-for-loop-not-only-the-ones-that-changed/37587/4

ゲージを LiveComponent を使って実装し直します

新しく　 GaugeBlock を追加します

lib/live_view_example_web/live/gauge_block.ex

```elixir
defmodule LiveViewExampleWeb.GaugeBlock do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~H"""
    <div id={@id} class={@class_name}></div>
    """
  end
end
```

ゲージのテンプレートを GaugeBlock を使った形に変更します

lib/live_view_example_web/templates/gauge/index.html.heex

```elixir
<div class="gauge-main">
  <div class="gauge-outer">
    <div class="gauge-text">
      <%= @num_blocks %>
    </div>
    <div class="gauge-container">
      <%= for block <- @blocks do %>
        <%= live_component LiveViewExampleWeb.GaugeBlock,
          id: block.id,
          class_name: block.class_name %>
      <% end %>
    </div>
  </div>
</div>
```

そうすると、差分は 600 程度まで小さくなりました

![Mar-08-2022 15-08-37.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/80bfda39-1a95-f0b4-f9a9-a705c35093da.gif)

```json
[
  "4",
  null,
  "lv:phx-FtpSVTth9Zi-LQSC",
  "diff",
  {
    "0": "83",
    "1": {
      "d": [
        [1],
        [2],
        ...
        [99],
        [100]
      ]
    },
    "c": {
      "84": {
        "0": "gauge-block high"
      }
    }
  }
]
```

これでも差分が大きいですが、、、

まだ実装が悪いのかもしれません

## 所感

LiveView は非常に面白い仕組みだと思いますが、実務で使う機会はなかなか無いかなあ、と感じました

- LiveView で差分を抑えるのにも工夫が必要
- ネットワークの遅延に弱く、オフラインでは手も足も出ない
- 関数型言語でフロントエンドを実装したいなら [Elm] がいい
- 結局、スタイルシートの理解は必須
- よりリッチな UI にしようとしたら無理が出てきそう
- フロントエンドに Web 以外のもの（モバイルアプリや他システム連携）を追加できない
- ある程度以上の規模だとバックエンドとフロントエンドは分業することになるため、使えない
- フロントエンドが軽くなる分、バックエンドが重くなってしまう
- モックなど、フロントエンドの動きだけ見せたいときであっても、 Phoenix を動かすための環境が必要になる

使えそうなケース

- 元々 Elixir ばかり使っているチームでフロンエンド経験者不在（体制を考え直した方が良い）
- Bootstrap だけで何とかなるくらいの入力フォームや検索機能
- ステータス監視用のコンソール
- Elixir の学習用

LiveView の使うべきケースと使うべきでないケースについて書かれた記事があります

https://quatermain32.medium.com/where-to-not-use-phoenix-liveview-7fb5ffb8318b

Negative use cases が多すぎませんかね、、、

結局のところ、 __フロントエンドエンジニアがいなくてもフロントエンドを実装できる__
というメリットが一番大きそうですが、
フロントエンドが書けるならあえてこれを選択する理由はなさそうです

[elm]: https://elm-lang.org/