8. Elm - Eine funktionale Programmiersprache zur Entwicklung von Web-Apps
8.7 Mit der Außenwelt kommunizieren: Ports
Was tun Sie, wenn Sie nicht die ganze Funktionalität Ihrer App in Elm schreiben können oder wollen? Vielleicht wollen Sie ja eine externe Api verwenden, die nun mal nur in Javascript existiert. Hierfür gibt es in Elm das Konzept der Ports. Ein ausgehender Port ist eine Funktion (z.B.schickeDatenAnJS
), die Ihr Argument an die
Javascript-Laufzeitumgebung sendet. Diese kann dann einen Callback definieren, d.h. eine
Funktion
processWhateverDataFromElm
, die jedes Mal aufgerufen wird,
wenn im Elm-Code per schickeDatenAnJS
ein Cmd
erzeugt wird.
Demonstrieren
wir es an einem kleinen Beispiel
(lesen Sie auf jeden Fall auch das Kapitel in
elmprogramming; meine
Beispiele bauen
stark auf denen aus diesem Buch auf).
Als erstes müssen wir die Rahmenbedingungen schaffen, um mit Javascript zu kommunizieren. Erinnerin Sie sich daran, dass Elm entworfen wurde, um Web-Apps zu erstellen. Sie können ja nicht erwarten, dass jeder User Elm installiert hat. Daher können Sie Ihre Seite von Elm nach Html kompilieren:
elm make src/Website03ExpandList.elm
Success! Compiled 1 module.
Website03ExpandList ───> index.html
Führen Sie das mit einem Ihrer Elm-Programme durch und schauen in die
index.html
.
Sie sehen hier
den zu Javascript kompilierten Elm-Code.
Wenn Sie selbst zusätzlich noch Javascript schreiben wollen, dann bietet es sich an, das
kompilierte Elm
von der Html-Datei zu trennen. Dies geht so:
elm make src/Website03ExpandList.elm --output mycompiledfile.js
Success! Compiled 1 module.
Website03ExpandList ───> mycompiledfile.js
Jetzt müssen Sie aber noch "per Hand" die umgebende Html-Seite schreiben.
<!DOCTYPE html>
<html>
<body>
<div id="elm-code-is-loaded-here"></div>
<script src="./mycompiledfile.js"></script>
<script>
var app = Elm.Website03ExpandList.init({
node: document.getElementById("elm-code-is-loaded-here")
});
</script>
</body>
</html>
Die rot markierten Stellen müssen Sie natürlich an Ihre Gegebenheiten anpassen. Sie können jetzt ganz normal diese Html-Datei erweitern, Css oder weitere Javascript-Dateien einbinden und so weiter.
Ausgehende Nachrichten: von Elm nach Javascript
Öffnen Sie die Datei Page11Outgoing.elm. Ich repetiere hier kurz die wichtigsten Zeilen.
port module Page11Outgoing exposing (..)
Die Zeile, die den Modulnamen festlegt, muss mit port module
statt
module
beginnen;
port sendData : String -> Cmd msg
Das port
ist ein Keyword; sendData
ist der von
uns festgelegte Name einer Funktion. Die Funktion existiert nur
als Signatur und hat keinen Körper; grund dafür ist, dass Elm automatisch den
Code intern erzeugt, und auch schon festgelegt ist, was diese Funktion machen soll:
sie schickt ihr Argument an die Javascript-Laufzeitumgebung, die ihn dann
"abhören" kann.
Der Rückgabewert von sendData
ist ein Cmd
, den
sie z.B. in der Funktion update
als Rückgabewert zurückliefern
können, so wie in diesen Zeilen:
SendDataToJS ->
( model, sendData model.inputString )
Ein Druck auf den Knopf verändert das Modell nicht, erzeugt
aber einen Command: den model.inputString
an
die Javascript-Laufzeitumgebung zu schicken.
Wenn die Funktion sendData
gar keinen Körper hat,
warum braucht sie dann überhaupt einen Namen? Warum können wir nicht
einfach
port model.inputString
statt
sendData model.inputString
schreiben, wenn eh klar
ist, was das auslöst?
Die Antwort wird klar, wenn Sie sehen, wie wir von der Javascript-Seite aus
die Nachricht empfangen: hierfür brauchen wir den Namen der Funktion;
anders ausgedrückt, wenn Sie mehrere Kanäle wollen, um von Elm
Daten nach Javascript zu schicken, dann würden Sie mit dem Schlüsselwort
port
mehrere Port-Funktionen definieren, z.B.
Wie lesen wir per Javascript aus Ports? Kompilieren Sie die Datei Page11Outgoing.elm:port sendUsername : String -> Cmd msg
port sendLoginTime : Date -> Cmd msg
port sendFilenames : List String -> Cmd msg
elm make ports/Page11Outgoing.elm --output page11outgoing.js
und speichern Sie die Datei
page11outgoing.html.
Öffnen Sie sie und fügen Sie die rot markierten Codezeilen ein:
<!DOCTYPE html>
<html>
<body>
<div id="elm-code-is-loaded-here"></div>
<script src="./page11outgoing.js"></script>
<script>
var app = Elm.Page11Outgoing.init({
node: document.getElementById("elm-code-is-loaded-here")
});
app.ports.sendData.subscribe(function(data) {
console.log("Data from Elm: ", data);
document.querySelector("#message-from-port").textContent = JSON.stringify(data);
});
</script>
<hr>
<h2>This is javascript</h2>
<p>Message from elm: <span style="color:blue" id="message-from-port"></span></p>
</body>
</html>
In Zeile 11 sprechen Sie über
app.ports.sendData
die in Ihrem Elm-Code definierte Funktion
port sendData : String -> Cmd msg
an.
Der Javascript-Funktion subscribe
überreichen Sie eine
Callback-Funktion (im obigen Beispiel anonym per
function(data) {...}
definiert); diese wird dann jedes Mal ausgeführt,
wenn der Elm-Code einen mit sendData
erzeugten Command schickt.
Vorsicht: Elm ist immer noch eine funktionale Sprache.
Wenn Sie also zum Beispiel sendData "useless message"
an anderer
Stelle Ihres Codes erzeugen, dann erzeugt Ihnen das ein Cmd
-Objekt;
das wird aber nicht von sich aus der Laufzeitumgebung
Übungsaufgabe
Experimentieren Sie. Was geschieht, wenn Sie andere Datentypen
als String
über den Port schicken?
Insbesondere, wenn Sie rekursive Datentypen
wie zum Beispiel
BinaryTree = Leaf Int | Brach BinaryTree BinaryTree
über den Port schicken?
Übungsaufgabe Erweitern Sie den Code. Dieser soll nun zwei ausgehende Ports definieren. Der erste soll wie oben funktionieren. Über den neuen soll auf Knopfdruck eine Nachricht geschickt werden; Javascript soll dann in einem Counter die Anzahl dieser Nachrichten zählen und ausgeben.
Subscriptions
Subscriptions sind ein den Commands verwandtes Konzept. Erinnern Sie sich an
Cmd
:
hiermit bitten wir die Laufzeitumgebung, etwas zu tun, was Elm-Code nicht direkt schafft, z.B.
einen Http-Request abzuschicken; der jeweilige Cmd
braucht dann üblicherweise
als Argument eine Message
, damit er weiß, was er unserem Elm-Code schicken soll,
wenn
der
Command ausgeführt ist (also beispielsweise der Http-Server geantwortet hat).
Subscriptions sind ähnlich, unterscheiden sich aber dadurch, dass sie sozusagen "andauernde"
Commands sind;
einfachstes Beispiel wäre wahrscheinlich "schicke mir alle 500 Millisekunden die Message
ClockTick
".
Hierfür müssen Sie das Paket elm/time
installieren:
elm install elm/time
Lesen und starten Sie dann bitte . Die entscheidenden Codezeilen sind
listenToClockTicks : Model -> Sub Message
listenToClockTicks model =
Time.every 500 ClockTick
in der ein Sub
-Objekt erzeugt wird, und
main : Program () Model Message
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = listenToClockTicks
}
wo wir per subscriptions = listenToClockTicks
unsere Subscription der Außenwelt
mitteilen.
Übungsaufgabe Schreiben Sie eine Elm-App, die im Sekundentakt eine Liste von Fibonacci-Zahlen erweitert. Beginnen soll sie mit 0 und 1.
Eingehende Nachrichten mit Ports
Erweitern wir nun unser altes Beispiel; jetzt sollen auch Daten von Javascript nach Elm
hereingeschickt werden
können. Wie können wir der Laufzeitumgebung mitteilen, dass immer, wenn Javascript uns etwas
schickt,
wir die Nachricht XYZ
erwarten? Wenn Sie immer-dann-wenn eine Nachricht
empfangen
wollen,
machen Sie das mit Subscriptions: Sub msg
, so wie im letzten
Beispiel listenToClockTicks
.
Schauen Sie sich Page12SendReceive.elm
an,
insbesondere folgende Zeilen:
Intype Message
= SendDataToJS
| InputChanged String
| ReceivedDataFromJS String
update
legen wir fest, was mit dieser Nachricht zu tun ist:
Wir erschaffen den Port:update : Message -> Model -> ( Model, Cmd Message )
update msg model =
case msg of
...ReceivedDataFromJS data ->
( { model | messageFromJavascript = data }, Cmd.none )
port receiveData : (String -> msg) -> Sub msg
und registrieren ihn schließlich als Subscription:
main : Program () Model Message
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = _ -> receiveData ReceivedDataFromJS
}
Wir kompilieren:
elm make ports/Page12SendReceive.elm --output page12sendreceive.js
und passen die Html-Seite an (port-spezifische Passagen sind rot).
<!DOCTYPE html>
<html>
<body>
<hr>
<h2>Here is elm.</h2>
<div id="elm-code-is-loaded-here"></div>
<script src="./page12sendreceive.js"></script>
<script>
app = Elm.Page12SendReceive.init({
node: document.getElementById("elm-code-is-loaded-here")
});
app.ports.sendData.subscribe(function (data) {
console.log("Data from Elm: ", data);
document.querySelector("#messages-from-port").textContent = JSON.stringify(data);
});
</script>
<div>
<hr>
<h2>Here is javascript</h2>
<p>You can enter a new Elm model in json: <span id="messages-from-port" style="color:blue"></p>
<p><textarea cols="40" rows="5" id="message-to-port" placeholder="elm model as json"></textarea>
<button onClick="buttonClicked()">Send to elm</button>
</p>
</div>
<script>
function buttonClicked() {
let data = document.querySelector("#message-to-port").value;
app.ports.receiveData.send(data);
}
</script>
</body>
</html>
Übungsaufgabe Schreiben Sie eine Elm-App, auf der man per Knopfdruck die Cookie-Daten anzeigen lassen kann und per Knopfdruck auch setzen kann.
Übungsaufgabe Schreiben Sie eine App mit Elm-Frontend und einem Server. Das Frontend soll eine Websocket-Verbindung mit dem Server aufbauen. Der Server liest von der Konsole Zeilen ein und schickt die dann über den Websocket. Das Frontend soll die dann anzeigen.
Hierfür brauchen Sie einen eingehenden Port. Das Javascript in der finalen Html-Seite soll dann beim Laden der Seite einen Websocket zum Server eröffnen und alle eingehenden Nachrichten der Elm-App über den Port schicken.
Übungsaufgabe
Fügen Sie der App aus der letzten Aufgabe eine Gegenrichtung hinzu.
Der User soll auf dem Frontend einen Text eingeben können, der
dann nicht per Http-Request sondern per Websocket an den Server übertragen werden soll.
Hierfür brauchen Sie einen ausgehenden Port. Der umgebenden Html-Seite
müssen Sie nun Javascript-Code app.ports.YOUR_PORT_NAME.subscribe(...)
hinzufügen
und in ...
eine Funktion schreiben, die die Daten über den Websocket
verschickt.
Übungsaufgabe Schreiben Sie eine Elm-App mit einem Svg-Objekt und MouseEvents. Jede Mausbewegung über dem Svg-Objekt soll über Websockets an einen Server übertragen werden. Den Websocket müssen Sie im Javascript-Code einrichten und dann über ausgehende Ports bedienen.
Übungsaufgabe Erweitern Sie Ihre Seite aus der letzten Aufgabe, so dass eingehende Nachrichten vom Websocket laufend angezeigt werden. Es steht Ihnen frei, zu entscheiden, welche Nachrichten der Server über den Websocket schicken soll. Zufallszahlen, um es einfach zu halten?