3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

東京電力電力供給状況監視 - Phoenix LiveView

Last updated at Posted at 2022-10-20

以前書いた記事 「東京電力電力供給状況監視 - Phoenix Channel」LiveView バージョンです。前は Phoenix ChannelElm を使って実装しました。私の不慣れが大きいのですが、読み難いコードになってしまっています。今回はシンプルで読みやすいものを目指しました。SPA の範囲で言うと、LiveViewはとてもシンプルです。多分コードの生産性やメンテナンス性にとても優れていると感じられます。まあ、業務用アプリなどはまだ無理かもしれませんが。

【関連記事】
Phoenix LiveView と キーボードイベント - Qiita
Phoenix LiveView の JavaScript Hook - Qiita
Phoenix LiveViewの基本設定 - Qiita
Phoenix1.6の基本的な仕組み - Qiita

実行環境
Elixir 1.13.0
Phoenix 1.6.12

1.LiveViewの基本設定

まずプロジェクトを作成します。

mix phx.new liveview_denki --no-ecto --no-mailer --no-dashboard
cd liveview_chart

次に慣例に従ってliveディレクトリを作成します

mkdir lib/liveview_denki_web/live

LiveView controllerdenki_live.ex という名前で作成します。

lib/liveview_denki_web/live/denki_live.ex
defmodule LiveviewDenkiWeb.DenkiLive do
  use LiveviewDenkiWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, socket}
  end
end

ここではrender の明示的な設定は行わず denki_live.html.heex という名前を合わせたファイルを作ることで暗示的に行います。

lib/liveview_denki_web/templates/layout/root.html.heex
<h1>LiveView Denki Page</h1>

root.html.heex の body を以下のもので置き換えます。

lib/liveview_denki_web/templates/layout/root.html.heex
  <body>
    <header>
      <section class="container">
        <h1>LiveView 東京電力電力供給状況監視</h1>
      </section>
    </header>
    <%= @inner_content %>
  </body>

router.ex パスを修正して、"/"パスを LiveView controller を指すようにします。

lib/liveview_chart_web/router.ex
  scope "/", LiveviewChartWeb do
    pipe_through :browser

    live "/", DenkiLive
  end

基本設定が終わったのでサーバを起動します。

mix phx.server

http://localhost:4000/ にアクセスし確認します。

2.JavaScript Hook

これからは実際にchartjsを実装し、サーバから送られるイベントに従ってチャートをリアルタイムに更新していくことを考えていきます。

chart.jsをインストールします。

npm install --save --prefix assets chart.js
lib/liveview_denki_web/live/denki_live.html.heex
<style>
  #chart-wrapper {
    display: inline-block;
    position: relative;
    width: 100%;
  }
</style>
<div id="chart-wrapper">
  <canvas id="myChart" phx-hook="chart"></canvas>
</div>
assets/js/chart_hook.js
import Chart from 'chart.js/auto';

const labels = [ '-45', '-40', '-35', '-30', '-25', '-20', '-15', '-10', '-5', 'now'];

const data = {
  labels: labels,
  datasets: [{
    label: '東京電力電力供給状況監視',
    backgroundColor: 'rgb(255, 99, 132)',
    borderColor: 'rgb(255, 99, 132)',
    data: [3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000],
  }]
};

const config = {
  type: 'line',
  data: data,
  options: {}
};

let hooks = {}
hooks.chart = {
    mounted() {
        var ctx = this.el
        var chart = new Chart(ctx, config)
        this.handleEvent("points", ({points}) => {
            chart.data.datasets[0].data = points
            chart.update()
        })
    }
}

export default hooks

app.jsでは上で定義したhookを読み込んで、LiveSocketに設定します。

assetss/js/app.js
---
import chart_hooks from './chart_hook';  // 追加

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")

let liveSocket = new LiveSocket("/live", Socket, 
    {params: {_csrf_token: csrfToken}, hooks: chart_hooks})  // 修正
---

ここで必要なライブラリをインストールします、

mix.exs
  defp deps do
    [
       ---
      {:httpoison, "~> 1.8"}     # 追加
    ]
  end

以下のコマンドを発行します。

mix deps.get

LiveView controller のchart_live.exを以下のように変更します。
mountは2度呼ばれますが、2度目にsocketがつながった状態で呼ばれます。この時、5分おきに自分にupdate_chartイベントを送信するタイマーを設定します。電力情報が5分おきに更新されるので。

