zurück zum Artikel

Skalierbare, robuste Webanwendungen mit Elixir und Phoenix

Martin Grotz
Skalierbare, robuste Webanwendungen mit Elixir und Phoenix

Verteilte Applikationen sicher entwickeln und ohne Laufzeitfehler ausführen? Mit dem Webserver Phoenix sowie den Programmiersprachen Elixir und Elm geht das.

Auf den ersten Blick scheint die Erweiterung der bestehenden Architekturmuster um Microservices eine relativ neue Entwicklung zu sein. Doch verteilte Systeme werden in Bereichen wie der Telekommunikation schon seit Jahrzehnten ganz selbstverständlich genutzt. Dabei findet vor allem die Programmiersprache Erlang mit ihrer BEAM genannten Ausführungsumgebung und dem Framework OTP für den Betrieb verteilter Anwendungen Verwendung.

Auf Basis von Erlang entstand in den letzten Jahren eine funktionale Programmiersprache, die die Vorteile von Erlang mit einer modernen Syntax und einem zeitgemäßen Tooling einfacher zugänglich macht: Elixir.

In der Entwicklung von Web-Frontends setzen sich, angeschoben durch Angular und TypeScript, mehr und mehr statisch typisierte Sprachen durch. Architekturmuster zum einfacheren Verwalten des Zustands der Anwendung wie React Redux sorgen zusätzlich dafür, dass die zunehmend umfangreicher werdenden Webanwendungen trotzdem stabil und zuverlässig funktionieren.

Reichen die Sicherheiten von TypeScript und Redux nicht aus, bietet sich eine Programmiersprache wie Elm an, die ein starkes Typsystem mit einer unidirektionalen Datenarchitektur kombiniert. Elm erlaubt sicheres Entwickeln und eine Ausführung ohne Laufzeitfehler. Eine detaillierte Auseinandersetzung damit folgt in einem zweiten Beitrag.

Im Rahmen dieses Zweiteilers wird ein einfaches Ratespiel entwickelt: Der Spieler muss eine Zahl zwischen 1 und 100 erraten. Der erste Teil beschreibt die Entwicklung der Server-Anwendung mit Elixir und Phoenix. Dabei kommt das Aktor-Modell von Erlang zum Einsatz, um für jeden Spieler eine eigene Spiel-Session zu erzeugen und zu verwalten, die unabhängig von allen anderen ist.

Im zweiten Teil des Artikels [1] folgt dann eine in Elm geschriebene Weboberfläche, die es Benutzern ermöglicht, ihre geratene Zahl über ein Formular einzugeben und das Ergebnis zu sehen.

Elixir ist eine funktionale Programmiersprache, deren Schwerpunkt im Erstellen skalierbarer, robuster und nebenläufiger Programme liegt. Sie nutzt die über Jahrzehnte hinweg erprobte und weiterentwickelte Erlang-VM, denn der Elixir-Compiler erzeugt Erlang-kompatiblen Bytecode. Die grundlegende Syntax ist an Ruby angelehnt und lässt sich über Makros erweitern. Obwohl Elixir noch verhältnismäßig jung ist – 2014 erschien Versionsnummer 1.0 – gibt es eine umfangreiche Tool-Unterstützung, ein ausreichend großes Ökosystem an Bibliotheken und eine hilfsbereite, offene und aktive Community.

Die Installation unterscheidet sich je nach gewähltem Betriebssystem – Elixir steht für macOS, Linux und Windows sowie den Raspberry Pi, aber auch als Docker-Image zur Verfügung. Für Windows gibt es ein All-inclusive-Installationspaket. Aber auch für die anderen Betriebssysteme existieren vorgefertigte Pakete, die alles mitbringen. Eine detaillierte Anleitung findet sich auf der Elixir-Homepage [2].

