Wir sprechen weiterhin über Elm 0.18 .
Ulme. Bequem und umständlich
Ulme. Bequem und umständlich. Json.Encoder und Json.Decoder
Ulme. Bequem und umständlich. HTTP, Aufgabe
In diesem Artikel werden die Probleme der Elm-Anwendungsarchitektur und mögliche Optionen für die Implementierung des Komponentenentwicklungsansatzes behandelt.
Betrachten Sie als Aufgabe die Implementierung eines Dropdown-Fensters, in dem ein registrierter Benutzer eine Frage hinzufügen kann. Im Falle eines anonymen Benutzers schlägt er vor, sich zuerst anzumelden oder zu registrieren.
Wir gehen auch davon aus, dass es später erforderlich sein kann, den Empfang anderer Arten von Benutzerinhalten zu implementieren, aber die Logik der Arbeit mit autorisierten und anonymen Benutzern bleibt dieselbe.
Umständliche Komposition
Der Quellcode einer naiven Implementierung . Im Rahmen dieser Implementierung werden wir alles in einem Modell speichern.
Alle für die Autorisierung und Benutzerabfrage erforderlichen Daten befinden sich im Modell auf derselben Ebene. Die gleiche Situation mit Nachrichten (Msg).
type alias Model = { user: User , ui: Maybe Ui -- Popup is not open is value equals Nothing , login: String , password: String , question: String , message: String } type Msg = OpenPopup | LoginTyped String | PasswordTyped String | Login | QuestionTyped String | SendQuestion
Der Schnittstellentyp wird als Vereinigungstyp Ui beschrieben, der mit dem Typ Vielleicht verwendet wird.
type Ui = LoginUi -- Popup shown with authentication form | QuestionUi -- Popup shown with textarea to leave user question
Somit beschreibt ui = Nothing das Fehlen eines Popup-Fensters, und das Just-Popup ist mit einer bestimmten Oberfläche geöffnet.
In der Aktualisierungsfunktion werden das Paar, die Nachricht und die Benutzerdaten abgeglichen. Abhängig von diesem Paar werden verschiedene Aktionen ausgeführt.
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of
Wenn Sie beispielsweise auf die Schaltfläche "Popup öffnen" klicken, wird eine OpenPopup-Nachricht generiert. Die OpenPopup-Nachricht in der Aktualisierungsfunktion wird auf verschiedene Arten behandelt. Für einen anonymen Benutzer wird ein Autorisierungsformular generiert und für einen autorisierten Benutzer ein Formular, in dem Sie eine Frage hinterlassen können.
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of
Offensichtlich kann dieser Ansatz Probleme mit dem Wachstum von Anwendungsfunktionen haben:
- Es gibt keine Gruppierung von Daten im Modell und in den Nachrichten. Alles liegt in einer Ebene. Somit gibt es keine Komponentengrenzen, eine Änderung der Logik eines Teils wirkt sich höchstwahrscheinlich auf den Rest aus.
- Die Wiederverwendung von Code ist nach dem Prinzip des Kopierens und Einfügens mit allen daraus resultierenden Konsequenzen möglich.
Praktische Komposition
Der Quellcode ist eine bequeme Implementierung . Im Rahmen dieser Implementierung werden wir versuchen, das Projekt in unabhängige Komponenten aufzuteilen. Zulässige Abhängigkeiten zwischen Komponenten.
Projektstruktur:
- Im Ordner Typ werden benutzerdefinierte Typen deklariert.
- Benutzerdefinierte Komponenten werden im Komponentenordner deklariert.
- Einstiegspunkt für das Main.elm-Dateiprojekt;
- Die Dateien login.json und question.json werden als Testdaten für die Antwort des Servers auf die Autorisierung bzw. das Speichern von Informationen zu der Frage verwendet.
Benutzerdefinierte Komponenten
Jede auf der Spracharchitektur basierende Komponente sollte Folgendes enthalten:
- Modell
- Nachrichten (Nachricht)
- Ergebnis der Ausführung (Return);
- Initialisierungsfunktion (init);
- Mutationsfunktion (Update);
- Ansichtsfunktion.
Jede Komponente kann bei Bedarf ein Abonnement enthalten.

