はじめに
ElixirでのWebアプリケーション開発において出会う様々な用語や概念を深堀します。今回は、Phoenixフレームワークを使用した際のLiveViewとWebソケット通信の関係性に焦点を当てて解説していきます。
前回の記事から読むとPhoenixフレームワークの概要が理解しやすいと思います。
PhoenixフレームワークにおけるHTTP通信とWebソケット通信①~Plug.connについて~
LiveViewとは
いきなり出てきましたが、LiveViewとはPhoenixに標準で組み込まれている拡張機能です。
これを使うことでElixirでSPA(Single Page Application)を構築できるようになります。
従来のSPA開発ではJavaScriptフレームワークを多用する必要がありましたが(Vue.jsとかReactとか)、LiveViewを使用すると、主にサーバーサイドでのコーディングにより同等のユーザー体験を提供できます。
ElixirとErlangのBEAM仮想マシン上でリアルタイムのウェブアプリケーションを構築するためのものであり、LiveViewを使用することで、サーバーサイドで動的なページの変更を処理し、その結果をリアルタイムでクライアント(ウェブブラウザ)にプッシュすることができます。
ページ全体の再読み込みを行わずに、ページの一部分を動的に更新することが可能になります。
LiveViewの特徴
LiveViewではクライアント(フロントエンド)とサーバー(バックエンド)でのデータのやり取りをElixirだけで実装できるのでAPIの開発が不要です。
従来のAPIベースのフロントエンドとバックエンドの分離に比べて、開発プロセスを簡略化し、開発時間を短縮する可能性があります。
LiveViewとWebソケット通信の関係
Webソケットは、LiveViewがクライアントとサーバー間でリアルタイム通信を行うために使用する技術です。LiveViewはWebソケット通信(または長期ポーリングを使用したフォールバックオプション)を通じて、サーバーサイドの状態の変更をリアルタイムでクライアントにプッシュし、DOMの更新を行います。これにより、アプリケーションのユーザーインターフェイスは常に最新の状態を反映することができ、ユーザーはよりリッチでインタラクティブな体験を得ることができます。
LiveViewとHTTP通信の関係
Phoenix LiveViewを使用する場合、基本的にWebソケット通信を活用してリアルタイムのインタラクションを実現します。ただし、LiveViewがWebソケット通信を主に使用しているとはいえ、HTTP通信が全く使われないわけではありません。
LiveViewの初期ロードやページ遷移の際には、HTTP通信が使用されます。
1.初期ロード
ユーザーがLiveViewページに初めてアクセスする時、通常はHTTP GETリクエストを通じてページがロードされます。この時点でのHTMLコンテンツの配信はHTTP通信によるものです。ページがロードされた後、JavaScriptがクライアント側で実行され、サーバーとのWebソケット接続を確立します。
2.ページ遷移が新たなページをロードされる形で実装されている場合
HTTP GETリクエストが使用されます。
LiveViewの接続順序
- 初期HTTPリクエスト: ユーザーがLiveViewページにアクセスすると、初めに通常のHTTPリクエストによってページがロードされます。この時点でのmount/3が初めて呼び出されます。この初期呼び出しでは、サーバーサイドでページの初期状態を設定し、初期レンダリングの準備を行います
2.WebSocket接続: HTTPリクエストによる初期ページロード後、LiveViewのJavaScriptクライアントがWebソケット接続を確立します。このWebソケット接続が確立されると、mount/3が2回目呼び出されます。この2回目の呼び出しでは、Webソケットを通じたリアルタイムの接続が確立されている状態で、ページやアプリケーションの状態を更新することができます - 初期レンダリング
サーバーサイドでのレンダリング: LiveViewはサーバーサイドでページの初期状態をレンダリングし、そのHTMLをクライアントに送信します。render/1関数は、この初期レンダリング時に呼び出されます - イベントハンドリングとサーバーサイドの処理
イベントの送信: ユーザーのインタラクション(クリックやフォームの送信など)は、Webソケットを介してサーバーに送信されます。
サーバーサイドでの処理: LiveViewはサーバーサイドでイベントを処理し、アプリケーションの状態を更新します - DOMの更新とクライアントサイドの反映
再レンダリングとDOMの更新: 更新された状態に基づいて、LiveViewは再レンダリングを行い、変更されたDOMの差分をクライアントに送信します。render/1関数は、この再レンダリングの際にも呼び出されますが、ページ全体ではなく、変更が必要な部分のみが更新されます
クライアントサイドでの更新: クライアントは受信したDOMの変更を現在のページに適用し、ユーザーインターフェイスを更新します
図解すると以下のようになります。
参考: Elixir実践入門 第10章 LiveViewによるフロントエンドの開発
JavaScriptによってどのようにWebソケット接続が確立されるか
通常、アプリケーションのエントリポイントであるapp.jsや特定のページのスクリプトファイル内でLiveSocketのインスタンスを作成します。
// app.js内
import {Socket} from "phoenix"
import LiveSocket from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
liveSocket.connect()
このコードスニペットでは、CSRFトークンを使用してセキュリティを強化しています。また、/liveパスでLiveSocketを作成し、サーバーとの接続を確立しています。liveSocket.connect()メソッドにより、クライアント側からサーバーへのWebソケット接続が確立されます。この接続は、LiveViewが提供する機能(ページの部分更新、イベントハンドリングなど)を実現するために使用されます。
基本の整理
マウントとは?
一般的なマウントの概念
一般的なマウントの概念は、特にフロントエンドのフレームワークやライブラリ(React、Vue.js、Angularなど)において、コンポーネントやアプリケーションのインスタンスが初めてDOMツリーに組み込まれるプロセスを指します。マウントのプロセスには、コンポーネントの初期化、初期状態の設定、プロパティの受け渡し、必要なイベントリスナーの設定などが含まれます。マウントは、そのコンポーネントが"生きている"(対話可能な状態で存在している)ことを意味し、この時点で初めてユーザーがインタラクションを開始できるようになります。
Phoenix LiveViewのmount/3
関数の概念
@callback mount(
params :: unsigned_params() | :not_mounted_at_router,
session :: map(),
socket :: Phoenix.LiveView.Socket.t()
) ::
{:ok, Phoenix.LiveView.Socket.t()}
| {:ok, Phoenix.LiveView.Socket.t(), keyword()}
LiveView のエントリポイント。
以下3つの引数を取ります。
- params: URLからのクエリパラメーターや、ページをロードするために使用されたパスパラメーターなど、リクエストに関連するパラメーターが含まれる
- session: セッションデータ。認証情報など、クライアントとサーバー間で共有されるセッション情報が含まれる
- socket: LiveViewソケット。このソケットを通じて、クライアントとサーバー間で状態やイベントが共有される
テンプレートのルートにある各 LiveView について、mount/3は 2 回呼び出されます。
- 最初のページの読み込みを実行するため(この時にJavaSriptのライブラリや処理が呼び出される)
- ライフソケットを確立するため(JavaScriptは呼ばれない)
※つまり2回目のマウントの時にはJavascriptのDOM操作が消されてしまうことがある
参考hexdocs: Phoenix LiveView mount/3
参考hexdocs: Phoenix LiveView dom-patching
レンダリングとは?
一般的なレンダリングの概念
レンダリングは、データや状態をもとにビジュアル出力(HTML、SVG、Canvasなど)を生成するプロセスです。ウェブ開発においては、コンポーネントやアプリケーションの状態が変更されたとき、その新しい状態を反映するために、レンダリングが再度行われます。レンダリングは、静的なHTMLページの生成だけでなく、動的なUIの更新も含む広範な概念です。レンダリングプロセスでは、最終的に生成されたビジュアル表現がDOMに挿入されますが、このプロセスはマウント時だけでなく、コンポーネントのライフサイクル内で繰り返し発生することがあります。
Phoenix LiveViewのrender/1
関数の概念
@callback render(assigns :: Phoenix.LiveView.Socket.assigns()) ::
Phoenix.LiveView.Rendered.t()
LiveViewコンポーネントの状態をもとにHTMLコンテンツを生成するために特化した関数です。この関数はLiveViewのライフサイクル内で何度も呼び出され得ますが、主に以下の2つの場面で重要な役割を果たします。
このコールバックは、新しいコンテンツをレンダリングしてクライアントに送信する必要があることを LiveView が検出するたびに呼び出されます。
render/1関数内では、LiveViewの状態を表すデータを受け取り、それをもとにHTMLマークアップを生成するためのロジックが記述されます。Phoenixでは、EEx(Embedded Elixir)テンプレートがこの目的によく用いられます。
参考hexdocs: Phoenix LiveView render/1
一般的なマウントとレンダリングの違い
マウントは「準備する」段階であり、レンダリングは「表示する」段階と言えます。
実際のコードでLiveViewとJSのデータ連携を確認する
LiveViewからJavaScriptへのデータ送信
画面が表示された時に、jsをよびjs側でチャートを描画する処理。
# hello.ex
def mount(_params, _session, socket) do
# 処理省略
socket =
socket
|> push_event(user)
{:ok, socket}
end
def push_event(socket,user,category_scores) do
socket
|> push_event("set_chart", %{category_scores: category_score})
end
LiveView側からcategory_scores
という値を渡し、js側ではe.detail.category_scores
で受け取っている。
このページで1度でもhookが使われているとhookを利用してjsが呼ばれる
<:right_icon>
<.header_icon phx-click="set_setup" action="setting" phx-hook="hello" />
</:right_icon>
// hello.js
import radarChart from './chart';
// 処理省略
const hello = {
mounted(){
window.addEventListener('phx:set_chart', (e) => {
chart(e.detail.category_scores, 'home');
}, { once: true });
}
}
LiveViewではpush_event/3関数を用いて、サーバーサイド(LiveView)からクライアントサイド(JS)へイベントを送信します。この方法を使うと、サーバー側の状態の変更や必要なデータをリアルタイムでクライアントに送ることができます。
参考hexdocs: Phoenix.LiveView.push_event/3
JavaScriptからLiveViewへのイベント呼び出し
これは画面側(クライアント)でスワイプの操作を行いその操作をフックにヒントの情報をセットする処理です。
//question.js
//省略
const ques = {
mounted(){
//ヒントをセットするイベントを送信する関数を持った変数
const setHint = (select_id) =>{
this.pushEvent('set_hint', {select_id})
}
}
}
# hint.ex
alias HelloServerWeb.QuestionLive.Editor.QEditor
# 省略
def handle_event("set_hint", %{"select_id" => select_id}, socket) do
editor = QEditor.set_hint(socket.assigns.editor, socket.assigns.current_index, select_id)
{:noreply, assign(socket, :editor, editor)}
end
# hint.html.heex
<div
phx-hook="question"
phx-update="ignore"
>
<%!-- 省略 --%>
</div>
クライアントサイドからサーバーサイドへイベントを送信するには、JavaScriptのpushEventメソッドを使用します。これにより、ユーザー操作やクライアントサイドのイベントをLiveViewに通知し、サーバーサイドで処理を行うことが可能になります。
参考hexdocs: Phoenix.LiveView.handle_event/3
なぜphx-update="ignore"
を入れているのか?
これはLiveViewによる自動DOM更新を制御するものです。今回はswiper.jsを使っており、このように特定のJavaScriptライブラリやフレームワークは、DOMの構造や状態に依存して動作することがあります。LiveViewによるDOMの自動更新がこれらのライブラリに影響を与え、意図しない挙動やエラーを引き起こす可能性があります。phx-update="ignore"
を使用することで、LiveViewの更新プロセスから特定のDOM要素を保護し、JavaScriptライブラリが期待するDOMの状態を維持できます。
つまり、LiveViewのライフサイクルにおいて2回mountが行われるうちの2回目のmountは行われないように制御しています。
要素の説明
phx-hook
とは?
LiveViewとJavaScriptの間でインタラクションを設定するための属性です。具体的には、phx-hookはLiveViewテンプレート内の特定のDOM要素にJavaScriptのコード(フック)を関連付けるために使用されます。これにより、LiveViewのライフサイクルイベント(マウント、更新、破棄など)が発生したときに、カスタムJavaScriptコードを実行することができます。
まとめ
今回はLiveViewとWebソケット通信を見ていきました。LiveViewとJavaScript間での双方向のイベント通信を実現するためには、適切なメソッドを使用することが重要です。サーバーからクライアントへはpush_event/3
を、クライアントからサーバーへはJavaScriptのpushEvent
メソッドを利用してください。
最後に
誤りなどありましたらご指摘お願いします!
また、thewaggleではElixirでの開発にジョインしてくださる仲間を募集しています!
ご興味ある方、お気軽にご連絡ください。