Nach der Elixir-Installation lässt sich deren Erfolg am einfachsten auf der Kommandozeile durch die Eingabe von elixir --version überprüfen. Im folgenden Projekt kommt Version 1.6.5 samt einer interaktiven Elixir-Konsole zum Einsatz, die nach Ausführung von iex startet. Leider funktioniert unter Windows in iex die Autovervollständigung von Modul- und Funktionsnamen via Tabulator-Taste nicht – unter Linux und macOS gibt es damit keine Probleme. Die interaktive Konsole führt jeden Befehl sofort aus, sofern dieser ein abgeschlossenes Elixir-Kommando ist. Besondere Endmarkierungen wie ;; oder eine spezielle Tastenkombination sind nicht nötig.

Zusätzlich zu den üblichen Datentypen wie Integer, Float und String kennt Elixir noch zwei, die ihren Ursprung in der Erlang-Welt haben: PID als Referenz auf Erlang-Prozesse auf dem lokalen oder einem entfernten System und atoms als Konstanten, mit denen sich bestimmte Werte oder Zustände markieren lassen. Jedes atom beginnt mit einem Doppelpunkt, gefolgt von einer Zeichenkette. So gibt es zum Beispiel Funktionen zum Dateizugriff, die nach erfolgreicher Ausführung einen mit :ok markierten Wert zurückliefern, oder andernfalls eine mit :error markierte Fehlermeldung.

Der letzte wichtige Basis-Datentyp ist die Map als Sammlung von Key-Value-Pärchen: Sie wird über die Syntax %{key1 => value1, key2 => value2} oder %{atom1: value, atom2: value} erzeugt. Die Datentypen der jeweiligen Schlüsselwerte müssen nicht alle vom gleichen Datentyp sein – häufig kommen aber atoms oder Strings zum Einsatz, ohne zu mischen. Der Zugriff erfolgt dann entweder über die Punktnotation, sofern die Schlüssel alle atoms sind, oder aber mit eckigen Klammern. Hauptunterschied: Bei eckigen Klammern wird beim Zugriff auf einen nicht existierenden Key der konstante Wert nil zurückgeliefert, bei der Punktnotation erscheint stattdessen ein KeyError.

Das Pattern Matching spielt in Elixir eine zentrale Rolle. Genau genommen gibt es nicht einmal eine einfache Zuweisung, da das Versehen eines Werts mit einem Bezeichner bereits dazu führt, dass Elixir versucht, die linke und rechte Seite des Gleichheitszeichens zur Übereinstimmung zu bringen. Dies lässt sich in iex sehr einfach zeigen:

iex> x = 3
iex> 2 = x
** (MatchError) no match of right hand side value: 3

Prinzipiell lässt sich die gleiche linke Seite aber mit verschiedenen rechten Seiten gleichsetzen, wobei jeweils der Wert des letzten Vorgangs gilt. Verhindern kann man dieses Verhalten mit dem Pin-Operator ^:

iex> x = 3
iex> x = 4
iex> x
4
iex> ^x = 5
** (MatchError) no match of right hand side value: 5

Pattern Matching funktioniert mit praktisch jedem Elixir-Datentyp. Es wird häufig bei der rekursiven Verarbeitung von Listen verwendet und zur Extraktion von Daten aus Tupeln oder Maps. Außerdem kann es in Kombination mit dem Schlüsselwort case zur Fallunterscheidung innerhalb von Funktionen dienen.

Da Elixir eine funktionale Programmiersprache ist, sind Funktionen ein zentraler Bestandteil jedes Programms. Hierbei entstehen vor allem pure Funktionen geringen Umfangs, deren Ausgabedaten nur von den explizit übergebenen Argumenten abhängen. Idealerweise kombiniert man mehrere solcher kleinen Transformationsfunktionen zu größeren Funktionalitäten, sodass am Ende ein gut test- und wartbares Gesamtsystem entsteht.

Jede Funktion ist dabei über ihren Namen und die Anzahl der Parameter (die sogenannte "Stelligkeit" bzw. im Englischen "arity") eindeutig festgelegt. Im Funktionskopf kann bereits ein Pattern Matching erfolgen. Nur wenn das Matching erfolgreich erfüllt ist, wird die jeweilige Funktionsdefinition ausgeführt. Normalerweise gibt es mehrere Definitionen, die jeweils unterschiedliche Fälle behandeln. Daher muss man nur selten auf if-else oder Fallunterscheidungen via case zurückgreifen. Gibt es keine Funktionsdefinition, in die sich die Argumente erfolgreich einpassen lassen, erhält man eine Fehlermeldung.