lib/liveview_denki_web/live/denki_live.ex
defmodule LiveviewDenkiWeb.DenkiLive do
  use LiveviewDenkiWeb, :live_view

  @denki_url "http://tepco-usage-api.appspot.com/quick.txt"

  def mount(_params, _session, socket) do
    if connected?(socket) do
      :timer.send_interval(300000, self(), :update_chart)
    end
    init_points = dummy_points()
    socket = assign(socket, points: init_points)
    {:ok, socket}
  end

  def handle_info(:update_chart, %{assigns: assigns}=socket) do
    %{points: [_|points]} = assigns
    denki = get_denki()
    [_, d, _] = String.split(denki, ",")

    new_points = points ++ [d]
    socket = assign(socket, points: new_points)
    socket = push_event(socket, "points", %{points: new_points})
    {:noreply, socket}
  end

  defp get_denki() do
    case HTTPoison.get(@denki_url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> body
      {:ok, %HTTPoison.Response{status_code: 404}} -> "0,0,0"
      {:error, %HTTPoison.Error{reason: reason}} -> "0,0,0"
    end
  end

  defp dummy_points, do: 1..10 |> Enum.map(fn _ -> 3000 end)
end

get_denki()の戻り値は "19:55,3116,4082" のような形で2番目の値が電力の値となっています。ここではコードをできるだけシンプルにするため、電力の値だけを使い、時刻情報などは使いません。

設定が終わったのでサーバを起動します。

mix phx.server

http://localhost:4000/ にアクセスし確認します。

image.png

3.GenServer

LiveView controller の chart_live.ex から状態管理とロジックの部分を、別プロセスの GenServer に切り出すことを考えます。両者の通信は PubSub を使います。以下のようなメリットが考えられます。

  • (1) LiveView controllerGenServerの役割が分けるこどで、コードがシンプルに理解しやすいものになる。
  • (2) GenServer が常に直近の45分の値を保持しているので、ブラウザ立ち上げ時にすぐにそれらの値を利用することができる。
  • (3) 2つ以上のブラウザを立ち上げても、グラフの遷移を同期させることができる。

個人的には私は(1)を最重要視しています。十分シンプルじゃないとコードが正しいか理解できないからです。「コードが正しい」と思えることが全てに優先されます。私は正しいプログラマじゃないですね。

先ずは GenServer です。状態管理とロジックの仕事を担います。LiveViewとは関係のないコードになっていますので、理解しやすいです。

【GenServerの過去記事】
Elixir Concurrent Programming(2) - GenServer

lib/liveview_denki/denki_listener.ex
defmodule LiveviewDenki.DenkiListener do
  use GenServer
  alias Phoenix.PubSub
  @denki_url "http://tepco-usage-api.appspot.com/quick.txt"

  def start_link(_) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @impl true
  def init(_) do
    PubSub.subscribe(LiveviewDenki.PubSub, "liveview_denki")
    :timer.send_interval(300000, self(), :update_state)
    {:ok, dummy_points}
  end

  @impl true
  def handle_info(:update_state, [_|state]) do
    denki = get_denki()
    [_, d, _] = String.split(denki, ",")
    new_state = state ++ [d]
    PubSub.broadcast(LiveviewDenki.PubSub, "liveview_denki", %{points: new_state})
    {:noreply, new_state}
  end

  @impl true
  def handle_info(%{points: points}, socket) do
    # do nothing
    {:noreply, socket}
  end

  defp dummy_points, do: 1..10 |> Enum.map(fn _ -> 3000 end)

  defp get_denki() do
    case HTTPoison.get(@denki_url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> body
      {:ok, %HTTPoison.Response{status_code: 404}} -> "0,0,0"
      {:error, %HTTPoison.Error{reason: reason}} -> "0,0,0"
    end
  end
end

次に LiveView controller からロジックや状態管理部分を追い出します。だいぶシンプルになりました。これが LiveView controller の仕事の本質と考えられます。

lib/liveview_denki_web/live/denki_live.ex
defmodule LiveviewDenkiWeb.DenkiLive do
  use LiveviewDenkiWeb, :live_view
  alias Phoenix.PubSub

  def mount(_params, _session, socket) do
    if connected?(socket) do
      PubSub.subscribe(LiveviewDenki.PubSub, "liveview_denki")
    end
    {:ok, socket}
  end

  def handle_info(%{points: points}, socket) do
    socket = push_event(socket, "points", %{points: points})
    {:noreply, socket}
  end
end

最後に application.ex の children に GenServer を追加して、サーバ起動時に自動的に起動されるようにします。

lib/liveview_denki/application.ex
  @impl true
  def start(_type, _args) do
    children = [
      ---
      {LiveviewDenki.DenkiListener, nil}   # 追加
    ]

設定が終わったのでサーバを起動します。

mix phx.server

http://localhost:4000/ にアクセスし確認します。

image.png

2個のブラウザが同期してグラフを遷移させていくのが確認できます。

今回は以上です。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?