Programming Phoenix LiveView 読書メモ
LiveView本を読んだ際のメモを書き殴っていく
chapter1, chapter2でLiveViewの全体像の説明、コード生成などの話。chapter3以降はゲームのサンプルを作るなどより実践って感じかな。
Pento
という架空の会社、システムを題材にシステム構築が進むみたい。
phoenixのv1.5ベースで書かれてある。p.10に mix phx.new pento --live
とあるが、v1.6では --live
はなく逆に --no-live
になった。デフォルトがLiveViewベースで、いらない場合に --no-live
をつける。
「LiveViewでの状態管理は Phoenix.LiveView.Socket
構造体で管理されるからまずそれを見ていくで」という流れ
iex(1)> h Phoenix.LiveView.Socket
Phoenix.LiveView.Socket
The LiveView socket for Phoenix Endpoints.
This is typically mounted directly in your endpoint.
socket "/live", Phoenix.LiveView.Socket
iex(2)> Phoenix.LiveView.Socket.__struct__
#Phoenix.LiveView.Socket<
assigns: %{__changed__: %{}},
endpoint: nil,
id: nil,
parent_pid: nil,
root_pid: nil,
router: nil,
transport_pid: nil,
view: nil,
...
>
-
assigns
が何より重要 - 画面の状態を保持するmap
-
Router -> Index.mount -> Index.render
が基本の流れ - mountで初期化が行われる
- routerで `live/3`関数を使って "live route" を定義できる
- 通常の(?) getやpostと別で、LiveView用のルーティングを定義する
「以下のコードができる」みたいな説明があるけど、v1.6ではなくなっている。
scope "/", PentoWeb do pipe_through :browser
live "/", PageLive, :index
フォーラムにもそのやりとりがあった
v1.6の場合はforumの回答通り、自分で
- page_live.ex
- page_live.html.heex
を追加してrouterにルーティングを追加すれば動いた
v1.5ベースなので .leex
で記述が進む。v1.6では .heex
になったので読み替えていく必要あり
全体の流れ
- 初回アクセス時にmount -> render
- ページ全体がrenderingされる
- いわゆるSSR(サーバーサイドレンダリング)といえばいいのかな
- WebSocket接続が開始される
- あとはひたすら以下ループ
- フロントからのイベント受信(click, submit, scroll, file upload...)
- stateの更新
- 更新したstateをもとに再度render
<app>_web/
配下に live/
ディレクトリを作って、そこにLiveViewモジュールを置いていくのがお作法っぽい
この辺のお作法はlivebookを見てくのが良さそう
この書き方もv1.5版。
def render(assigns) do
~L"""
<h1>Your score: <%= @score %></h1>
<h2>
<%= @message %>
</h2>
<h2>
<%= for n <- 1..10 do %>
<a href="#" phx-click="guess" phx-value-number="<%= n %>"><%= n %></a>
<% end %>
</h2>
"""
end
Phoenix v1.6では ~H
を使う。これで動いた。タグの属性で変数展開する場合は <%= %>
ではなくて { variable }
の書き方をするっぽい
def render(assigns) do
~H"""
<h1>Your score: <%= @score %></h1>
<h2>
<%= @message %>
</h2>
<h2>
<%= for n <- 1..10 do %>
<a href="#" phx-click="guess" phx-value-number={ n }><%= n %></a>
<% end %>
</h2>
"""
end
@message
は socket.assigns.message
の省略記法。 @
がついてたらsocketから取ってきてると思えばヨシか
templateの現在時刻を表示する関数を生やして再度ページを見ても、変更が加わるのは一度だけ。LiveViewは変更が必要なDOMのみ再描画するため効率的。なるほど。
def render(assigns) do
~H"""
<h1>Your score: <%= @score %></h1>
<h2>
<%= @message %>
It's <%= time() %>
</h2>
<h2>
<%= for n <- 1..10 do %>
<a href="#" phx-click="guess" phx-value-number={n}><%= n %></a>
<% end %>
</h2>
"""
end
def time() do
DateTime.utc_now |> to_string
end
Chapter2ではLiveViewではAuthenticationをどう扱うかやっていく
認証の本題に入る前に、実装パターンのお話。CRC(Constructors, Reducers, Converters)のパターンを覚えようとのこと。
- Constructors: 適切な入力からコアの型を作成
- Reducers: その型のまま変換
- Converters: 別の型へ変換
実装例が載っている。
defmodule Number do
def new(string), do: Integer.parse(string) |> elem(0)
def add(number, addend), do: number + addend
def to_string(number), do: Integer.to_string(number)
end
こういったパターンはデータの変換には頻出で、例えばReactだと state reducer パターンなるのがあると。
phoenixでいうとPlugもこのCRCパターンに該当すると。イメージのコード例が書かれてある。
connection
|> process_part_of_request(...)
|> process_part_of_request(...)
|> render()
1つ1つのplug関数はreducersであり、%Plug.Conn{}
に対して Plug.Conn
の構造体のままいろんなデータを付加していくイメージ。
Phoenixのデータの流れ。custom_applicationにcontrollerやLiveViewが該当する。
connection_from_request
|> endpoint
|> router
|> custom_application
1つ1つの小さなplugによってPhoenixという1つの大きな関数が出来ているとイメージすると良さげ
In order to understand how Phoenix handles web requests, and therefore how LiveView handles web requests, you can think of Phoenix requests as simply one big function broken down into smaller plugs. These plugs are stitched together, one after another, as if they were in one big pipeline.
plugの全体像の話が終わって、認証の実装へ。 phx.gen.auth
を使って実装を進める。
この辺は今の最新版と同じ挙動
$ mix phx.gen.auth Accounts User users
$ mix deps.get
$ mix ecto.migrate
$ mix test
chapter3は phx.gen.live
にはじまって、生成されたコードの説明が進む。Ecto.SchemaやEcto.Changesetの説明もあって丁寧
chapter3はliveviewから一旦離れてEctoの話、Contextの考え方など知識として入れておきたい話がまとまっていた
chapter4では生成されたファイルをさらに深掘り、routerからの処理の流れの解説が始まる
-
router.ex
で使われているlive/4
macroはPhoenix.LiveView.Routerによって定義されている - phoenixプロジェクトを生成したタイミングから利用可能
-
use <App>Web, :router
の記述 -><app>_web.ex
のrouter/0
に定義してあるマクロが展開される ->Phoenix.LiveView.Router
がimportされる
この辺のマクロを活用した記述はLiveViewないときでも変わらずなところだな
live/4
の最後の引数は live action と呼ばれるらしい。controllerのactionと同じ考えで良さそう。
chapter4のコードの説明で .leex
の説明が登場するが、ここも .heex
に読み替える必要あり。内容自体は今とそこまで齟齬なさげ
live_patch/2
でURLの変更を伴う画面の更新ができると。本文にも書かれているけど、まさに "画面にパッチを当てる" イメージ。
内部ではjsのpushStateの機能を使っているとのこと
live_patch/2
で状態遷移したタイミングでは mount/3
は呼ばれない
自前でassignせずとも@live_action
で現在のアクションを取得可能できる
<%= if @live_action in [:new, :edit] do %>
ページ中段に書いてある
LiveComponent周りの話は最新だと違ってきてそうだ。この辺は書籍は参考に留めて、生成されたコードを読んだ方が良さげ
HEEXだと
-
<.modal ...>
のようにViewの関数を実行できる -
<.live_component ...>
でコンポーネントを呼び出し- LiveViewの中に埋め込めるLiveView的なイメージ?
-
render_slot/2
を使って引数に応じてrendering結果を変える
<.form ...>
のように呼んでいるform関数はLiveView.Helpersで最初から定義されていて、 Phoenix.HTML.form_for/4
をwrapする形になっていると。
This function is built on top of Phoenix.HTML.Form.form_for/4. For more information about options and how to build inputs, see Phoenix.HTML.Form.
let
って何?ってなったけどこの辺に書いてあるな。
render_slot/2
でレンダリングする際に渡した引数をcaller(親)側に戻すことができると。
実装追ってみると、 f
にはHelperを介して %Phoenix.HTML.Form{}
が入っていることがわかる
<.form
let={f}
for={@changeset}
id="product-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
<%= label f, :name %>
<%= text_input f, :name %>
<%= error_tag f, :name %>
<%= label f, :description %>
<%= text_input f, :description %>
<%= error_tag f, :description %>
<%= label f, :unit_price %>
<%= number_input f, :unit_price, step: "any" %>
<%= error_tag f, :unit_price %>
<%= label f, :sku %>
<%= number_input f, :sku %>
<%= error_tag f, :sku %>
<div>
<%= submit "Save", phx_disable_with: "Saving..." %>
</div>
</.form>
live_componentのライフサイクルの説明はこの辺
-
mount/1
が呼ばれる- 任意
- その後
update/2
が呼ばれる- ここでsocketに必要なデータをassignできる
ex側では push_redirect/2
、 heex側では live_redirect
を使うことで別LiveViewへリダイレクトできる。
patchと違い、redirectだと再度 mount/3
が呼ばれるのがポイント。保存した結果などを再度DBから取得する必要があるため、edit後やshowへの遷移はredirectで行うのが良い
生成したコマンドにそこまで手は加えず、細かい説明を加えたところでPart1は終了。Part1だけでかなり基礎は抑えられた感。
Part2 Chapter5。Ecto.Changesetを使ったリアルタイムバリデーションの実践から。応用に入っていく
Ecto.ChangesetをDB関係なく利用するパターンの説明が続く。
ちょっと古い記事だけど、内容としてはこの辺の話
phx-disable-with
, phx-debounce
といった便利なbindingが用意されている。リアルタイムバリデーションのフォームの実装をしつつ、この辺の説明が入る。便利〜
File uploadの説明が入る
- socketに対して
allow_upload/3
を呼び出すと%Phoenix.LiveView.UploadConfig{}
が:uploads
というkeyでsocketに追加される - 許可する拡張子や、自動でのuploadをする/しないなど、設定を持つ
-
allow_upload/3
の第一引数に指定した値を指定して、live_file_input
を実行、inputタグを生成する
file uploadまで組んでchapter5は終了。chapter6, chapter7とコンポーネントの話が続く模様
chapter6 stateless componentというお話。アンケートツールの開発を題材に進む
LiveViewに入る前にContext関数の実装を進める。Queryモジュールを別に作って各クエリの再利用性を上げていく実装。この辺はLiveView関係なくフツーに勉強になる。
defmodule Pento.Catalog.Product.Query do
import Ecto.Query
alias Pento.Survey.Rating
alias Pento.Catalog.Product
def base, do: Product
def with_user_rating(query \\ base(), user) do
ratings_query = Rating.Query.preload_user(user)
query
|> preload(ratings: ^ratings_query)
end
end
def list_products_with_user_ratings(user) do
Product.Query.with_user_rating(user)
|> Repo.all()
end
これは重要なところだ
- 初回アクセス時と、WebSocket接続確立時の2回
mount/3
が呼ばれる - mountでクエリを発行してしまうと無駄なDBアクセスに繋がるためNG
- 初回アクセス時に
Plug.Conn.assigns
の値はsocket.private.assign_new
に格納される - websocket接続が確立して2回目のマウント後は
socket.private.assign_new
の値は利用できない -
assign_new/3
を使うと、socketに値が無ければ第3引数に渡した関数の結果をsocketにアサインすることができる - ∴ ↓↓のように書けば、初回アクセス時にはPlug.Conn.assignsの値が入っているためクエリは実行されず、socket接続後に
Plug.Conn.assigns
の値が使えなくなる =socket.private.assign_new
の値が空になっているため、クエリが実行される-
:current_user
の値がPlug.Conn.assignsに入っている前提
-
def mount(_params, %{"user_token" => token} = _session, socket) do
{:ok, socket |> assign_user(token)}
end
defp assign_user(socket, token) do
assign_new(socket, :current_user, fn ->
Accounts.get_user_by_session_token(token)
end)
end
The Plug.Conn assigns will not be available during the connected mount
:live_component
に関して、mount/1
, update/2
などはすべてoptional
componentを作成して描画するところまで進んでchapter6は完了。submitイベントのハンドリングからchapter7へ進む