はじめに
Phoenix LiveView を使ったことがないので、やってみました
せっかくなので、リアルタイムに動いてる感が出るものを作ってみます
完成品は以下のイメージです
一定間隔でカウントアップし、それに応じてゲージが伸びます
実装したコードの全量はこちら
Phoenix LiveView とは
ざっくり言うと、 Elixir でフロントエンドを書いてしまおう、というもの
-
バックエンドもフロントエンドも全て Elixir で書くことができるため、 JS を書く必要がない
-
バックエンドとフロントエンドが統合されており、 API を作る必要がないため、開発が速い
-
バックエンドで HTML を生成するため、 フロントエンドのサイズが小さくなる
-
データが変更されたとき、自動的に差分だけを抽出して送信するため、 通信量が少なくなる
開発環境
OS: macOS Monterey 12.2.1
Elixir: 1.13.3
Erlang: 23.3.4
カウンターの実装
まずは以下のサイトを参考に単純なカウンターを実装しました
README の通りに実装し、問題なく動作しました
二つウィンドウを開き、片方で更新した内容がリアルタイムにもう片方に反映されています
詳しくは上記のサイトを参照してください
WebSocket の通信内容を見てみましょう
- Chrome のデベロッパーツールを開く
- Network タブを開く
- フィルターアイコンをクリックする
- Filter の中から WS を選択する
- 再読込したあとカウンターを操作すると、更新時の WebSocket の内容が見える
[
"4",
"9",
"lv:phx-FtpNuytuwcs7iLOI",
"phx_reply",
{
"response": {
"diff": {
"0": "9"
}
},
"status": "ok"
}
]
diff
の項目が "0": "9"
になっており、ここが差分であろうと思われます
ゲージの実装
まず、以下のような形に実装してみました(一部を抜粋)
lib/live_view_example/gauge.ex
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
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
<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>
結果、以下のように動きました
WebSocket の中身は以下のようになっていて、長さは 1,800 程度になっています
[
"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 を使うと良い、という情報がありました
ゲージを LiveComponent を使って実装し直します
新しく GaugeBlock を追加します
lib/live_view_example_web/live/gauge_block.ex
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
<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 程度まで小さくなりました
[
"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 の使うべきケースと使うべきでないケースについて書かれた記事があります
Negative use cases が多すぎませんかね、、、
結局のところ、 フロントエンドエンジニアがいなくてもフロントエンドを実装できる
というメリットが一番大きそうですが、
フロントエンドが書けるならあえてこれを選択する理由はなさそうです