Interaktive Apps mit Phoenix LiveView entwickeln
Erstellen Sie eine Suche mit Phoenix LiveView, serverseitigem Zustand, HEEx-Vorlagen, Events und integriertem JavaScript-Hook. Erfahren Sie, wann LiveView passt.
Phoenix LiveView rendert HTML auf dem Server, hält den Zustand jeder Seite in einem langlebigen Elixir-Prozess und sendet über eine persistente Verbindung nur die geänderten Teile der Seite an den Browser.
Wenn Sie CRUD-Oberflächen mit React oder Vue entwickeln, kennen Sie die Routine: ein JSON-Endpunkt, ein clientseitiger Store, Ladezustände und eine Validierung, die auf beiden Seiten jeweils separat implementiert wird. Der Großteil dieses Codes verschiebt lediglich Daten zwischen zwei Kopien desselben Zustands.
Dieser Artikel zeigt, wie LiveView die API-Schicht für diese Art von Anwendung überflüssig macht. Sie bauen ein Live-Suchfeld mit der Syntax von LiveView 1.2, ergänzen ein Stück clientseitiges JavaScript und sehen, wo das Modell an seine Grenzen stößt.
Das Wichtigste in Kürze
- Eine LiveView hält den UI-Zustand in einem Serverprozess. Browser-Events werden über einen WebSocket übertragen, und LiveView sendet nur das HTML zurück, das sich geändert hat.
- Das erste Rendering einer LiveView ist eine gewöhnliche HTTP-Antwort, und
mount/3wird erneut ausgeführt, sobald der WebSocket verbunden ist. - Eine einfache LiveView benötigt drei Callbacks:
mount/3setzt den Anfangszustand,render/1gibt ein HEEx-Template zurück, undhandle_event/3aktualisiert den Zustand als Reaktion auf Bindings wiephx-clickundphx-change. - Colocated Hooks platzieren das JavaScript einer Komponente in einem
<script>-Tag direkt neben ihrem Markup. Sie setzen Phoenix 1.8 oder neuer voraus. - Jedes serverseitig verarbeitete Event bedeutet einen Netzwerk-Roundtrip. Für Offline-First-Apps und UIs mit intensiver clientseitiger Interaktion sind React oder Vue weiterhin die bessere Wahl.
Was macht Phoenix LiveView?
Phoenix LiveView verlagert Zustand, Event-Handling und Rendering vom Browser auf den Server, während der Browser eine aktive Verbindung offen hält, um Updates zu empfangen. Die LiveView-Dokumentation beschreibt jede LiveView als Prozess: Sie nimmt Events entgegen, ändert ihren Zustand und sendet der Seite nur die Teile, die sich unterscheiden.
Übertragen auf das bekannte SPA-Modell:
| SPA-Konzept | Entsprechung in LiveView |
|---|---|
Komponentenzustand (useState, ref) | Assigns auf dem serverseitigen Socket |
Event-Handler (onClick) | Callback handle_event/3 |
| Virtual-DOM-Patch | HTML-Diff, der über den WebSocket gepusht wird |
| API-Aufruf zum Abrufen von Daten | Ein Funktionsaufruf innerhalb desselben Prozesses |
Der Lebenszyklus verläuft in zwei Phasen. Das erste Rendering ist eine gewöhnliche HTTP-Antwort, sodass die Seite sichtbar und indexierbar ist, bevor JavaScript geladen wird. Anschließend öffnet der Browser einen WebSocket, und mount/3 wird ein zweites Mal ausgeführt – innerhalb eines Prozesses, der so lange aktiv bleibt, wie die Seite geöffnet ist. Code in mount/3 läuft daher bei jedem Seitenaufruf zweimal.
Wenn Sie htmx verwendet haben, wird Ihnen das Konzept, die UI mit serverseitig gerendertem HTML zu steuern, bekannt vorkommen. Der Unterschied besteht darin, dass LiveView zusätzlich für jede verbundene Seite einen zustandsbehafteten Prozess auf dem Server vorhält.
Wie erstellt man ein Phoenix-Projekt und fügt eine Live-Route hinzu?
Eine neue Phoenix-1.8-App bringt LiveView bereits vorkonfiguriert mit, sodass ein einziger Generator-Befehl alles bereitstellt, was dieses Beispiel benötigt. Nachdem Sie Elixir und den Phoenix-Installer installiert haben, führen Sie Folgendes aus:
mix phx.new my_app --no-ecto
cd my_app
mix phx.server
Das Flag --no-ecto lässt die Datenbankschicht weg, die für eine Demo nicht benötigt wird. Der Dev-Server lauscht auf http://localhost:4000. Ein --live-Flag ist nicht erforderlich: Ältere Tutorials verwenden es zwar, doch LiveView ist inzwischen Bestandteil jeder generierten App.
Öffnen Sie lib/my_app_web/router.ex. Fügen Sie innerhalb des bestehenden Blocks scope "/", MyAppWeb do unterhalb von pipe_through :browser Folgendes hinzu:
live "/search", SearchLive
Das Makro live ordnet einem Pfad ein LiveView-Modul zu, ähnlich wie get einem Pfad einen Controller zuordnet. Ersetzen Sie MyApp überall durch den Namen Ihrer eigenen App. Phoenix liefert Generatoren, Auth-Scaffolding und LiveView direkt mit und ist damit ein Framework nach dem Prinzip „Batteries included“.
Wie baut man ein Live-Suchfeld mit Phoenix LiveView?
Ein Live-Suchfeld benötigt ein Formular-Binding, das bei der Eingabe reagiert, und ein Klick-Binding, das den Zustand zurücksetzt. Legen Sie lib/my_app_web/live/search_live.ex an:
defmodule MyAppWeb.SearchLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, query: "", results: frameworks())}
end
def render(assigns) do
~H"""
<form phx-change="search" phx-submit="search">
<input
id="search-input"
type="text"
name="q"
value={@query}
placeholder="Filter frameworks"
autocomplete="off"
phx-debounce="300"
/>
</form>
<button phx-click="clear">Clear</button>
<p>{length(@results)} matches</p>
<ul>
<li :for={name <- @results}>{name}</li>
</ul>
"""
end
def handle_event("search", %{"q" => q}, socket) do
{:noreply,
socket
|> assign(:query, q)
|> assign(:results, filter(q))}
end
def handle_event("clear", _params, socket) do
{:noreply, assign(socket, query: "", results: frameworks())}
end
defp frameworks do
["Phoenix", "Rails", "Laravel", "Django", "Next.js", "Nuxt", "SvelteKit", "Remix"]
end
defp filter(q) do
needle = String.downcase(q)
Enum.filter(frameworks(), &String.contains?(String.downcase(&1), needle))
end
end
mount/3 erhält Params, Session und Socket in genau dieser Reihenfolge und gibt den Socket mit den initialen Assigns zurück. render/1 gibt ein HEEx-Template zurück, das mit dem Sigil ~H erstellt wird. phx-change, phx-submit und phx-click senden jeweils ein benanntes Event an handle_event/3. Diese Funktion matcht auf den Event-Namen, gibt {:noreply, socket} mit aktualisierten Assigns zurück und überlässt es LiveView, einen Diff des betroffenen HTML zu pushen.
phx-change wird bei jeder Eingabe ausgelöst, sodass jeder Tastenanschlag einen Roundtrip bedeuten würde. phx-debounce begrenzt die Frequenz und akzeptiert Millisekunden oder "blur". Indem phx-submit auf dasselbe Event zeigt, verarbeitet LiveView die Enter-Taste, statt dass der Browser das Formular absendet. Der Leitfaden zu Formular-Bindings zeigt, dass Formular-Params als Map mit String-Schlüsseln ankommen, die nach den Eingabefeldern benannt sind, etwa %{"q" => "pho"}.
Templates interpolieren mit {@query}. Tutorials, die <%= @query %> verwenden, stammen aus der Zeit vor der aktuellen Syntax. Das Attribut :for ist die Kurzschreibweise von HEEx für eine Comprehension.
Das Elixir-Wissen, das Sie für diese Datei benötigen:
- Atome wie
:ok,:noreplyund:querysind benannte Konstanten, vergleichbar mit String-Enums. - Tupel wie
{:noreply, socket}sind Gruppen von Werten mit fester Größe. Callbacks geben sie zurück, um mitzuteilen, was passiert ist. - Funktionsköpfe mit Pattern Matching: Die beiden
handle_event/3-Klauseln sind separate Definitionen, und Elixir führt diejenige aus, deren erstes Argument zum Event-Namen passt.%{"q" => q}destrukturiert die Params so, wie esconst { q } = paramsin JavaScript tut. - Pipes:
|>übergibt den Wert auf der linken Seite als erstes Argument an den nächsten Aufruf. - Unveränderlichkeit (Immutability):
assigngibt einen neuen Socket zurück. Nichts wird an Ort und Stelle verändert.
Wie fügt man clientseitiges JavaScript mit einem Colocated Hook hinzu?
Mit einem Colocated Hook schreiben Sie das JavaScript einer Komponente in einem <script>-Tag direkt neben ihrem Markup. Der Name des Hooks beginnt mit einem Punkt, und das Element, an das er gebunden wird, muss eine eindeutige DOM-ID besitzen. Der Leitfaden zur JS-Interoperabilität behandelt die vollständige Hook-API.
Das folgende Beispiel ergänzt Tastenkürzel: / fokussiert das Suchfeld, Escape leert es. Ersetzen Sie das <input> in render/1 durch:
<input id="search-input" type="text" name="q" value={@query}
placeholder="Filter frameworks" autocomplete="off"
phx-debounce="300"
phx-hook=".SearchShortcuts" />
<script :type={Phoenix.LiveView.ColocatedHook} name=".SearchShortcuts">
export default {
mounted() {
this.onKey = (e) => {
if (e.key === "/" && document.activeElement !== this.el) {
e.preventDefault()
this.el.focus()
}
if (e.key === "Escape" && document.activeElement === this.el) {
this.el.value = ""
this.el.blur()
this.pushEvent("clear", {})
}
}
window.addEventListener("keydown", this.onKey)
},
destroyed() {
window.removeEventListener("keydown", this.onKey)
}
}
</script>
mounted wird ausgeführt, wenn das Element zur Seite hinzugefügt wird, und destroyed, wenn es entfernt wird. So wird der Listener einmal registriert und anschließend wieder aufgeräumt. pushEvent("clear", {}) löst dieselbe handle_event("clear", ...)-Klausel aus, die auch der Button verwendet, sodass Client und Server einen gemeinsamen Codepfad nutzen. Da das Feld geleert und defokussiert wird, bevor das Event gepusht wird, bleibt der sichtbare Wert mit dem Serverzustand synchron.
Das Modul ColocatedHook extrahiert das Skript zur Kompilierzeit. Ein Name mit vorangestelltem Punkt wird mit dem Modulnamen in einen Namespace gesetzt, und das Feature setzt Phoenix 1.8 oder neuer voraus. Die von mix phx.new generierte app.js registriert diese Hooks bereits:
import {hooks as colocatedHooks} from "phoenix-colocated/my_app"
// ...
hooks: {...colocatedHooks}
Wann passt LiveView – und wann nicht?
Jedes Event, das handle_event/3 verarbeitet, ist ein Roundtrip zum Server. Eine LiveView fühlt sich in einem schnellen Netzwerk verzögerungsfrei an, in einem langsamen dagegen träge. Rein visuelle Änderungen, etwa das Öffnen eines Dropdowns, lassen sich mit Phoenix.LiveView.JS-Befehlen im Browser ausführen und erreichen den Server gar nicht erst.
Auf localhost bleibt diese Latenz verborgen. Die von mix phx.new generierte assets/js/app.js stellt den Socket bereits als window.liveSocket bereit. Um eine langsame Verbindung zu simulieren, führen Sie Folgendes in der Browserkonsole aus:
liveSocket.enableLatencySim(500)
// later
liveSocket.disableLatencySim()
Der Latenzsimulator fügt die übergebene Anzahl an Millisekunden als einfache Latenz (One-Way) zum und vom Server hinzu. Im Produktivbetrieb zeigen Session Replays von LiveView-Apps bei langsamen Verbindungen dasselbe Problem aus Sicht der Nutzer: Eingaben, die dem Tippen hinterherhinken, und ein Aufflackern, wenn die Seite die Verbindung verliert und wiederherstellt.
LiveView bringt zwei weitere Kosten mit sich. Offline-Unterstützung ist schwierig, da Events eine aktive Serververbindung benötigen. Außerdem müssen Sie Elixir in Produktion betreiben, mit einem Prozess pro verbundener Seite.
| Situation | Bessere Wahl | Begründung |
|---|---|---|
| CRUD-Oberflächen, Dashboards, Formulare, Admin-Tools | LiveView | Keine separate API-Schicht; Zustand und Rendering bleiben auf dem Server |
| Offline-First-Apps oder Apps bei schlechter Konnektivität | React / Vue | LiveView benötigt zur Event-Verarbeitung eine aktive Verbindung |
| Intensive clientseitige Interaktion (Editoren, Canvas, UIs mit viel Drag-and-drop) | React / Vue | Jede Interaktion kostet einen Roundtrip; Hooks helfen, ersetzen aber kein Client-Framework |
| Team ohne Interesse an Elixir | React / Vue | Elixir muss auf dem Server betrieben und gewartet werden |
Einen breiteren Überblick über die Abwägung zwischen server- und clientgesteuerten UIs bietet dieser Vergleich von htmx mit Vue und React.
Fazit
LiveView eignet sich gut für CRUD-artige Anwendungen, die andernfalls eine JSON-API und einen clientseitigen Store benötigen würden. Sie schreiben Callbacks in einem einzigen Modul, der Server hält den Zustand, und Hooks oder JS-Befehle decken ab, was im Browser laufen muss. Ein sinnvoller nächster Schritt: Generieren Sie eine Phoenix-1.8-App, bauen Sie ein Formular aus Ihrer aktuellen SPA als LiveView nach und aktivieren Sie den Latenzsimulator, um das Verhalten bei einer langsamen Verbindung zu prüfen, bevor Sie sich festlegen.
FAQs
Was passiert, wenn eine LiveView ihre WebSocket-Verbindung verliert?
Der Client verbindet sich automatisch mit exponentiellem Back-off neu, und mount/3 wird erneut ausgeführt. Alle Assigns, die nicht in mount/3 neu aufgebaut werden, gehen daher verloren. Formulare, die sowohl ein id-Attribut als auch ein phx-change-Binding besitzen, werden automatisch wiederhergestellt: Nach dem erneuten Verbinden sendet LiveView die aktuellen Formularwerte über das phx-change-Event erneut. Mit phx-auto-recover lösen Sie ein anderes Wiederherstellungs-Event aus, mit dem Wert 'ignore' deaktivieren Sie die Wiederherstellung.
Wie verhindere ich, dass Code in mount/3 zweimal ausgeführt wird?
Kapseln Sie ihn in eine connected?(socket)-Prüfung. Phoenix.LiveView.connected?/1 gibt während des statischen HTTP-Renderings false zurück und true, sobald der WebSocket verbunden ist. Subscriptions, Timer und aufwendige Abfragen innerhalb eines if connected?(socket)-Blocks laufen daher nur im Live-Prozess. Belassen Sie günstige Standard-Assigns außerhalb der Prüfung, damit das statische Rendering weiterhin vollständiges, indexierbares HTML erzeugt.
Wie teste ich eine LiveView ohne Browser?
Verwenden Sie Phoenix.LiveViewTest, das mit LiveView ausgeliefert wird und unter ExUnit läuft. Rufen Sie live(conn, path) auf, um die View zu mounten und ein View-Handle zu erhalten, steuern Sie sie dann mit Helfern wie render_click und render_change und prüfen Sie das zurückgegebene HTML mit Assertions. Diese Tests kommunizieren direkt mit dem LiveView-Prozess, daher wird clientseitiges Hook-JavaScript darin nicht ausgeführt.
Was ist der Unterschied zwischen einer Function Component und einer LiveComponent?
Eine Function Component ist eine zustandslose Funktion, die Assigns entgegennimmt und HEEx zurückgibt; Events aus ihrem Markup gehen daher an die übergeordnete LiveView. Eine LiveComponent, definiert mit use Phoenix.LiveComponent, verwaltet ihren eigenen Zustand und verarbeitet ihre eigenen Events, wenn ihr Markup phx-target={@myself} setzt. Beide laufen innerhalb des Prozesses der übergeordneten LiveView. Verwenden Sie standardmäßig Function Components und greifen Sie nur dann zu einer LiveComponent, wenn Zustand und Event-Handling innerhalb der Komponente gekapselt sein müssen.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k