Es gibt verschiedene Arten der Funktionsdefinition:

# Funktion in einem Modul
def function_name(param1, param2) do
...
end
# Anonyme Funktion
anon_fn = fn param1, param2 -> param1 + param2 end
# Kurzschreibweise für anonyme Funktion
anon_fn = &(&1 + &2)

Das grundlegende Programmiermodell in Erlang, und damit auch in Elixir, beruht auf Aktoren. Hierbei tauschen voneinander unabhängige Aktoren asynchron Nachrichten aus. Diese werden jeweils in einer Mailbox pro Aktor abgelegt, bis der jeweilige Aktor sie explizit verarbeitet. Die Verarbeitung innerhalb eines Aktors erfolgt synchron, jedwede Kommunikation zwischen Aktoren aber asynchron und ausschließlich nachrichtenbasiert.

Skalierbare, robuste Webanwendungen mit Elixir und Phoenix

Das Aktor-Modell von Erlang und Elixir (Abb.1)

In Erlang ist jeder Aktor ein eigener Erlang-Prozess, wobei diese Prozesse nicht mit „schwergewichtigen“ Betriebssystemprozessen zu verwechseln sind. Erlang selbst läuft auf modernen Betriebssystemen in einem einzigen Prozess, der wiederum intern Threads verwendet, um mit einem auf Nebenläufigkeit optimierten präemptiven Scheduler [3] die Erlang-Prozesse möglichst effizient mit CPU-Zeit zu versorgen.

Durch die Kombination von Aktorenmodell und Erlang-Scheduler lässt sich eine optimale Auslastung beliebig vieler CPU-Kerne erreichen, ohne dass man bei der Entwicklung der Software besondere Rücksicht auf die Synchronisation der einzelnen Aktoren nehmen muss. Dies ist eine deutliche Erleichterung gegenüber der expliziten (und fehleranfälligen) Synchronisation in anderen Programmiermodellen beziehungsweise -sprachen.

Als Beispiel für die Arbeit mit Elixir und das Zusammenspiel mit Elm dient ein einfaches Spiel, bei dem Benutzer auf einer Website eine Zahl zwischen 1 und 100 erraten müssen. Zum Anlegen des Projekts zur Server-Implementierung kommt das wichtigste Tool zur Projektverwaltung mit Elixir zum Einsatz: mix. Damit lassen sich unter anderem Projekte erstellen, der Compiler aufrufen, Code generieren und Abhängigkeiten aktualisieren. Ein Aufruf von mix help zeigt die gesamte Liste der möglichen Befehle, die sich zudem beliebig erweitern lässt – etwa durch Bibliotheken oder eigenen Code.

Das Elixir-Projekt (bzw. Phoenix) lässt sich mit einem einzigen Befehl einrichten. Elixirs standardmäßige Datenzugriffsbibliothek Ecto kommt explizit nicht zum Einsatz, da das Beispielprojekt keine Datenbank benötigt:

mix phx.new guess_the_number --module Game --app game --no-ecto

Die erstellte Ordnerstruktur folgt bestimmten Konventionen, die am Anfang durchaus gewöhnungsbedürftig sind. Sämtlicher Anwendungscode liegt im Unterordner /lib und ist aufgeteilt in die webspezifischen Teile (Controller, Views, Templates, Routing) in /app_name_web/ und die eigentliche Geschäftslogik in den Contexts in /app_name/. Im Ordner /config/ findet sich mit der config.exs die allgemeine Konfiguration sowie in den jeweils anderen Dateien die Einstellungen je nach gewählter Umgebung.

