Phoenix LiveViewへのサービスリプレイスの際に既存コンテンツからの段階的な移行を考えてみる


この記事は、[fukuoka.ex Elixir／Phoenix Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fukuokaex) 10日目です。
前日は、 @koga1020  さんの[Elixirのfor文をおさらいしてみる](https://zenn.dev/koga1020/articles/903d6a6bdde26b) でした

# 前書きとやりたいこと

前から気になっていたLiveViewを触りたいのですが、
折角なので仕事で利用する上で気になっている事をついでに考えてみたいと思います。

JSフレームワークなどで既に作られたサービスをLiveViewを使ってリプレイスする場合、
既にあるコンテンツを一気に置き換えるような期間や予算が取れない場合もあるかと思います。
既存のコンテンツを共存させつつ、一部をLiveViewに置き換えていくような事ができないかと考えています。

具体的には以下のふたつのユースケースについて現状を探ってみたいと思います。

- LiveViewで描画するページの中で、既存のJSコンポーネントを使用する（今回はGoogleMapAPI）
- 既存のWebサイトの一部としてLiveViewで描画したコンテンツを表示する(今回はiframeを使用)



# 検証に使う環境

elixir          1.11.2
erlang          23.0

```
iex -v
Erlang/OTP 23 [erts-11.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

IEx 1.11.2 (compiled with Erlang/OTP 21)
```

環境はMAC上にasdfを使って導入しました。 
※ 参考 [Elixirのバージョン管理環境をasdfを使って作った](https://qiita.com/nishiuchikazuma/items/b9d319732ddb540fd990)

# LiveViewと既存のJSコンポーネントを共存させる

### LiveViewのシンプルなアプリを用意する

こちらの記事を参考に、まずはシンプルなLiveViewアプリケーションを作成します。
LiveViewでの実装は完全初のため、@kikuyuta　さんの記事[Phoenix LiveView によるカウンタの作り方【Elixir 入門者向け 翻訳】](https://qiita.com/kikuyuta/items/c5b0788ad5d09c210c0a)を元にしシンプルなカウンター表示を実装します、

まずはLiveView用のプロジェクトを作成します。

```
mix phx.new liveview_counter_sample --live --no-ecto
```

検証バージョンでは、 --liveオプションをつけて生成すれば、LiveView用の設定が一とおり入った状態のプロジェクトが生成されます。

今回はさっくり動かしたいだけなので、手抜き実装してますw
まずはデフォルトで用意されたpage_live.exを直接書き換え、以下の関数を準備します。

- mount関数でsocketにカウンター値:valを設定
- カウントアップのincハンドラを追加
- カウントダウンのdecハンドラを追加

```elixir:lib/liveview_counter_sample_web/live/page_live.ex
defmodule LiveviewCounterSampleWeb.PageLive do
  use LiveviewCounterSampleWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :val, 0)}
    #{:ok, assign(socket, query: "", results: %{})}
  end

  def handle_event("inc", _, socket) do
    {:noreply, update(socket, :val, &(&1 + 1))}
  end

  def handle_event("dec", _, socket) do
    {:noreply, update(socket, :val, &(&1 - 1))}
  end

end
```

同じくpage_live.html.leexも直接書き換え:valの表示領域と操作ボタンを追加します。

```elixir:lib/liveview_counter_sample_web/live/page_live.html.leex
<!-- LiveViewCounte -->
<div>
  <h1 phx-click="boom">The count is: <%= @val %></h1>
  <button phx-click="dec">-</button>
  <button phx-click="inc">+</button>
</div>
```

Phoenixを起動すると・・・

```
mix phx.server
```

これだけの書き換えで、チュートリアルのカウンターが動作します。
（楽すぎて逆に勉強にならないですねw）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/9319758a-5c2e-3b6e-a7e6-9c473f0cf14a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/e400bb9a-95b4-5a3a-ad72-02cd974e6824.png)


### LiveView外のJS実装であるGoogleMapAPIを追加する

次はこちらの記事[How to use Google Maps with Phoenix LiveView](https://www.coletiv.com/blog/how-to-use-google-maps-phoenix-live-view/)
を元に、LiveViewでGoogle Maps JavaScript APIを使って地図を表示するようにコンテンツを追加します。

テンプレートにGoogle Maps JavaScript APIのコードと描画領域を追加します。
※事前にGCPのコンソールからGoogle Maps JavaScript APIの設定を行い、{YOUR_API_KEY}部分を自分のAPIキーに置き換えてください

```html:lib/liveview_counter_sample_web/live/page_live.html.leex

<section class="row">
<div>
  <h1 phx-click="boom">The count is: <%= @val %></h1>
  <button phx-click="dec">-</button>
  <button phx-click="inc">+</button>
</div>
</section>

<!-- 以降GoogleMapAPI実行を追記 -->

<section class="row">
  <div id="map" class="column">
  </div>
</section>

<script
  src="https://maps.googleapis.com/maps/api/js?key={YOUR_API_KEY}&callback=initMap&libraries=&v=weekly"
  defer
></script>

<script>
  (function(exports) {
    "use strict";
    function initMap() {
      exports.map = new google.maps.Map(document.getElementById("map"), {
        center: {
          lat: -34.397,
          lng: 150.644
        },
        zoom: 8
      });
    }
    exports.initMap = initMap;
  })((this.window = this.window || {}));
</script>

```

スタイルにMAP領域の描画サイズ指定を追加します。

```elixir:priv/static/css/app.css

#map {
  height: 400px; }
```


通常のGoogle Maps JavaScript APIの実装でも一瞬地図が表示されますが・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/94ed2191-e909-ec31-17e4-c4dcca23ad0e.png)

参考記事のとおり、すぐに消えてしまいます。
LiveViewによって上書きされてしまった影響ですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/3eccde04-e427-ef4c-265f-d0b5d5eec4ab.png)

これを回避するため、Google Maps JavaScript APIの描画領域にphx-update="ignore"を指定します。

```html:lib/liveview_counter_sample_web/live/page_live.html.leex

〜抜粋〜

<section class="row" phx-update="ignore">
<!-- <section class="row"> -->
  <!-- <div id="map" class="column"> -->
  <div id="map" class="column">
  </div>
</section>

〜抜粋〜
```

これによって、対象がLiveViewに上書きされることがなくなり、Mapの描画が維持されます。
LiveView実装のカウンターとも共存できているし、地図も動かせます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/acd87e68-5dd1-7988-c589-bd3acbb46ea2.png)


