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


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

**【関連記事】**
[Phoenix LiveView と キーボードイベント - Qiita](https://qiita.com/sand/items/a72a74ce7885a80eb969)
[Phoenix LiveView の JavaScript Hook - Qiita](https://qiita.com/sand/items/66049160bf5309e272ea)
[Phoenix LiveViewの基本設定 - Qiita](https://qiita.com/sand/items/5aea96852ceb2981021b)
[Phoenix1.6の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)


**実行環境**
Elixir 1.13.0 
Phoenix 1.6.12

# １．LiveViewの基本設定

まずプロジェクトを作成します。
```
mix phx.new liveview_denki --no-ecto --no-mailer --no-dashboard
cd liveview_chart
```

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

**LiveView controller** を **denki_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** という名前を合わせたファイルを作ることで暗示的に行います。

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

**root.html.heex** の body を以下のもので置き換えます。
```elixir: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** を指すようにします。

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

    live "/", DenkiLive
  end
```

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

```
mix phx.server
```

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



# ２．JavaScript Hook


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



**chart.js**をインストールします。
```
npm install --save --prefix assets chart.js
```



```elixir: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>
```


```elixir: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に設定します。


```elixir: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})  // 修正
---
```

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

```elixir: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分おきに更新されるので。


```elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/f7a60e8b-d527-5c2a-9eea-56475406c1ad.png)


# ３．GenServer

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

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

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


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

**【GenServerの過去記事】**
[Elixir Concurrent Programming(2) - GenServer](https://qiita.com/sand/items/5fd91c0b86b4919d7cc9)


```elixir: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** の仕事の本質と考えられます。

```elixir: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** を追加して、サーバ起動時に自動的に起動されるようにします。 

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

```

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

```
mix phx.server
```

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


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/a0c24f61-3a27-ace5-0f89-ab2b80210edf.png)


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

今回は以上です。