Im /priv/-Ordner sind die Dateien abgelegt, die später unverändert in ein Release-Archiv kopiert werden, zum Beispiel statische Website-Assets oder Datenbank-Migrationsskripte. Alle Tests landen im /test-Ordner. Die Verwaltung von Website-Assets wie Elm-Code spielt sich komplett im /assets-Ordner ab. Hier kommen npm zur Paketverwaltung und webpack als Build-Tool zum Einsatz. Auf oberster Ebene findet sich noch die Datei mix.exs, in die alle Abhängigkeiten sowie die Startup-Konfiguration der Anwendung eingetragen werden.

Der Server setzt sich aus mehreren verschiedenen Teilen zusammen. Der grundlegende Ablauf einer Spielsitzung beginnt mit dem expliziten Start der Sitzung. Der dabei erzeugte Prozess und dessen Prozess-ID (PID) werden zusammen mit einer laufenden Sitzungsnummer gespeichert. Mit der Sitzungsnummer kann der Client genau seine Sitzung ansprechen und seine Rate-Versuche übermitteln. Am Ende des Spiels wird die Sitzung, und damit auch der Prozess, beendet.

Jede Spielsitzung ist also ein eigener Prozess und dadurch von allen anderen Sitzungen entkoppelt – auch ein eventueller Absturz betrifft nur genau diese Sitzung. Elixir kennt verschiedene Abstraktionen zur Ausgestaltung von Prozessen. Die beiden wichtigsten sind GenServer ("generic server process") für zustandsbehaftete Server-Prozesse und Agents als reine Zustandsspeicher. In beiden Fällen ist der vorgehaltene Zustand nicht persistent – endet der jeweilige Prozess, so ist auch der Zustand unrettbar verloren.

Der Session-GenServer wird in der Datei /lib/game/session.ex implementiert. In jedem GenServer müssen mindestens zwei Funktionen vorhanden sein, die zum Starten des Prozesses notwendig sind:

defmodule Game.Session do
use GenServer

alias Game.GameState

def init(:no_args) do
{:ok, %GameState{number: Enum.random(1..100)}}
end

def start_link(_) do
GenServer.start_link(__MODULE__, :no_args)
end
end

Da der Prozess keine Start-Argumente übergeben bekommt, ist das Atom :no_args zu verwenden. Um eine Zufallszahl zwischen 1 und 100 zu erzeugen, kommt in der init-Funktion die Enum.random()-Funktion zusammen mit einer Range zum Einsatz. Die init-Funktion muss als Ergebnis den initialen Zustand des GenServers zurückgeben. Im vorliegenden Fall ist das eine Map mit festgelegten Keys, was in Elixir einem struct entspricht und in der Datei game_state.ex zusammen mit Standardwerten wie folgt definiert wird:

defmodule Game.GameState do
defstruct number: nil, guesses: []
end

Der Bequemlichkeit halber lassen sich Module via alias-Anweisung in anderen Modulen mit verkürztem Namen verwenden.

Die nach außen hin sichtbare API des Session-GenServers wird über einfache Funktionen beschrieben, die dann intern den eigentlichen GenServer-Aufruf absetzen. Das Ratespiel benötigt nur eine einzige Funktion:

def guess(session_id, guess) do
send_guess(guess, Game.SessionStore.id_to_pid(session_id))
end

Um die verschiedenen möglichen Antworten des später noch genauer beschriebenen SessionStore abhandeln zu können, bietet es sich an, direkt im Funktionskopf ein Pattern Matching auszuführen. Dafür muss man die send_guess-Funktion zweimal ausführen:

defp send_guess(_guess, :error) do
:fatal_error
end

defp send_guess(guess, {:ok, pid}) do
GenServer.call(pid, {:guess, guess})
end

Im Fehlerfall wird ein entsprechendes Atom zurückgegeben, im Fall, dass es geklappt hat, wird eine Anfrage an die übergebene Prozess-ID gesendet. Die Nachricht ist hierbei das zweite Argument für GenServer.call: ein Tupel aus einem Atom für den Nachrichtentyp und den Nutzdaten, also der geratenen Zahl. Der Aufruf via call erfolgt synchron, das heißt, der jeweilige Prozess wartet auf die Antwort und ist solange blockiert. Im Fall eines cast wird nicht gewartet, sondern der aufrufende Code läuft sofort mit der nächsten Anweisung weiter.

