どうも、靖宗です。
なにやらLiveViewなる機能がちらほらツイッターで見かけまして、一応触っておこうと思いました。
たぶん未履修分野には被らないはず・・・
下記サイトを参考にしております。
dockyard.com
github.com
LiveView
名前からも感じますが、どうやらLiveViewというのはリアルタイムの双方向性のあるレンダリング機能のようです。via WebSockets
とあるのでWebSocketでうまいこと実装されているんでしょう。
JavaScriptでフロント+バックエンド(Phoenix)やと複雑やしメンテナンスたいへんやろ?というモチベーションのようです。
主な機能としては
- リアルタイムで値をアップデートする
- クライアント側でのデータのバリデーション(バックエンドでの評価が要らない)
- 入力のオートコンプリート
- リアルタイム性のあるゲーム
をJavaScript無しでできるそうです。まじか。
Programming Model
書き方は大まかにはController
+View
と通常のHTMLレンダリングに近いようです。
おそらく今までレンダリングしていたテンプレートがモジュールと化し、そのモジュールで定義する関数が様々なイベントのコールバック関数になるイメージでしょう。
プロジェクト作成
なにはともあれ動かしてみます。新しいプロジェクトでやってみましょう。
新しいプロジェクト作る練習にもなります。名前はhello_liveview
にしました。
PS > mix phx.new hello_liveview
hello_liveview/config/dev.exs
を編集します。
...
config :hello_liveview, HelloLiveview.Repo,
username: "elixir",
password: "elixir",
database: "hello_liveview_dev",
hostname: "localhost",
pool_size: 10
データベースのユーザー情報などは適宜合わせて下さい。
再起動してコンテナが落ちてたので起動。
PS > cd hello_liveview
PS \hello_liveview> docker start ph_psql
ph_psql
mix ecto.create
でデータ作成。
PS \hello_liveview> mix ecto.create
The database for HelloLiveview.Repo has been created
これで起動まではいく筈。とりあえず起動。
PS \hello_liveview> mix phx.server
...
問題ナシ。
参考にしたブログでもPageController
を編集してるのでそれに則る。
LiveViewの機能を有効にする
LiveViewは標準機能ではなくオプションとして提供されています。
ですので依存関係をmix.exs
に追記してmix deps.get
してやる必要があります。
...
defp deps do
[
...
{:plug_cowboy, "~> 2.0"},
{:phoenix_live_view, github: "phoenixframework/phoenix_live_view"}
]
end
...
mix deps.get
する。
PS \hello_liveview> mix deps.get
* Getting phoenix_live_view (https://github.com/phoenixframework/phoenix_live_view.git)
...
ソケット通信の設定
もうちょっと設定が必要なよう。エンドポイントにsigning_salt
なるものが必要だそうで。mix phx.gen.secret 32
を実行して得られたお塩(salt)をconfig.exs
のエンドポイントの設定に追記してやります。
PS \hello_liveview> mix phx.gen.secret 32
SECRET_SALT(なんか変な文字列がでます)
config.exs
のエンドポイントの設定に追記します。
...
config :hello_liveview, HelloLiveviewWeb.Endpoint,
...
pubsub: [name: HelloLiveview.PubSub, adapter: Phoenix.PubSub.PG2],
live_view: [
signing_salt: SECRET_SALT(さっき出力した文字列)
]
...
config :hello_liveview,
句の一番最後に追記しました。
テンプレートファイルを有効にする
ついでにLiveViewのテンプレートは.leex
になるそうなので、この設定も同じconfig.exs
に書いときます。
...
config :phoenix,
template_engines: [leex: Phoenix.LiveView.Engine]
一番最後に追記しました。
router.exのパイプラインに設定を追加する
お次にパイプラインに設定を追加しておきます。lib/hello_liveview_web/router.ex
の:browser
パイプラインを編集します。
defmodule HelloLiveviewWeb.Router do
use HelloLiveviewWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug Phoenix.LiveView.Flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :put_layout, {HelloLiveviewWeb.LayoutView, :app}
end
...
レイアウトに当てはめるplugを明示的に記載していますが、後述するコントローラからLiveViewを呼び出す場合は要らないと思います。
hello_liveview_web.exのviewとrouterでインポートの設定を追記
次にlib/hello_liveview_web.ex
のviewとrouterの箇所も編集します。
def view do
quote do
...
import Phoenix.LiveView, only: [live_render: 2, live_render: 3]
end
end
def router do
quote do
...
import Phoenix.LiveView.Router
end
end
LiveViewのレンダリング関数とルーティング機能のインポートを追記しました。
これは全てのViewでインポートされてしまうのでいちいちLiveViewの方で読み込んでも良いのかもしれませんが、LiveView多めのアプリケーションならこの手法で良いと思います。
Endpointの設定
まだ設定は続きます。(かなり手間なのでそのうちこの辺はmix phx.new
のオプションとかになるんじゃないでしょうか?)
lib/hello_liveview_web/endpoint.ex
にLiveViewに使用されるソケット通信を追記します。
defmodule HelloLiveviewWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello_liveview
socket "/live", Phoenix.LiveView.Socket,
websocket: true
...
PhoenixのGitHubではotp_app: :hello_liveview
は無いですが、Phoenixのデフォルトなので一応書いておきます。
LiveViewを入れとくフォルダの作成+ホットリロードの設定
LiveViewを入れておくフォルダlib/hello_liveview_web/live
を作成しておき、ホットリロードを有効にするためにconfig/dev.exs
に追記しておきます。
...
config :hello_liveview, HelloLiveviewWeb.Endpoint,
live_reload: [
patterns: [
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{lib/hello_liveview_web/views/.*(ex)$},
~r{lib/hello_liveview_web/templates/.*(eex)$},
~r{lib/hello_liveview_web/live/.*(ex)$}
]
]
...
以上でおそらくPhoenix自体の初期設定は終わりました。仕上げにレンダリングされるJavaScript周りに手を加えます。
バックで動いているJSの依存関係を編集します。assets/package.json
を編集します。
...
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view"
},
...
上記依存関係を追記したらnpm install
しておいて下さい。単純な事ですが僕はコレでハマりました(笑)
PS \hello_liveview> cd .\assets\
PS \hello_liveview\assets> npm isntall
ソケット通信開始の箇所を追記する
レンダリングされるJavaScript側からPhoenixへの通信を開始する箇所を追記します。
assets/js/app.js
の最後に下記を追記します。
import LiveSocket from "phoenix_live_view"
let liveSocket = new LiveSocket("/live")
liveSocket.connect()
これで初期設定は大体OKなはずです。
ThermostatViewの実装
試しにサンプルのThermostatViewを実装してみます。
追加の依存関係
ThermostatViewはcalendar
というライブラリを使用しています。mix.exs
に:calendar
の依存関係を追記しておきます。
...
defp deps do
[
{:phoenix, "~> 1.4.1"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:phoenix_live_view, github: "phoenixframework/phoenix_live_view"},
{:calendar, "~> 0.17.4"}
]
end
...
mix deps.get
をお忘れ無きよう。(忘れててもmixが教えてくれますが。)
ThermostatのCSSを追加します。この編は素直にassets/css
に保存しておきます。
ただ、レイアウトが違うのかちょっと位置が微妙なのでassets/css/thermostat.css
のmargin-topだけいじります。
...
.thermostat {
...
margin-top: 250px;
}
...
assets/css/app.css
でインポートしておきます。
"./phoenix.css";
"./thermostat.css";
LiveViewのルーティング方法は2種類
LiveViewのルーティング方法は2種類あります。
1個目はrouter.ex
からコントローラを介さずに直接レンダリングする方法で、live
マクロを利用します。
...
scope "/", HelloLiveviewWeb do
pipe_through :browser
get "/", PageController, :index
live "/thermostat", ThermostatView
end
...
2個目は従来通りコントローラを介してレンダリングします。
...
scope "/", HelloLiveviewWeb do
pipe_through :browser
get "/", PageController, :index
get "/thermostat", PageController, :thermo
end
...
たぶんコントローラを介してレンダリングする方が柔軟性があると思いますのでそちらで実装していきたいと思います。
故に:brouser
パイプラインのplug :put_layout, {HelloLiveviewWeb.LayoutView, :app}
は不要ですので消していただいても問題無いかと思います。
ルーティング設定
上記に従ってlib/hello_liveview_web/router.ex
を編集します。
defmodule HelloLiveviewWeb.Router do
use HelloLiveviewWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug Phoenix.LiveView.Flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloLiveviewWeb do
pipe_through :browser
get "/", PageController, :index
get "/thermostat", PageController, :thermo
end
end
コントローラの設定
めんどくさいのでlib/hello_liveview_web//controllers/page_controller.ex
を編集して使います。
defmodule HelloLiveviewWeb.PageController do
use HelloLiveviewWeb, :controller
alias Phoenix.LiveView
def index(conn, ) do
render(conn, "index.html")
end
def thermo(conn, ) do
LiveView.Controller.live_render(conn, HelloLiveviewWeb.ThermostatView, session: %{})
end
end
live_render
はlive_render/2
もインポートしてるので最後のマップは要らなさそうな気もするんですがなんか渡さないと怒られたのでlive_render/3
を利用しています。
ちょっとこの辺は後ほど再確認したいところです・・・
index/2
とthermo/2
を比較すると、関数名こそ違えどほぼ同じ様な構造になっているので抵抗感無く使えるかと思います。
LiveViewの追加
lib/hello_liveview_web/live/thermostat_live.ex
を作成し、LiveViewを作って行きます。
defmodule HelloLiveviewWeb.ThermostatView do
use Phoenix.LiveView
import Calendar.Strftime
def render(assigns) do
~L"""
<div class="thermostat">
<div class="bar <%= @mode %>">
<a phx-click="toggle-mode"><%= @mode %></a>
<span><%= strftime!(@time, "%r") %></span>
</div>
<div class="controls">
<span class="reading"><%= @val %></span>
<button phx-click="dec" class="minus">-</button>
<button phx-click="inc" class="plus">+</button>
</div>
</div>
"""
end
def mount(, socket) do
if connected?(socket), do: Process.send_after(self(), :tick, 1000)
{:ok, assign(socket, val: 72, mode: :cooling, time: :calendar.local_time())}
end
def handle_info(:tick, socket) do
Process.send_after(self(), :tick, 1000)
{:noreply, assign(socket, time: :calendar.local_time())}
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
def handle_event("toggle-mode", , socket) do
{:noreply,
update(socket, :mode, fn
:cooling -> :heating
:heating -> :cooling
end)}
end
end
render/1
の所は通常のViewと同様にテンプレートを置いておけば書かなくても良いのかもしれませんが、とりあえずサンプル通りにしておきます。
上記の挙動から鑑みるに、下記のタイミングで再レンダリングがかかっています。
- メッセージを受けた時
update/3
を使用した時
厳密にはソケットのステートが変更された際(Phoenix側でsocketに値がアサインされた時)に再レンダリングするようです。
Thermostatの例では、マウント時に1秒後に:tick
というメッセージを自分宛に発行しています。これを利用して:tick
というメッセージを受けた際にhandle_info/2
が呼び出される(このへんはGenServerを参照してください)ので、そのコールバック関数の中でソケットに値をアサインしています。(コレを1秒毎に繰り返す)
クライアントサイドではphx-click="hoge"
というディレクティブを要素に追加するとクリック時にイベントが発生し、handle_event/3
で受けれるといった仕組みのようです。
それぞれ呼び出されるイベント毎にソケットに値をアサインしてレンダリングを実行しているようです。
所感
実際にやってることとしては他のフレームワークとそう大差ない(通常のフレームワークであればおそらくソケット通信よりも非同期通信で処理するのが一般的だとは思いますが)ですが、裏を返せば他のフレームワークを学習しなくてもデータバインディングやコンポーネントの再レンダリングができるので、ラピッドプロトタイピングがかなり捗りそうです。
既存の資産に縁が無い(初心者やバックエンドしかやったことのない人)なら、これはかなり有り難いんじゃ無いでしょうか?僕は有り難いです!
でももうちょっとすんなり入るようになってほしい・・・(´゚'ω゚`)