# iframeを使って他のHTMLの一部として描画する

次は先ほど作成したカウンター＋GoogleMapの謎コンテンツをiFrame経由で描画してみます。
こちらも同じことを検討した方々がすでにいらっしゃり、今回はこちらのディスカッション[Phoenix LiveView in an iframe](https://elixirforum.com/t/phoenix-liveview-in-an-iframe/30055)
を参考に実装しました。

### iframeで呼び出す側のHTMLを作成

まずはifarmでコンテンツを呼び出すHTML（ローカルファイル）を用意します。

```html
<!DOCTYPE html>
<html>
  <head>
    <title>iframe sample</title>
  </head>
  <body>

    <p>iFrameでLiveview経由で表示 Start</p>
    <iframe src="http://localhost:4000" width="900" height="600"></iframe>  
    <p>iFrameでLiveview経由で表示 End</p>
  </body>
</html>
```

該当のHTMLをブラウザで開いてみると、デフォルト生成されたPhoenixのコンテンツではifame経由でのアクセスが許可されていない為、表示できません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/15c1d129-eed5-6d48-a391-e698703fa803.png)

### iframeでの呼び出しを許可

HTTPのレスポンスヘッダーにx-frame-optionsを追加するPlugを作成し、これを回避します。

新規にPlugを実装します。

```elixir:lib/liveview_counter_sample/plug/AllowIFrame.ex
defmodule LiveviewCounterSample.Plug.AllowIframe do

  alias Plug.Conn

  def init(opts \\ %{}), do: Enum.into(opts, %{})

  def call(conn, _opts) do
    Conn.put_resp_header(conn,"x-frame-options","ALLOW-FROM http://localhost:4000")
  end
end
```

ルーターにプラグを追加します。

```elixir:lib/liveview_counter_sample_web/router.ex

defmodule LiveviewCounterSampleWeb.Router do
  use LiveviewCounterSampleWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {LiveviewCounterSampleWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug LiveviewCounterSample.Plug.AllowIframe # <-- 追加
  end
〜以下略〜
```

iframeでの表示が許可されますが、このままではフォーラムで言及されているとおリ、ロードを繰り返してしまいまともに動きません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/9866f62c-010d-3dfb-35f2-4c6f18949ad7.png)

※ Phoenixのログにに以下のメッセージが出力されますが、検証バージョンではこれらの５ステップは既に設定済みの状態でした。


```
[debug] LiveView session was misconfigured or the user token is outdated.

1) Ensure your session configuration in your endpoint is in a module attribute:

    @session_options [
      ...
    ]

2) Change the `plug Plug.Session` to use said attribute:

    plug Plug.Session, @session_options

3) Also pass the `@session_options` to your LiveView socket:

    socket "/live", Phoenix.LiveView.Socket,
      websocket: [connect_info: [session: @session_options]]

4) Define the CSRF meta tag inside the `<head>` tag in your layout:

    <%= csrf_meta_tag() %>

5) Pass it forward in your app.js:

    let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
    let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}});
```

エンドポイントの設定を書き換えsessionの情報をwebsockerで受け渡さないようにすることでとりあえず回避できます。

```elixir:lib/liveview_counter_sample_web/endpoint.ex
  # socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
  socket "/live", Phoenix.LiveView.Socket, websocket: []
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243163/6b6d07a8-4a24-ceef-7a6a-ba94f6bbafa5.png)

ただし、今回はサンプルがシンプルすぎるので問題なく動いているように目えますが、商用レベルのサービス構築ではこのやり方が許容できるケースは少ないと思われる為、他の解決策を模索する必要があるでしょう。

# まとめ

- テンプレートの要素にphx-update="ignore" を指定することでLiveViewによる上書きを防止できる
- x-frame-optionsを指定すれば、iframe経由でも動作する(ただし、sessionの課題は要注意。さらに調査検討が必要)

以上、
LiveViewを初めてさわりましたが、バージョンアップにより事前の設定修正がほぼ必要なくなり手軽に検証することができました。
既存のJSリソースとの共有も可能ということがわかったので、既存のサービスへのリプレイスの際に、全ての機能を一から作り直す必要がないのは大きな強みになるかと思いますので、今後既存サービスのPhoenix+LiveViewへのリプレースにチャレンジしていきたいと思います。


fukuoka.ex Elixir／Phoenix Advent Calendar 2020 の11日目は @tomoaki-kimura さんの記事です お楽しみに！