Passend zu call und cast gibt es die tatsächlichen Message-Handler handle_call und handle_cast. Diese sind für jede Nachrichtentyp- und Nutzdatenkombination einzeln zu implementieren. Die Spielsitzung benötigt nur einen Handler, der die geratene Zahl entgegennimmt, prüft, ob diese bereits geraten wurde, und falls nicht noch mit der gesuchten Zahl vergleicht.

Die als Ergebnis zurückgelieferten Atoms variieren je nach beschrittenem Pfad. Am Ende jedes handle_call muss der aktuelle, gegebenenfalls veränderte Zustand mit zurückgegeben werden, um ihn beim nächsten Aufruf wieder an handle_call übergeben zu können. Die eigentliche Zustandsverwaltung erfolgt hierbei transparent im Hintergrund. Im konkreten Fall wird die Liste der bisher geratenen Zahlen um den neuesten Versuch erweitert, eine entsprechend veränderte Kopie des bisherigen Zustands erzeugt und zusammen mit dem Rate-Ergebnis zurückgeliefert:

 def handle_call({:guess, guess}, _from, state) do
result =
case Enum.member?(state.guesses, guess) do
true ->
:already_guessed

false ->
case guess === state.number do
true ->
:correct

false ->
if guess < state.number do
:wrong_higher
else
:wrong_lower
end
end
end

{:reply, result, %GameState{state | guesses: [guess | state.guesses]}}
end

Zur Verwaltung von laufender Session-ID und zugehöriger Prozess-ID dient ein Agent als einfache Abstraktion zur Zustandsverwaltung. Er wird in der Datei session_store.ex abgelegt. Jeder Agent muss jeweils wieder eine Callback-Funktion zum Erzeugen des Prozesses implementieren. Da das Beispielprojekt nur einen einzigen SessionStore benötigt, kann dieser mit einem Namen versehen und so später leichter angesprochen werden:

defmodule Game.SessionStore do
use Agent

@me SessionStore

def start_link(_opts) do
Agent.start_link(fn -> %{next_id: 1} end, name: @me)
end
end

In start_link muss eine Funktion angegeben werden, die den initialen Zustand zurückliefert – eine Map mit einem Key next_id, unter dem die jeweils nächste Session-ID zu finden ist.

Der SessionStore bietet nach außen hin drei Funktionen an, über die sich Session-Prozess-Zuordnungen anlegen, abfragen und wieder löschen lassen:

def add(pid) do
Agent.get_and_update(@me, fn store ->
{next_id, store} =
Map.get_and_update(store, :next_id, fn old_id -> {old_id, old_id + 1} end)

store = Map.put(store, next_id, pid)

{next_id, store}
end)
end

def remove(session_id) do
Agent.update(@me, &Map.delete(&1, session_id))
end

def id_to_pid(session_id) do
Agent.get(@me, &Map.fetch(&1, session_id))
end

Das erste Argument jeder Agent-Funktion ist der jeweilige Prozess. Hier ist das ein lokaler Prozess mit einem Namen. In einem verteilten System kann es aber auch ein Prozess auf einem anderen Rechner sein. In get_and_update werden die nächste Session-ID aus der Zustands-Map geholt und der Wert anschließend aktualisiert. Unter der gerade ermittelten Session-ID ist dann noch die zugehörige Prozess-ID abzulegen und der Rückgabewert – ein Tupel aus dem tatsächlich zum Aufrufer der add-Funktion zurückgelieferten Wert und dem neuen Zustand – zu konstruieren. Das Ergebnis des letzten Ausdrucks in einer Funktion verwendet Elixir automatisch als Rückgabewert der Funktion, sodass kein return nötig ist.