Abb. 1. Komponentenaktivitätsdiagramm
Initialisierung
Jede Komponente muss ausgelöst werden, d.h. muss empfangen werden:
- Modell
- Der Befehl oder die Liste der Befehle, die den Status der Komponente initialisieren sollen
- Ausführungsergebnis. Das Ergebnis der Ausführung zum Zeitpunkt der Initialisierung kann erforderlich sein, um die Autorisierung des Benutzers zu überprüfen, wie in den Beispielen zu diesem Artikel.
Die Liste der Argumente der Initialisierungsfunktion (init) hängt von der Logik der Komponente ab und kann beliebig sein. Es kann mehrere Initialisierungsfunktionen geben. Angenommen, für eine Autorisierungskomponente können zwei Initialisierungsoptionen bereitgestellt werden: mit einem Sitzungstoken und mit Benutzerdaten.
Code, der die Komponente verwendet, muss nach der Initialisierung mithilfe der Funktion Cmd.map Befehle an elm runtime übergeben .
Mutation
Die Funktion zum Aktualisieren der Komponente muss für jede Komponentennachricht aufgerufen werden. Als Ergebnis der Ausführung gibt die Funktion ein Tripel zurück:
- neues Modell oder neuer Zustand (Modell);
- Befehl oder Befehlsliste für die Elm-Laufzeit (Cmd Msg). Die Befehle können Befehle zum Ausführen von HTTP-Anforderungen, zur Interaktion mit Ports und mehr sein.
- Ergebnis der Ausführung (Vielleicht zurück). Vielleicht hat Typ zwei Zustände Nichts und Nur ein. In unserem Fall gibt es nichts - es gibt kein Ergebnis, nur ein - es gibt ein Ergebnis. Für die Autorisierung kann das Ergebnis beispielsweise Just (Authenticated UserData) sein - der Benutzer ist mit UserData-Daten angemeldet.
Nach der Mutation sollte der Code, der die Komponente verwendet, das Komponentenmodell aktualisieren und Befehle mithilfe der Funktion Cmd.map an Elm Runtime übergeben .
Erforderliche Argumente für die Aktualisierungsfunktion gemäß der Elm-Anwendungsarchitektur:
- Nachricht (Nachricht)
- Modell (Modell).
Bei Bedarf kann die Liste der Argumente ergänzt werden.
Einreichung
Die Ansichtsfunktion wird aufgerufen, wenn die Komponentenansicht in die allgemeine Anwendungsansicht eingefügt werden muss.
Das erforderliche Argument für die Ansichtsfunktion muss das Komponentenmodell sein. Bei Bedarf kann die Liste der Argumente ergänzt werden.
Das Ergebnis der Ausführung der Ansichtsfunktion muss an die Funktion Html.map übergeben werden.
Anwendungsintegration
Das Beispiel beschreibt zwei Komponenten: Auth und Question . Komponenten der oben beschriebenen Prinzipien. Überlegen Sie, wie sie in die Anwendung integriert werden können .
Lassen Sie uns zunächst festlegen, wie unsere Anwendung funktionieren soll. Wenn Sie darauf klicken, wird auf dem Bildschirm eine Schaltfläche angezeigt:
- Für einen nicht autorisierten Benutzer wird nach der Autorisierung ein Autorisierungsformular angezeigt - ein Formular zum Posten von Fragen.
- Für einen autorisierten Benutzer wird ein Formular zur Platzierung von Fragen angezeigt.
Um die Anwendung zu beschreiben, die Sie benötigen:
- Modell
- Nachrichten (Nachricht)
- Anwendungsstartpunkt (Haupt);
- Initialisierungsfunktion (init);
- Mutationsfunktion;
- Präsentationsfunktion;
- Abonnementfunktion.
Modell
type alias Model = { user: User , ui: Maybe Ui } type Ui = AuthUi Component.Auth.Model | QuestionUi Component.Question.Model
Das Modell enthält Informationen über den Benutzer (Benutzer) und den Typ der aktuellen Schnittstelle (UI). Die Schnittstelle kann sich entweder im Standardzustand (Nothing) oder in einer der Komponenten Just a befinden.
Zur Beschreibung der Komponenten verwenden wir den Ui-Typ, der jedes Komponentenmodell mit einer bestimmten Variante aus einer Reihe von Typen verknüpft (markiert). Ein AuthUi-Tag verknüpft beispielsweise ein Berechtigungsmodell (Component.Auth.Model) mit einem Anwendungsmodell.
Nachrichten
type Msg = OpenPopup | AuthMsg Component.Auth.Msg | QuestionMsg Component.Question.Msg
In Nachrichten müssen Sie alle Komponentennachrichten kennzeichnen und in die Anwendungsnachrichten aufnehmen. Die Tags AuthMsg und QuestionMsg verknüpfen die Nachrichten der Autorisierungskomponente bzw. die Frage des Benutzers.
Eine OpenPopup-Nachricht wird benötigt, um eine Anforderung zum Öffnen einer Schnittstelle zu verarbeiten.
Hauptfunktion
main : Program Never Model Msg main = Html.program { init = init , update = update , subscriptions = subscriptions , view = view }
Der Anwendungseinstiegspunkt wird normalerweise für eine Elm-Anwendung beschrieben.
Initialisierungsfunktion
init : ( Model, Cmd Msg ) init = ( initModel, Cmd.none ) initModel : Model initModel = { user = Anonymous , ui = Nothing }
Die Initialisierungsfunktion erstellt ein Startmodell und erfordert keine Ausführung von Befehlen.
Mutationsfunktion
Funktionsquellcode update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.ui) of (OpenPopup, Nothing) -> case Component.Auth.init model.user of (authModel, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (authModel, commands, _) -> ( { model | ui = Just <| AuthUi authModel }, Cmd.map AuthMsg commands ) (AuthMsg authMsg, Just (AuthUi authModel)) -> case Component.Auth.update authMsg authModel of (_, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (newAuthModel, commands, _) -> ( { model | ui = Just <| AuthUi newAuthModel }, Cmd.map AuthMsg commands ) (QuestionMsg questionMsg, Just (QuestionUi questionModel)) -> case Component.Question.update questionMsg questionModel of (_, commands, Just (Component.Question.Saved record)) -> ( { model | ui = Nothing }, Cmd.map QuestionMsg commands ) (newQuestionModel, commands, _) -> ( { model | ui = Just <| QuestionUi newQuestionModel }, Cmd.map QuestionMsg commands ) _ -> ( model, Cmd.none )
Weil Wenn das Modell und die Nachrichten mit der Anwendung verbunden sind, werden einige Nachrichten (Msg) und die Art der Schnittstelle (model.ui: Ui) verarbeitet.
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.ui) of
Arbeitslogik
Wenn eine OpenPopup-Nachricht empfangen wird und die Standardschnittstelle im Modell angegeben ist (model.ui = Nothing), initialisieren wir die Auth-Komponente. Wenn die Auth-Komponente meldet, dass der Benutzer autorisiert ist, initialisieren wir die Question-Komponente und speichern sie im Anwendungsmodell. Andernfalls speichern wir das Komponentenmodell im Anwendungsmodell.
(OpenPopup, Nothing) -> case Component.Auth.init model.user of (authModel, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (authModel, commands, _) -> ( { model | ui = Just <| AuthUi authModel }, Cmd.map AuthMsg commands )
Wenn eine Nachricht mit dem AuthMsg ein Tag empfangen wird und die Autorisierungsschnittstelle im Modell angegeben ist (model.ui = Just (AuthUi authModel)), übergeben wir die Komponentennachricht und das Komponentenmodell an die Auth.update-Funktion. Als Ergebnis erhalten wir ein neues Komponentenmodell, neue Befehle und ein neues Ergebnis.
Wenn der Benutzer autorisiert ist, initialisieren wir die Fragekomponente, andernfalls aktualisieren wir die Schnittstellendaten im Anwendungsmodell.
(AuthMsg authMsg, Just (AuthUi authModel)) -> case Component.Auth.update authMsg authModel of (_, commands, Just (Component.Auth.Authenticated userData)) -> let (questionModel, questionCommands, _) = Component.Question.init userData in ( { model | ui = Just <| QuestionUi questionModel, user = User userData }, Cmd.batch [Cmd.map AuthMsg commands, Cmd.map QuestionMsg questionCommands] ) (newAuthModel, commands, _) -> ( { model | ui = Just <| AuthUi newAuthModel }, Cmd.map AuthMsg commands )
Ähnlich wie bei der Auth-Komponente werden Nachrichten für die Question-Komponente verarbeitet. Wenn die Frage erfolgreich gestellt wurde, ändert sich die Schnittstelle auf die Standardeinstellung (model.ui = Nothing).
(QuestionMsg questionMsg, Just (QuestionUi questionModel)) -> case Component.Question.update questionMsg questionModel of (_, commands, Just (Component.Question.Saved record)) -> ( { model | ui = Nothing }, Cmd.map QuestionMsg commands ) (newQuestionModel, commands, _) -> ( { model | ui = Just <| QuestionUi newQuestionModel }, Cmd.map QuestionMsg commands )
Alle anderen Fälle werden ignoriert.
_ -> ( model, Cmd.none )
Ansichtsfunktion
view : Model -> Html Msg view model = case model.ui of Nothing -> div [] [ div [] [ button [ Events.onClick OpenPopup ] [ text "Open popup" ] ] ] Just (AuthUi authModel) -> Component.Auth.view authModel |> Html.map AuthMsg Just (QuestionUi questionModel) -> Component.Question.view questionModel |> Html.map QuestionMsg
Abhängig vom Typ der Schnittstelle (model.ui) generiert die Präsentationsfunktion entweder eine Standardschnittstelle oder ruft die Komponentenpräsentationsfunktion auf und ordnet den Komponentennachrichtentyp dem Anwendungsnachrichtentyp (Html.map) zu.
Abonnementfunktion
subscriptions : Model -> Sub Msg subscriptions model = Sub.none
Kein Abonnement.
Weiter
Dieses Beispiel ist zwar etwas praktischer, aber recht naiv. Was fehlt:
- Blockieren der Interaktion mit der Anwendung während des Downloadvorgangs;
- Datenvalidierung. Benötigt ein separates Gespräch;
- wirklich Popup-Fenster mit der Fähigkeit zu schließen.