Die meisten Sprachen gelten in ihrer Anfangsphase als „Spielzeugsprachen“ und werden nur für triviale oder kleine Projekte verwendet. Aber das ist bei Elm nicht der Fall, wo seine wahre Stärke in komplexen und großen Anwendungen zum Tragen kommt.
Es ist nicht nur möglich, Teile einer Anwendung in Elm zu erstellen und diese Komponenten in eine größere JS-Anwendung zu integrieren, sondern es ist auch möglich, die gesamte Anwendung zu erstellen, ohne eine andere Sprache zu berühren, was sie zu einer ausgezeichneten Alternative zu JS-Frameworks wie React macht.
In diesem Artikel untersuchen wir die Struktur einer Elm-Anwendung anhand eines einfachen Beispiels einer Website zur Verwaltung von Textdokumenten.
Artikelserie
- Warum Elm? (Und wie man damit anfängt)
- Einführung in die Elm-Architektur und wie wir unsere erste Anwendung erstellen
- Die Struktur einer Elm-Anwendung (Sie sind hier!)
Einige der in diesem Artikel behandelten Themen sind
- Anwendungsarchitektur und wie die Dinge darin ablaufen.
- Wie die Funktionen
init,updateundviewin der Beispielanwendung definiert sind. - API-Kommunikation mit Elm Commands.
- Single-Page-Routing.
- Arbeiten mit JSON-Daten.
Dies sind die Hauptthemen, denen Sie beim Erstellen fast jeder Art von Anwendung begegnen werden, und die gleichen Prinzipien können auf größere Projekte erweitert werden, indem einfach die benötigte Funktionalität ohne wesentliche oder grundlegende Änderungen hinzugefügt wird.
Um diesem Artikel folgen zu können, empfiehlt es sich, das Github-Repository auf Ihrem Computer zu klonen, damit Sie das Gesamtbild sehen können; dieser Artikel erklärt alles auf beschreibende Weise, anstatt ein Schritt-für-Schritt-Tutorial zu sein, das sich mit Syntax und anderen Details befasst. Daher können die Elm-Syntaxseite und die Elm-Paketseite für spezifische Details zum Code, wie z. B. Typ-Signaturen der verwendeten Funktionen, sehr hilfreich sein.
Über die Anwendung
Das Beispiel, das wir in diesem Artikel beschreiben werden, ist eine CRUD-Anwendung für Textdokumente mit HTTP-Kommunikation zu einer API zur Interaktion mit einer Datenbank. Da das Hauptthema dieses Artikels die Elm-Anwendung ist, wird der Server nicht im Detail erklärt; es handelt sich lediglich um eine gefälschte Node.js REST-API, die Daten in einer einfachen JSON-Datei mithilfe von json-server speichert.

In der Anwendung enthält die Hauptseite ein Eingabefeld zum Schreiben des Titels eines neuen Dokuments und darunter eine Liste zuvor erstellter Dokumente. Wenn Sie auf ein Dokument klicken oder ein neues erstellen, wird eine Bearbeitungsseite angezeigt, auf der Sie Inhalt anzeigen, bearbeiten und hinzufügen können.