remove und id_to_pid nutzen jeweils Standardfunktionen der Elixir-Map. Durch die &-Kurzschreibweise bei Funktionen lassen sich anonyme Funktionen ad-hoc erzeugen. Die übergebenen Argumente lassen sich dann in dieser Funktion mit &1, &2, ... ansprechen. &Map.fetch(&1, session_id) entspricht also ausgeschrieben fn store -> Map.fetch(store, session_id) end.

Als letzter Baustein des Servers fehlt jetzt noch der Verwalter aller Sessions: der SessionSupervisor. Dieser Prozess erzeugt und beendet dynamisch die Session-Prozesse. Daher kommt die von Elixir bereitgestellte DynamicSupervisor-Abstraktion in der Datei session_supervisor.ex zum Einsatz:

defmodule Game.SessionSupervisor do
use DynamicSupervisor

@me SessionSupervisor

def start_link(_) do
DynamicSupervisor.start_link(__MODULE__, :no_args, name: @me)
end

def init(:no_args) do
DynamicSupervisor.init(strategy: :one_for_one)
end
end

Als API werden zwei Funktionen angeboten:

def connect() do
{:ok, pid} = DynamicSupervisor.start_child(@me, Game.Session)
Game.SessionStore.add(pid)
end

def disconnect(session_id) do
terminate(session_id, Game.SessionStore.id_to_pid(session_id))
end

In connect werden ein neuer Session-Prozess gestartet und dessen PID im SessionStore hinterlegt. Da das Ergebnis des letzten Ausdrucks als Rückgabewert dient und die add-Funktion des SessionStore die neue Session-ID zurückgibt, bekommt der Aufrufer von connect auch eben diese ID als Ergebnis.

Die in disconnect aufgerufene terminate-Funktion nutzt wiederum ein Pattern Matching direkt im Funktionskopf zur Unterscheidung der möglichen Rückgaben von id_to_pid, wobei Fehler im Beispiel einfach ignoriert werden:

defp terminate(session_id, {:ok, pid}) do
DynamicSupervisor.terminate_child(@me, pid)
Game.SessionStore.remove(session_id)
end

defp terminate(_session_id, :error) do
# NOOP
end

Damit die beiden Prozesse SessionStore und SessionSupervisor überhaupt starten, sind sie in der application.ex in die Liste der zu startenden Prozesse einzutragen:

def start(_type, _args) do
children = [
GameWeb.Endpoint,
Game.SessionStore,
Game.SessionSupervisor
]
...

Damit ist der Server komplett und lässt sich nach Eingabe von iex -S mix in der Kommandozeile im Projektordner ausprobieren:

iex> session_id = Game.SessionSupervisor.connect()
iex> Game.Session.guess(session_id, 50)
iex> Game.SessionSupervisor.disconnect(session_id)

Als Standard-Webserver für Elixir hat sich Phoenix etabliert. Phoenix kombiniert diverse Bibliotheken mit Makros und Konventionen zu einem umfangreichen Paket, mit dem sich schnell erste Ergebnisse erzielen lassen, das aber trotzdem keine Kompromisse bei der Geschwindigkeit eingeht [4].

Die Gesamtarchitektur von Phoenix umfasst verschiedene Bausteine. Eine Beschreibung der für das Beispielprogramm relevanten folgt später. Channels als Abstraktion über Websockets sind ebenso wenig nötig wie die für die Code-Organisation der Business-Logik bei komplexeren Programmen üblichen Contexts. Beide Elemente bleiben daher in den weiteren Betrachtungen unberücksichtigt.

Skalierbare, robuste Webanwendungen mit Elixir und Phoenix

Die Basisarchitektur des Webservers Phoenix (Abb. 2)

Der Game-Server des Beispielprogramms nutzt nur wenige Aspekte der Phoenix-Funktionen, da die eigentliche Bedienoberfläche nicht via Phoenix-Templates umgesetzt, sondern als Elm Single Page Application ausgelegt ist. Der Phoenix-Webserver startet im Projektverzeichnis via mix phx.server. Anschließend steht unter der Adresse http://localhost:4000 die Standardstartseite bereit:

Skalierbare, robuste Webanwendungen mit Elixir und Phoenix

