この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar 2020 10日目です。
前日は、 @koga1020 さんのElixirのfor文をおさらいしてみる でした
前書きとやりたいこと
前から気になっていた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を使って作った
LiveViewと既存のJSコンポーネントを共存させる
LiveViewのシンプルなアプリを用意する
こちらの記事を参考に、まずはシンプルなLiveViewアプリケーションを作成します。
LiveViewでの実装は完全初のため、@kikuyuta さんの記事Phoenix LiveView によるカウンタの作り方【Elixir 入門者向け 翻訳】を元にしシンプルなカウンター表示を実装します、
まずはLiveView用のプロジェクトを作成します。
mix phx.new liveview_counter_sample --live --no-ecto
検証バージョンでは、 --liveオプションをつけて生成すれば、LiveView用の設定が一とおり入った状態のプロジェクトが生成されます。
今回はさっくり動かしたいだけなので、手抜き実装してますw
まずはデフォルトで用意されたpage_live.exを直接書き換え、以下の関数を準備します。
- mount関数でsocketにカウンター値:valを設定
- カウントアップのincハンドラを追加
- カウントダウンのdecハンドラを追加
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の表示領域と操作ボタンを追加します。
<!-- 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)
LiveView外のJS実装であるGoogleMapAPIを追加する
次はこちらの記事How to use Google Maps with Phoenix LiveView
を元に、LiveViewでGoogle Maps JavaScript APIを使って地図を表示するようにコンテンツを追加します。
テンプレートにGoogle Maps JavaScript APIのコードと描画領域を追加します。
※事前にGCPのコンソールからGoogle Maps JavaScript APIの設定を行い、{YOUR_API_KEY}部分を自分のAPIキーに置き換えてください
<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領域の描画サイズ指定を追加します。
#map {
height: 400px; }
通常のGoogle Maps JavaScript APIの実装でも一瞬地図が表示されますが・・・
参考記事のとおり、すぐに消えてしまいます。
LiveViewによって上書きされてしまった影響ですね。
これを回避するため、Google Maps JavaScript APIの描画領域にphx-update="ignore"を指定します。
〜抜粋〜
<section class="row" phx-update="ignore">
<!-- <section class="row"> -->
<!-- <div id="map" class="column"> -->
<div id="map" class="column">
</div>
</section>
〜抜粋〜
これによって、対象がLiveViewに上書きされることがなくなり、Mapの描画が維持されます。
LiveView実装のカウンターとも共存できているし、地図も動かせます。
iframeを使って他のHTMLの一部として描画する
次は先ほど作成したカウンター+GoogleMapの謎コンテンツをiFrame経由で描画してみます。
こちらも同じことを検討した方々がすでにいらっしゃり、今回はこちらのディスカッションPhoenix LiveView in an iframe
を参考に実装しました。
iframeで呼び出す側のHTMLを作成
まずはifarmでコンテンツを呼び出す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経由でのアクセスが許可されていない為、表示できません。
iframeでの呼び出しを許可
HTTPのレスポンスヘッダーにx-frame-optionsを追加するPlugを作成し、これを回避します。
新規にPlugを実装します。
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
ルーターにプラグを追加します。
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での表示が許可されますが、このままではフォーラムで言及されているとおリ、ロードを繰り返してしまいまともに動きません。
※ Phoenixのログにに以下のメッセージが出力されますが、検証バージョンではこれらの5ステップは既に設定済みの状態でした。
[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で受け渡さないようにすることでとりあえず回避できます。
# socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
socket "/live", Phoenix.LiveView.Socket, websocket: []
ただし、今回はサンプルがシンプルすぎるので問題なく動いているように目えますが、商用レベルのサービス構築ではこのやり方が許容できるケースは少ないと思われる為、他の解決策を模索する必要があるでしょう。
まとめ
- テンプレートの要素にphx-update="ignore" を指定することでLiveViewによる上書きを防止できる
- x-frame-optionsを指定すれば、iframe経由でも動作する(ただし、sessionの課題は要注意。さらに調査検討が必要)
以上、
LiveViewを初めてさわりましたが、バージョンアップにより事前の設定修正がほぼ必要なくなり手軽に検証することができました。
既存のJSリソースとの共有も可能ということがわかったので、既存のサービスへのリプレイスの際に、全ての機能を一から作り直す必要がないのは大きな強みになるかと思いますので、今後既存サービスのPhoenix+LiveViewへのリプレースにチャレンジしていきたいと思います。
fukuoka.ex Elixir/Phoenix Advent Calendar 2020 の11日目は @tomoaki-kimura さんの記事です お楽しみに!