Unter dem Titel des Dokuments befinden sich zwei Links: **Speichern** und **Löschen**. Wenn auf **Speichern** geklickt wird, wird das aktuelle Dokument als PUT-Anforderung an den Server gesendet. Wenn auf **Löschen** geklickt wird, wird eine DELETE-Anforderung an die id des aktuellen Dokuments gesendet.
Dokumente werden erstellt, indem eine POST-Anforderung mit dem Titel des neuen Dokuments und einem leeren Inhaltsfeld gesendet wird. Sobald das Dokument erstellt ist, wechselt die Anwendung in den Bearbeitungsmodus und Sie können mit dem Hinzufügen des Textes fortfahren.
Ein schneller Tipp zur Arbeit mit den Quelldateien
Wenn Sie mit Elm – und wahrscheinlich mit jeder anderen Sprache – arbeiten, müssen Sie sich mit externen Details befassen, die über den Code selbst hinausgehen. Das Wichtigste, was Sie tun möchten, ist, die Befehle für die Entwicklung zu automatisieren: Starten des API-Servers, Starten des Elm Reactors, Kompilieren von Quelldateien und Starten eines Dateibeobachters, um bei jeder Änderung erneut zu kompilieren.
In diesem Fall habe ich Jake verwendet, ein einfaches Node.js-Tool, das Make ähnelt. Im Jakefile habe ich alle notwendigen Befehle und einen Standardbefehl enthalten, der die anderen parallel ausführt, sodass ich nur jake default im Terminal/in der Befehlszeile ausführen muss und alles einsatzbereit ist.
Wenn Sie ein größeres Projekt haben, können Sie auch anspruchsvollere Tools wie Gulp oder Grunt verwenden.
Nach dem Klonen der Anwendung und der Installation der Abhängigkeiten können Sie den Befehl npm start ausführen, der den API-Server, den Elm Reactor und einen Dateibeobachter startet, der Elm-Dateien bei jeder Änderung kompiliert. Sie können die kompilierten Dateien unter **https://:3000** und die Anwendung im Debug-Modus (mit Elm Reactor) unter **https://:8000** sehen.
Die Anwendungsarchitektur
Im vorherigen Artikel haben wir die Idee der Elm-Architektur vorgestellt, aber um Komplexität zu vermeiden, haben wir eine Anfängerversion mit der Funktion Html.beginnerProgram präsentiert. In dieser Anwendung verwenden wir eine erweiterte Version, die es uns ermöglicht, Befehle und Abonnements einzubeziehen, obwohl die Prinzipien gleich bleiben.
Die vollständige Struktur sieht wie folgt aus
Jetzt haben wir eine Funktion Html.program, die einen Record mit 4 Funktionen akzeptiert
- init : ( Model, Cmd Msg ): Die Funktion
initgibt ein Tupel mit dem Anwendungsmodell und einem Befehl zurück, der eine Nachricht trägt. Dies ermöglicht uns die Kommunikation mit der Außenwelt und die Erzeugung von Seiteneffekten. Wir werden diesen Befehl verwenden, um Daten über HTTP abzurufen und an die API zu senden. - update : Msg -> Model -> ( Model, Cmd Msg ): Die Update-Funktion nimmt zwei Dinge entgegen: eine Nachricht mit allen möglichen Aktionen in unserer Anwendung und ein Modell, das den Zustand der Anwendung enthält. Sie gibt dasselbe wie die vorherige Funktion zurück, jedoch mit aktualisierten Werten, abhängig von den empfangenen Nachrichten.
- view : Model -> Html Msg: Die View-Funktion nimmt ein Modell mit dem Anwendungszustand entgegen und gibt HTML zurück, das Nachrichten verarbeiten kann. Normalerweise enthält sie eine Reihe von Funktionen, die HTML ähneln und Werte aus dem Modell rendern.
- subscriptions : Model -> Sub Msg: Die Subscriptions-Funktion nimmt ein Modell entgegen und gibt ein Abonnement mit einer Nachricht zurück. Jedes Mal, wenn ein Abonnement etwas empfängt, sendet es eine Nachricht, die in der
update-Funktion abgefangen werden kann. Wir können uns für Aktionen abonnieren, die jederzeit auftreten können, wie z. B. Mausbewegungen oder Netzwerkereignisse.
Sie können beliebig viele Funktionen haben, aber am Ende kehren alle zu diesen vier zurück.
Hinter den Kulissen verwaltet die Elm Runtime den Ablauf unserer Anwendung, alles, was wir tun, ist im Grunde die Definition von was fließt. Zuerst beschreiben wir den Anfangszustand der Anwendung, ihre Datenstruktur und einen Befehl, der initial ausgeführt wird, dann werden die Ansichten basierend auf diesen Daten angezeigt. Bei jeder Interaktion, jedem Abonnementereignis oder jeder Befehlsausführung wird eine neue Nachricht an die Update-Funktion gesendet, und der Zyklus beginnt erneut, mit einem neuen Modell und/oder einem auszuführenden Befehl.
Wie Sie sehen können, müssen wir uns tatsächlich nicht mit irgendeiner Art von Kontrollfluss in der Anwendung befassen. Da Elm eine funktionale Sprache ist, deklarieren wir Dinge lediglich.
Routing: Die Navigation.program Funktion
Die Beispielanwendung besteht aus zwei Hauptseiten: der Startseite mit einer Liste zuvor erstellter Dokumente und einer Bearbeitungsseite, auf der Sie das Dokument anzeigen oder bearbeiten können. Der Übergang zwischen den beiden Ansichten erfolgt jedoch ohne Neuladen der Seite. Stattdessen werden nur die benötigten Daten vom Server abgerufen und die Ansicht aktualisiert, einschließlich der URL (Zurück- und Vorwärts-Schaltflächen funktionieren weiterhin wie erwartet).
Um dies zu erreichen, haben wir zwei Pakete verwendet: Navigation und UrlParser. Das erste kümmert sich um den gesamten Navigationsbereich, und das zweite hilft uns bei der Interpretation von URL-Pfaden.
Das Navigation-Paket bietet eine Wrapper-Funktion für Html.program, die es uns ermöglicht, Seitenpositionen zu verwalten. Sie sehen also im Code, dass wir stattdessen Navigation.program verwenden, was im Grunde dasselbe wie das vorherige ist, aber auch eine Nachricht akzeptiert, die wir UrlChange genannt haben und die jedes Mal gesendet wird, wenn der Browser den Standort wechselt. Die Nachricht trägt einen Wert vom Typ Navigation.Location, der alle Informationen enthält, die wir benötigen, einschließlich des Pfads, den wir parsen können, um die richtige Ansicht auszuwählen.
Die Init-Funktion
Die Init-Funktion kann als Eintrittspunkt der Anwendung betrachtet werden und repräsentiert sowohl den Anfangszustand (Modell) als auch jeden Befehl, den wir beim Start der Anwendung ausführen möchten.
Typdefinitionen
Wir beginnen mit der Definition der Typen von Werten, die wir verwenden werden, beginnend mit dem Model-Typ, der **allen** Zustand der Anwendung enthält.
type alias Model =
{ currentLocation : Maybe Route
, documents : List Document
, currentDocument : Maybe Document
, newDocument : Maybe String
}
- Wir speichern einen Wert
currentDocumentvom TypMaybe Route, der den aktuellen Standort auf der Seite enthält. Wir verwenden diesen Wert, um zu wissen, was auf dem Bildschirm angezeigt werden soll. - Wir haben eine Liste von Dokumenten namens
documents, in der wir alle Dokumente aus der Datenbank speichern. Wir brauchen hier keinenMaybe-Wert; wenn wir keine Dokumente haben, können wir einfach eine leere Liste haben. - Wir benötigen auch einen Wert
currentDocumentvom TypMaybe Document. Er enthältJust Document, wenn wir ein Dokument öffnen, undNothing, wenn wir uns auf der Startseite befinden. Dieser Wert wird abgerufen, wenn wir ein bestimmtes Dokument aus der Datenbank anfordern. - Schließlich haben wir
newDocument, das den Titel eines neuen Dokuments in Form einesMaybe Stringdarstellt. Es istJust String, wenn etwas im Eingabefeld steht, andernfalls ist esNothing. Dieser Wert wird an die API gesendet, wenn das Formular abgeschickt wird.
Hinweis: Es mag unnötig erscheinen, diesen Wert hier zu haben. Aus JavaScript kommend könnten Sie denken, dass Sie den Wert direkt aus dem Eingabeelement abrufen können, aber in Elm müssen Sie alles im Modell definieren; wenn Sie etwas in das Eingabeelement eingeben, wird das Modell aktualisiert, und wenn Sie das Formular absenden, wird der Wert im Modell über HTTP gesendet.
Wie Sie sehen können, verwenden wir in Model auch andere Typ-Aliase, die wir definieren müssen, nämlich Document und Route.
type alias Document =
{ id : Int
, title : String
, content : String
}
type Route
= HomeRoute
| DocumentRoute Int
Zuerst definieren wir einen Typ-Alias Document, um die Struktur eines Dokuments darzustellen: ein Wert id, der eine Ganzzahl enthält, dann den Titel und den Inhalt, beides sind Strings.
Wir erstellen auch einen Vereinigungstyp, der zwei oder mehr verwandte Typen gruppieren kann. In diesem Fall sind sie für die Navigation der Anwendung nützlich. Sie können sie beliebig benennen. Wir haben nur zwei: einen für die Startseite namens HomeRoute und einen für die Bearbeitungsansicht, der DocumentRoute heißt und eine Ganzzahl enthält, die die id des angeforderten spezifischen Dokuments darstellt.
Zusammenfügen
Sobald wir die Typen definiert haben, deklarieren wir die Init-Funktion mit ihren Anfangswerten.
init : Navigation.Location -> ( Model, Cmd Msg )
init location =
( { currentLocation = UrlParser.parsePath route location
, documents = []
, currentDocument = Nothing
, newDocument = Nothing
}
, getDocumentsCmd
)
Nachdem wir das Navigation-Paket eingeführt haben, akzeptiert unsere init-Funktion nun einen Wert vom Typ Navigation.Location, der Informationen vom Browser über den aktuellen Seitenstandort enthält. Wir speichern diesen Wert in einem Parameter namens location, damit wir ihn parsen und als currentLocation speichern können. Wir verwenden diesen Wert, um zu wissen, welche richtige Ansicht angezeigt werden soll.
Der Wert currentLocation wird mithilfe der Funktion parsePath aus dem Navigation-Paket ermittelt. Sie akzeptiert eine Parser-Funktion (vom Typ Parser (a -> a) a) und eine Location.
Der in currentLocation gespeicherte Wert hat einen Maybe-Typ. Wenn wir beispielsweise einen Pfad /documents/12 im Browser haben, erhalten wir Just DocumentRoute 12.
Die Parser-Funktion, die wir route genannt haben, ist wie folgt aufgebaut:
route : UrlParser.Parser (Route -> a) a
route =
UrlParser.oneOf
[ UrlParser.map HomeRoute UrlParser.top
, UrlParser.map DocumentRoute (UrlParser.s "document" </> UrlParser.int)
]
Die wichtigsten Teile sind
HomeRoute UrlParser.top
Wir erstellen im Grunde eine Beziehung, bei der HomeRoute der Typ ist, den wir für die Home-Route definiert haben, und UrlParser.top, der die Wurzel (/) im Pfad darstellt.
Dann haben wir
DocumentRoute (UrlParser.s "document" </> UrlParser.int)
Hier haben wir wieder einen Routentyp namens DocumentRoute und dann (UrlParser.s "document" </> UrlParser.int), was einem Pfad wie /document/<id> entspricht. Die Funktion s akzeptiert eine Zeichenkette, in diesem Fall document, und gleicht alles ab, was document enthält (wie /document/…). Dann haben wir die Funktion </>, die als Darstellung des Schrägstrichzeichens im Pfad (/) betrachtet werden kann, um den Teil document vom int-Wert zu trennen; die ID des Dokuments, das wir sehen möchten.
Der Rest unseres Modells besteht aus einer Liste von Dokumenten, die standardmäßig leer ist, obwohl sie aufgefüllt wird, sobald der Befehl getDocumentCmd abgeschlossen ist. Es gibt auch Werte für das aktuelle Dokument und ein neues Dokument, beide sind Nothing.
Die Update-Funktion
Die Update-Funktion arbeitet mit einer Nachricht und einem Modell als Eingabe und gibt ein Tupel mit einem neuen Modell und einem Befehl als Ausgabe zurück. Normalerweise hängt die Ausgabe von der verarbeiteten Nachricht ab.
In unserer Anwendung haben wir für jedes Ereignis eine Nachricht definiert
- Wenn ein neuer Seitenstandort angefordert wird.
- Wenn sich der Seitenstandort ändert.
- Wenn ein neuer Dokumententitel in das Eingabeelement eingegeben wird.
- Wenn ein neues Dokument gespeichert und erstellt wird.
- Wenn alle Dokumente in der Datenbank abgerufen wurden.
- Wenn ein bestimmtes Dokument angefordert und abgerufen wird.
- Wenn der Titel und Inhalt eines Dokuments, das gerade aktualisiert wird.
- Wenn ein bestimmtes Dokument gespeichert und abgerufen wird.
- Wenn ein Dokument gelöscht wird.
Und dies kann mithilfe eines Vereinigungstyps geschehen
type Msg
= NewUrl String
| UrlChange Navigation.Location
| NewDocumentMsg String
| CreateDocumentMsg
| CreatedDocumentMsg (Result Http.Error Document)
| GotDocumentsMsg (Result Http.Error (List Document))
| ReadDocumentMsg Int
| GotDocumentMsg (Result Http.Error Document)
| UpdateDocumentTitleMsg String
| UpdateDocumentContentMsg String
| SaveDocumentMsg
| SavedDocumentMsg (Result Http.Error Document)
| DeleteDocumentMsg Int
| DeletedDocumentMsg (Result Http.Error String)
Einige Nachrichten müssen zusätzliche Informationen tragen, und diese können neben dem Nachrichtennamen definiert werden. Zum Beispiel hat die Nachricht NewUrl eine angehängte String, die einen neuen URL-Pfad enthält.
Außerdem sind die meisten Nachrichten in Paaren zu finden, insbesondere Nachrichten, die dem Runtime einen neuen Befehl hinzufügen. Eine Nachricht wird vor der Ausführung des Befehls gesendet und die andere, nachdem er ausgeführt wurde.
Wenn Sie beispielsweise ein Dokument löschen, senden Sie eine Nachricht DeleteDocumentMsg mit der id des zu löschenden Dokuments. Sobald das Dokument gelöscht ist, wird eine Nachricht DeletedDocumentMsg gesendet, die das Ergebnis des HTTP-Aufrufs enthält: einen Statuswert Http.Error und das Ergebnis als String.
Wie wir als Nächstes sehen werden, sollten Nachrichten, die das Ergebnis eines Befehls enthalten, für beide Werte gepatternt werden, entweder als Fehler oder als erfolgreicher Wert.
Sobald wir alle Nachrichten definiert haben, können wir damit beginnen, zu verarbeiten, was wir mit jeder einzelnen tun werden. Dazu pattern wir die Nachricht. Nehmen wir zum Beispiel das Lesen eines bestimmten Dokuments:
ReadDocumentMsg id ->
( model, getDocumentCmd id )
Dies gleicht die Nachricht ReadDocumentMsg ab, die eine int (gemäß unserer Typdefinition) namens id enthält.
Hinweis: Der Name des int-Wertes wird zugewiesen, wenn er abgeglichen wird. Bevor er abgeglichen wird, ist der Wert lediglich etwas vom Typ Int.
Dann geben wir ein Tupel zurück, das das Modell ohne Änderungen enthält, aber wir geben auch einen auszuführenden Befehl zurück, der getDocumentCmd heißt und die ID des Dokuments als Eingabe erhält. Machen Sie sich noch keine Gedanken über die Befehlsdefinition, die behandeln wir weiter unten.
Jetzt müssen wir die Nachricht abgleichen, die gesendet wird, sobald wir das angeforderte Dokument erhalten haben.
GotDocumentMsg (Ok document) ->
( { model | currentDocument = Just document }, Cmd.none )
GotDocumentMsg (Err _) ->
( model, Cmd.none )
Denken Sie daran, dass die Nachricht GotDocumentMsg einen Wert vom Typ (Result Http.Error Document) trug, daher müssen wir ihre beiden möglichen Werte abgleichen: wenn sie erfolgreich war und wenn sie fehlgeschlagen ist.
Der erste Fall hier passt, wenn der Fehler vom Typ Ok ist, was bedeutet, dass kein Fehler aufgetreten ist, und der zweite Wert das abgerufene Dokument ist. Dann können wir das Tupel zurückgeben, das ein modifiziertes model enthält, bei dem der Wert currentDocument das gerade abgerufene Dokument ist, vorangestellt durch ein Just, da currentDocument einen Typ von Maybe hat. Außerdem geben wir nun im zweiten Teil des Tupels an, dass wir keinen Befehl ausführen werden (Cmd.none).
Im zweiten Fall, bei dem ein Fehler aufgetreten ist, gleichen wir ihn mit einem Wert vom Typ Err ab und können ein _ als Platzhalter für alles verwenden, was dort sein könnte. In einer realen Anwendung könnten wir eine Informationsbox anzeigen, die den Benutzer über den Fehler informiert, aber um die Komplexität in diesem Beispiel zu vermeiden, werden wir ihn einfach ignorieren. Wir geben also das Modell erneut ohne Änderungen zurück und führen auch keinen Befehl aus.
Alle anderen Nachrichtenabgleiche folgen demselben Muster: Sie geben ein neues Modell mit den von der Nachricht getragenen Informationen und/oder führen einen Befehl aus.
API-Kommunikation mit Befehlen
Obwohl Elm eine reine funktionale Programmierung ist, können wir immer noch Seiteneffekte wie HTTP-Kommunikation mit einem Server durchführen, und dies geschieht mithilfe von Befehlen.
Wie wir bereits gesehen haben, geben wir jedes Mal, wenn eine Nachricht abgeglichen wird, ein Tupel mit einem neuen Modell und einem Befehl zurück. Ein Befehl ist jede Funktion, die einen Wert vom Typ Cmd zurückgibt.
Werfen wir einen Blick auf den Befehl, den wir in unserer init-Funktion aufgenommen haben, der beim Start der Anwendung eine Anfrage an den Server mit allen Dokumenten in der Datenbank durchführt.
getDocumentsCmd : Cmd Msg
getDocumentsCmd =
let
url =
"https://:3000/documents?_sort=id&_order=desc"
request =
Http.get url decodeDocuments
in
Http.send GotDocumentsMsg request
Die beiden wichtigen Teile der Funktion sind ihre Typdeklaration getDocumentsCmd : Cmd Msg und Http.send GotDocumentsMsg request im in-Abschnitt.
Der Typ bedeutet, dass es sich um einen Befehl handelt und er auch eine Nachricht trägt. Dies ergibt sich aus dem Typ, den die Funktion Http.send zurückgibt, den Sie in der Paketdokumentation sehen können.
Im Körper der Funktion sehen wir die Nachricht, die gesendet wird, sobald die Anfrage abgeschlossen ist. Zu Klarheitszwecken haben wir zwei Variablen erstellt: eine mit der URL der API, an die die Anfrage gesendet wird, und die andere, die die Anfrage selbst enthält, die Http.send senden wird.
Die Anfrage wird mithilfe der Funktion Http.get erstellt, da wir eine GET-Anfrage an den Server senden möchten.
Sie können dort auch eine Funktion decodeDocuments bemerken; dies ist ein JSON-Decoder. Wir verwenden ihn, um die Serverantwort in JSON in einen verwendbaren Elm-Wert zu transformieren. Wie die in dieser Anwendung verwendeten Decoder aufgebaut sind, sehen wir im nächsten Abschnitt.
Der Befehl zum Abrufen eines einzelnen Dokuments vom Server ist ziemlich ähnlich, da die Funktion Http.get die meiste Arbeit für uns erledigt, um die Anfrage zu erstellen. Wir ändern nur die URL der Ressource, die wir wollen, in diesem Fall unter Verwendung der id des angeforderten Dokuments.
Um Daten an den Server zu senden, ist die Historie etwas anders; stattdessen können wir die Anfrage selbst mithilfe der Funktion Http.request erstellen.
Betrachten wir die Funktion, die ein neues Dokument an den Server sendet.
createDocumentCmd : String -> Cmd Msg
createDocumentCmd documentTitle =
let
url =
"https://:3000/documents"
body =
Http.jsonBody <| encodeNewDocument documentTitle
expectedDocument =
Http.expectJson decodeDocument
request =
Http.request
{ method = "POST"
, headers = []
, url = url
, body = body
, expect = expectedDocument
, timeout = Nothing
, withCredentials = False
}
in
Http.send CreatedDocumentMsg request
Wieder haben wir eine Funktion, die einen Wert vom Typ Cmd Msg zurückgibt, aber jetzt nehmen wir auch einen Wert vom Typ String, nämlich den Titel des neuen zu erstellenden Dokuments.
Mithilfe der Funktion Http.request übergeben wir einen Record als Parameter, der alle Teile der Anfrage enthält. Wir interessieren uns hauptsächlich für Folgendes:
method: Die HTTP-Methode der Anfrage. Wir haben zuvor GET verwendet, um Informationen vom Server abzurufen, aber jetzt, da wir die Informationen senden, verwenden wir die Methode POST.url: Endpunkt der API, der die Anfrage empfängt.body: Der Rumpf der Anfrage, der das Dokument enthält, das wir der Datenbank hinzufügen möchten, in Form von JSON. Zum Erstellen des Rumpfes verwenden wir die Funktion Http.jsonBody, die automatisch einen HeaderContent-Type: application/jsonhinzufügt. Diese Funktion erwartet einen JSON-Wert, den wir mit einem JSON-Encoder und dem Titel des neuen Artikels erstellen. Wie der JSON-Encoder implementiert ist, sehen wir im nächsten Abschnitt.expect: Hier geben wir an, wie wir die Antwort der Anfrage interpretieren sollen. In unserem Fall erhalten wir das neue Dokument zurück, also verwenden wir die Funktion Http.expectJson, um die Antwort mithilfe unseres JSON-DecodersdecodeDocumentzu transformieren.
Die Funktion Http.send ist praktisch dieselbe wie die zuvor erwähnte; der einzige Unterschied besteht darin, dass wir nun eine Nachricht CreatedDocumentMsg senden, sobald das Dokument erstellt wurde.
Der Befehl zum Aktualisieren eines Dokuments ist dem Befehl zum Erstellen eines neuen Dokuments sehr ähnlich. Die Hauptunterschiede sind:
- Wir senden die Daten an einen anderen API-Endpunkt, abhängig von der
iddes Dokuments, das wir aktualisieren möchten. - Der Rumpf wird mit einem vollständigen Dokument erstellt und mithilfe eines anderen Encoders in JSON kodiert.
- Die verwendete HTTP-Methode ist
PUT, was die bevorzugte Methode für Aktualisierungen vorhandener Ressourcen ist. - Wir verwenden die Nachricht
SavedDocumentMsg, sobald wir eine Antwort erhalten.
Zuletzt haben wir die Befehlsfunktion deleteDocumentCmd. Die Prinzipien bleiben dieselben, aber in diesem Fall senden wir nichts im Rumpf der Anfrage, daher verwenden wir Http.emptyBody. Außerdem geben wir an, dass wir einen Wert vom Typ String erwarten, aber das ist eigentlich egal, da wir ihn in unserer Anwendung nicht verwenden.
Arbeiten mit JSON-Werten
In Elm können wir JSON nicht direkt in unserem Code verwenden, noch können wir eine einfache Parsing-Funktion wie JSON.parse() verwenden, wie wir es in JavaScript tun, da wir sicherstellen müssen, dass die Daten, mit denen wir arbeiten, typsicher sind.
Um JSON in Elm zu verwenden, müssen wir den JSON-Wert in einen Elm-Wert dekodieren und können dann damit arbeiten. Dies tun wir mithilfe von JSON-Decodern. Auch das Umgekehrte ist ähnlich; um einen JSON-Wert zu erzeugen, müssen wir einen Elm-Wert mithilfe von JSON-Encodern kodieren.
In unserer Beispielanwendung haben wir zwei Decoder und zwei Encoder. Analysieren wir die Decoder.
decodeDocuments : Decode.Decoder (List Document)
decodeDocuments =
Decode.list decodeDocument
decodeDocument : Decode.Decoder Document
decodeDocument =
Decode.map3 Document
(Decode.field "id" Decode.int)
(Decode.field "title" Decode.string)
(Decode.field "content" Decode.string)
Eine Decoder-Funktion muss den Typ Decoder haben (der in diesem Fall Decode.Decoder ist, aufgrund der Art, wie wir das JSON-Paket importiert haben). In der Signatur wird auch der Typ der Daten im Decoder angegeben. Der erste ist eine Liste von Dokumenten, daher ist der Typ List Document, und der zweite ist einfach ein Dokument, daher hat er den Typ Document (diesen Typ haben wir zu Beginn der Anwendung definiert).
Wie Sie bemerken können, komponieren wir diese beiden Encoder tatsächlich, da der erste eine Liste von Dokumenten dekodiert, können wir den Dokumenten-Decoder für die Funktion Decode.list verwenden.
Im Decoder decodeDocument passiert die eigentliche Arbeit. Wir verwenden die Funktion Decode.map3, um einen Wert mit drei Feldern zu dekodieren: id, title und content, mit ihren jeweiligen Typen. Das Ergebnis wird dann in den Document-Typ eingefügt, den wir zu Beginn der Anwendung definiert haben, um den endgültigen Wert zu erstellen.
Hinweis: Elm hat acht Mapping-Funktionen zur Verarbeitung von JSON-Werten. Wenn Sie mehr als das benötigen, können Sie das Paket elm-decode-pipeline verwenden, das den Aufbau beliebiger Decoder mit dem Pipeline-Operator (|>) ermöglicht.
Jetzt können wir sehen, wie die beiden Encoder implementiert sind.
encodeNewDocument : String -> Encode.Value
encodeNewDocument title =
let
object =
[ ( "title", Encode.string title )
, ( "content", Encode.string "" )
]
in
Encode.object object
encodeUpdatedDocument : Document -> Encode.Value
encodeUpdatedDocument document =
let
object =
[ ( "id", Encode.int document.id )
, ( "title", Encode.string document.title )
, ( "content", Encode.string document.content )
]
in
Encode.object object
Zum Kodieren eines JavaScript-Objekts verwenden wir die Funktion Encode.object, die eine Liste von Tupeln akzeptiert. Jedes Tupel enthält den Namen des Schlüssels und den kodierten Wert, abhängig von seinem Typ, in diesem Fall Encode.int und Encode.string. Außerdem geben diese Funktionen im Gegensatz zu Decodern immer einen Wert vom Typ Value zurück.
Da wir ein Dokument mit leerem Inhalt erstellen, benötigt der erste Encoder nur den Titel dieses Dokuments, und wir kodieren manuell ein leeres Inhaltsfeld, bevor wir es an die API senden. Der zweite Encoder akzeptiert ein vollständiges Dokument und erzeugt einfach ein JSON-Äquivalent.
Weitere Funktionen im Zusammenhang mit JSON finden Sie auf der Elm-Paketseite: Json.Decode und Json.Encode.
Die View-Funktion
Die View-Funktion bleibt im Vergleich zum Code früherer Artikel ziemlich einfach. Die interessante Änderung hier ist die Art und Weise, wie wir jede Seite basierend auf dem URL-Pfad anzeigen.
Zuerst haben wir einen Link, der immer auf die Startseite zeigt. Anstatt normale Links zu verwenden, fangen wir das Klickereignis ab und senden eine Nachricht NewUrl mit dem neuen Pfad.
Da wir in unserer Anwendung immer noch normale <a>-Elemente anstelle von Schaltflächen verwenden, haben wir ein benutzerdefiniertes Ereignis namens onClickLink erstellt, das dasselbe wie das Ereignis onClick ist, jedoch mit der Verhinderung des Standardverhaltens (preventDefault) des angeklickten Elements.
Die Implementierung dieses Ereignisses sieht wie folgt aus:
onClickLink : msg -> Attribute msg
onClickLink message =
let
options =
{ stopPropagation = False
, preventDefault = True
}
in
onWithOptions "click" options (Decode.succeed message)
Wichtig ist hier die Verwendung der Funktion onWithOptions, die es uns ermöglicht, zwei Optionen zum click-Ereignis hinzuzufügen: stopPropagation und preventDefault. Die Option, die hier den entscheidenden Unterschied macht, ist preventDefault, die das Standardverhalten des <a>-Elements verhindert.
Als Nächstes haben wir die Implementierung der Funktion, die die anzuzeigende Seite basierend auf dem Pfad in der URL verwaltet.
page : Model -> Html Msg
page model =
case model.currentLocation of
Just route ->
case route of
HomeRoute ->
viewHome model
DocumentRoute id ->
case model.currentDocument of
Just document ->
viewDocument document
Nothing ->
div [] [ text "Nothing here…" ]
Nothing ->
div [] [ text "404 – Not Found" ]
Denken Sie daran, dass wir den aktuellen Standort in einer Variable currentLocation im Modell speichern. Daher können wir diese Variable mit Mustervergleich (Pattern Matching) verwenden und je nach Wert etwas anzeigen. In unserem Beispiel prüfen wir zuerst, ob der Maybe-Wert vom Typ Just Route oder Nothing ist. Dann, wenn wir eine Route haben, prüfen wir, ob es sich um eine HomeRoute oder eine DocumentRoute handelt. Für den ersten Fall fügen wir die Funktion viewHome ein, die den Inhalt der Startseite repräsentiert, und für den zweiten Fall übergeben wir den Wert currentDocument an die Funktion viewDocument, die das ausgewählte Dokument anzeigt.
Beachten Sie für jeden Dokumenteneintrag in der Funktion viewDocumentEntry, dass wir erneut eine Nachricht NewUrl mit dem Link zum jeweiligen Dokument mithilfe des Ereignisses onLinkClick senden. Diese Nachricht ist für das Laden des entsprechenden Dokuments verantwortlich.
Schließlich können wir Inline-CSS in jeder Komponente hinzufügen, indem wir eine Funktion vom Typ Attribute mit Html.Attributes.style hinzufügen, die folgende Form hat:
myStyleFunction : Attribute Msg
myStyleFunction =
Html.Attributes.style
[ ( "<property>", "<value>" )
]
Im Beispielanwendung haben wir die Stile einiger Komponenten und andere generische Stile direkt in der HTML-Datei, in der die Anwendung eingebettet ist. Sie können wählen, ob Sie CSS-Dateien direkt einbinden möchten, so wie Sie es normalerweise auf jeder Website tun würden, oder ob Sie sie direkt in den Elm-Quelldateien schreiben möchten. Während die in diesem Beispiel gezeigte Methode recht einfach ist, gibt es eine spezialisierte Bibliothek dafür, falls Sie mehr Kontrolle benötigen: Elm-css.
Ein Wort zu Abonnements
Abonnements sind in vielen Anwendungen üblich. Obwohl wir in diesem Beispiel keine Abonnements verwendet haben, ist ihr Mechanismus recht einfach: Sie ermöglichen es uns, auf bestimmte Dinge zu *hören*, ohne zu wissen, wann sie passieren werden.
Sehen wir uns die Grundstruktur eines Abonnements an
subscriptions : Model -> Sub Msg
subscriptions model =
WebSocket.listen "ws://echo.websocket.org" NewMessage
Das Erste, was erwähnt werden muss, ist, dass alle Abonnements einen Typ `Sub` haben. Hier haben wir `Sub Msg`, weil wir jedes Mal eine Nachricht senden, wenn wir etwas über das Abonnement empfangen.
Die Funktionsweise ist, dass die Funktion `WebSocket.listen` einen Socket-Listener für die Adresse `ws://echo.websocket.org` erstellt. Jedes Mal, wenn etwas ankommt, wird die Nachricht `NewMessage` gesendet, und in unserer Update-Funktion können wir entsprechend auf diese Nachricht reagieren, wie wir es bereits getan haben (dank der Elm-Architektur).
Anwendungs-Einbettung
Nachdem wir nun gesehen haben, wie eine vollständige Anwendung aufgebaut ist, ist es an der Zeit zu sehen, wie wir diese Anwendung in eine HTML-Datei zur Verteilung aufnehmen können. Obwohl Elm HTML-Dateien generieren kann, können Sie auch einfach JavaScript generieren und es selbst einbinden, sodass Sie auch Kontrolle über andere Dinge, wie z. B. die Formatierung, haben.
In HTML können Sie Folgendes einfügen
…
<body>
<main>
<!-- The app is going to appear here -->
</main>
<script src="main.js"></script>
<script>
// Get the <main> element
var node = document.getElementsByTagName('main')[0];
// Embed the Elm application in the <main> element
var app = Elm.Main.embed(node);
</script>
</body>
…
Zuerst binden wir die kompilierte Elm-Datei als .js in einem `