Allgemeine Startseite eines neuen Phoenix-Projekts (Abb. 3)

Anschließend sind im Router, durch den alle Anfragen immer laufen müssen, in /lib/game_web/router.ex die Routen anzulegen, um den Zugriff via HTTP zu ermöglichen. Es bietet sich an, vorher eine API-Pipeline zu definieren, in der die bei jedem API-Zugriff zu durchlaufenden Schritte als Kette von Plugs – also kleine, den Request oder Response verändernde Funktionen – festgehalten sind:

pipeline :api do
plug :accepts, ["json"]
plug :fetch_query_params
end

Die benötigten Routen zum Starten und Beenden von Game-Sessions sowie zum Übermitteln einer geratenen Zahl werden der Einfachheit halber allesamt als GET-Requests implementiert:

scope "/api", GameWeb do
pipe_through :api

get "/guess", GuessController, :guess
get "/session/connect", SessionController, :connect
get "/session/disconnect", SessionController, :disconnect
end

Durch die Angabe eines Scopes lassen sich automatisch alle Routen mit diesem Präfix versehen und pipe_through sorgt dafür, dass jede Anfrage die entsprechende Plugs-Pipeline durchläuft. get wiederum erwartet als Argumente eine URL, einen Controller, an den die Anfrage durchgereicht wird, und die anzusteuernde Funktion im Controller als Atom. Controller dienen in Phoenix als Brücke zwischen HTTP-Anfragen und -Antworten sowie der Geschäftslogik im jeweiligen Context.

Nach dem Speichern der Datei lässt sich die korrekte Erstellung der Routen in der Kommandozeile via mix phx.routes prüfen. Dies ist vor allem dann sehr hilfreich, wenn komplexe Scope-Hierarchien erstellt oder aber mit dem resource-Makro ganze REST-Ressourcen mit nur einer Zeile Code festgelegt werden. mix phx.routes zeigt immer die einzelnen, tatsächlich generierten Routen an.

Sofern der Webserver noch läuft (andernfalls lässt er sich via mix phx.server wieder starten), können die Routen im Browser getestet werden. Ein Aufruf von http://localhost:4000/api/session/connect liefert eine Session-ID zurück, zum Beispiel 1. Nach Aufruf von http://localhost:4000/api/guess?sessionId=1&guess=50 lässt sich damit ein Tipp abgeben. Via http://localhost:4000/api/session/disconnect?sessionId=1 lässt sich die Session anschließend beenden.

Der Spielserver ist damit einsatzbereit und lässt sich von jedem Client nutzen, der HTTP-Anfragen absetzen kann. Im zweiten Teil folgt die Entwicklung eines ebensolchen Clients in Elm [5]. Dabei sollen die Vorteile der unidirektionalen Datenflussarchitektur von Elm genauso zum Tragen kommen wie die verschiedenen Möglichkeiten des Typsystems, um zügig eine fehlerfreie Webanwendung zu entwickeln.

Martin Grotz
beschäftigt sich seit dem Aufkommen von Single Page Applications als Full-Stack-Entwickler mit Architektur, Umsetzung und dem Betrieb komplexer Web-Anwendungen. Fest davon überzeugt, dass es immer noch besser gehen kann, hat er sich mittlerweile verstärkt der funktionalen Programmierung zugewandt und dabei unter anderem die Programmiersprachen Elm und Elixir für sich entdeckt.

Siehe hierzu auf heise Developer auch:

(map [7])


URL dieses Artikels:
https://www.heise.de/-4286403

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Weboberflaechenentwicklung-mit-Elm-4301420.html
[2] https://elixir-lang.org/install.html
[3] https://hamidreza-s.github.io/erlang/scheduling/real-time/preemptive/migration/2016/02/09/erlang-scheduler-details.html
[4] https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections
[5] https://www.heise.de/hintergrund/Weboberflaechenentwicklung-mit-Elm-4301420.html
[6] https://www.heise.de/hintergrund/Weboberflaechenentwicklung-mit-Elm-4301420.html
[7] mailto:map@ix.de