Jemand mochte Redux in React wegen seiner Implementierung auf JS nicht?
Ich mochte den ungeschickten Schalter in Reduzierstücken nicht, es gibt Sprachen mit bequemerem Mustervergleich und ich schreibe bessere Modellereignisse und Modelle. Zum Beispiel F #.
Dieser Artikel enthält eine Erläuterung des Messaging-Geräts in Elmish .
Ich werde ein Beispiel für eine Konsolenanwendung geben, die auf dieser Architektur geschrieben ist. In ihrem Beispiel wird klar, wie dieser Ansatz verwendet wird, und dann werden wir die Elmish-Architektur verstehen.
Ich habe eine einfache Konsolenanwendung zum Lesen von Gedichten geschrieben. In Seed gibt es mehrere Gedichte, eines für jeden Autor, die auf der Konsole angezeigt werden.
Das Fenster enthält nur 4 Textzeilen. Durch Drücken der Tasten "Auf" und "Ab" können Sie durch das Gedicht scrollen. Die digitalen Tasten ändern die Farbe des Textes. Mit den Tasten links und rechts können Sie sich durch den Verlauf der Aktionen bewegen. Beispielsweise hat der Benutzer Puschkins Gedicht gelesen und auf Yesenins Gedicht umgestellt die Farbe des Textes, und dann dachte, dass die Farbe nicht sehr gut war und Yesenin es nicht mochte, doppelklickte auf den linken Pfeil und kehrte zu der Stelle zurück, an der er Puschkin gelesen hatte.
Dieses Wunder sieht so aus:

Betrachten Sie die Implementierung.
Wenn Sie alle Optionen durchdenken, ist es klar, dass der Benutzer nur die Taste drücken kann. Durch Drücken dieser Taste können Sie bestimmen, was der Benutzer möchte, und er kann Folgendes wünschen:
- Autor wechseln
- Farbe ändern
- Scrollen (hoch / runter)
- Zur vorherigen / nächsten Version wechseln
Da der Benutzer in der Lage sein sollte, zur Version zurückzukehren, müssen Sie seine Aktionen korrigieren und sich an das Modell erinnern. Daher werden alle möglichen Meldungen wie folgt beschrieben:
type Msg = | ConsoleEvent of ConsoleKey | ChangeAuthor of Author | ChangeColor of ConsoleColor | ChangePosition of ChangePosition | ChangeVersion of ChangeVersion | RememberModel | WaitUserAction | Exit type ChangeVersion = | Back | Forward type ChangePosition = | Up | Down type Author = | Pushkin | Lermontov | Blok | Esenin type Poem = Poem of string
Im Modell müssen Sie Informationen über den Text in der Konsole, den Verlauf der Benutzeraktionen und die Anzahl der Aktionen speichern, die der Benutzer zurücksetzen wird, um zu wissen, welches Modell angezeigt werden muss.
type Model = { viewTextInfo: ViewTextInfo countVersionBack: int history: ViewTextInfo list } type ViewTextInfo = { text: string; formatText: string; countLines: int; positionY: int; color: ConsoleColor }
Elmish Architektur - Modell-Ansicht-Update, das Modell wurde bereits berücksichtigt, fahren wir fort mit der Ansicht:
let SnowAndUserActionView (model: Model) (dispatch: Msg -> unit) = let { formatText = ft; color = clr } = model.viewTextInfo; clearConsoleAndPrintTextWithColor ft clr let key = Console.ReadKey().Key; Msg.ConsoleEvent key |> dispatch let clearConsoleAndPrintTextWithColor (text: string) (color: ConsoleColor) = Console.Clear(); Console.WriteLine() Console.ForegroundColor <- color Console.WriteLine(text)
Dies ist eine der Ansichten. Sie wird basierend auf viewTextInfo gezeichnet, wartet auf die Reaktion des Benutzers und sendet diese Nachricht an die Aktualisierungsfunktion .
Später werden wir im Detail untersuchen, was genau passiert, wenn der Versand aufgerufen wird und welche Art von Funktion es ist.
Update :
let update (msg: Msg) (model: Model) = match msg with | ConsoleEvent key -> model, updateConsoleEvent key | ChangeAuthor author -> updateChangeAuthor model author | ChangeColor color -> updateChangeColor model color | ChangePosition position -> updateChangePosition model position | ChangeVersion version -> updateChangeVersion model version | RememberModel -> updateAddEvent model | WaitUserAction -> model, []
Abhängig von der Art der Nachricht wird ausgewählt, welche Funktion die Nachricht verarbeitet.
Dies ist eine Aktualisierung der Aktion des Benutzers, bei der die Schaltfläche der Nachricht zugeordnet wird. Der letzte Fall - gibt das WaitUserAction- Ereignis zurück - ignoriert den Klick und wartet auf weitere Benutzeraktionen.
let updateConsoleEvent (key: ConsoleKey) = let msg = match key with | ConsoleKey.D1 -> ChangeColor ConsoleColor.Red | ConsoleKey.D2 -> ChangeColor ConsoleColor.Green | ConsoleKey.D3 -> ChangeColor ConsoleColor.Blue | ConsoleKey.D4 -> ChangeColor ConsoleColor.Black | ConsoleKey.D5 -> ChangeColor ConsoleColor.Cyan | ConsoleKey.LeftArrow -> ChangeVersion Back | ConsoleKey.RightArrow -> ChangeVersion Forward | ConsoleKey.P -> ChangeAuthor Author.Pushkin | ConsoleKey.E -> ChangeAuthor Author.Esenin | ConsoleKey.B -> ChangeAuthor Author.Blok | ConsoleKey.L -> ChangeAuthor Author.Lermontov | ConsoleKey.UpArrow -> ChangePosition Up | ConsoleKey.DownArrow -> ChangePosition Down | ConsoleKey.X -> Exit | _ -> WaitUserAction msg |> Cmd.ofMsg
Wir ändern den Autor. Beachten Sie, dass countVersionBack sofort auf 0 zurückgesetzt wird. Wenn der Benutzer seine Story zurückgesetzt hat und dann die Farbe ändern möchte, wird diese Aktion als neu behandelt und dem Verlauf hinzugefügt.
let updateChangeAuthor (model: Model) (author: Author) = let (Poem updatedText) = seed.[author] let updatedFormatText = getlines updatedText 0 3 let updatedCountLines = (splitStr updatedText).Length let updatedViewTextInfo = {model.viewTextInfo with text = updatedText; formatText = updatedFormatText; countLines = updatedCountLines } { model with viewTextInfo = updatedViewTextInfo; countVersionBack = 0 }, Cmd.ofMsg RememberModel
Wir senden auch eine RememberModel- Nachricht, deren Handler den Verlauf aktualisiert und das aktuelle Modell hinzufügt.
let updateModelHistory model = { model with history = model.history @ [ model.viewTextInfo ] }, Cmd.ofMsg WaitUserAction
Der Rest der Updates ist hier zu sehen, sie ähneln den betrachteten.
Um die Leistung des Programms zu testen, werde ich Tests für verschiedene Szenarien durchführen:
TestsDie Ausführungsmethode übernimmt die Struktur, in der die Nachrichtenliste gespeichert ist, und gibt das Modell zurück, nachdem sie verarbeitet wurden.
[<Property(Verbose=true)>] let `` `` (authors: Author list) = let state = (createProgram (authors |> List.map ChangeAuthor) |> run) match (authors |> List.tryLast) with | Some s -> let (Poem text) = seed.[s] state.viewTextInfo.text = text | None -> true [<Property(Verbose=true)>] let `` `` changeColorMsg = let state = (createProgram (changeColorMsg|>List.map ChangeColor)|> run) match (changeColorMsg |> List.tryLast) with | Some s -> state.viewTextInfo.color = s | None -> true [<Property(Verbose=true,Arbitrary=[|typeof<ChangeColorAuthorPosition>|])>] let `` `` msgs = let tryLastSomeList list = list |> List.filter (Option.isSome) |> List.map (Option.get) |> List.tryLast let lastAuthor = msgs |> List.map (fun x -> match x with | ChangeAuthor a -> Some a | _ -> None) |> tryLastSomeList let lastColor = msgs |> List.map (fun x -> match x with | ChangeColor a -> Some a | _ -> None) |> tryLastSomeList let state = (createProgram msgs |> run) let colorTest = match lastColor with | Some s -> state.viewTextInfo.color = s | None -> true let authorTest = match lastAuthor with | Some s -> let (Poem t) = seed.[s]; state.viewTextInfo.text = t | None -> true authorTest && colorTest
Hierzu wird die FsCheck-Bibliothek verwendet, mit der Daten generiert werden können.
Betrachten wir nun den Kern des Programms, der Code in Elmish wurde für alle Gelegenheiten geschrieben, ich habe ihn vereinfacht (der ursprüngliche Code) :
type Dispatch<'msg> = 'msg -> unit type Sub<'msg> = Dispatch<'msg> -> unit type Cmd<'msg> = Sub<'msg> list type Program<'model, 'msg, 'view> = { init: unit ->'model * Cmd<'msg> update: 'msg -> 'model -> ('model * Cmd<'msg>) setState: 'model -> 'msg -> Dispatch<'msg> -> unit } let runWith<'arg, 'model, 'msg, 'view> (program: Program<'model, 'msg, 'view>) = let (initModel, initCmd) = program.init() //1 let mutable state = initModel //2 let mutable reentered = false //3 let buffer = RingBuffer 10 //4 let rec dispatch msg = let mutable nextMsg = Some msg; //5 if reentered //6 then buffer.Push msg //7 else while Option.isSome nextMsg do // 8 reentered <- true // 9 let (model, cmd) = program.update nextMsg.Value state // 9 program.setState model nextMsg.Value dispatch // 10 Cmd.exec dispatch cmd |> ignore //11 state <- model; // 12 nextMsg <- buffer.Pop() // 13 reentered <- false; // 14 Cmd.exec dispatch initCmd |> ignore // 15 state //16 let run program = runWith program
Der Dispath <'msg> -Typ ist genau der Versand, der in der Ansicht verwendet wird. Er nimmt die Nachricht und gibt die Einheit zurück
Sub <'msg> - Die Abonnentenfunktion akzeptiert die Versand- und Rückgabeeinheit. Wir erzeugen die Sub- Liste, wenn wir von Msg verwenden :
let ofMsg<'msg> (msg: 'msg): Cmd<'msg> = [ fun (dispatch: Dispatch<'msg>) -> dispatch msg ]
Nach dem Aufruf von msg , z. B. Cmd.ofMsg RememberModel am Ende der updateChangeAuthor- Methode, wird nach einer Weile der Abonnent aufgerufen und die Nachricht gelangt in die update- Methode
Cmd <'msg> - Sheet Sub <' msg>
Fahren wir mit dem Programmtyp fort . Dies ist ein generischer Typ. Er nimmt den Typ des Modells, der Nachricht und der Ansicht an . In der Konsolenanwendung muss nichts aus der Ansicht zurückgegeben werden , aber in der Elmish.React-Ansicht wird die F # -Struktur des DOM-Baums zurückgegeben.
Init- Feld - Diese Funktion wird zu Beginn von elmish aufgerufen und gibt das ursprüngliche Modell und die erste Nachricht zurück. In meinem Fall gebe ich Cmd.ofMsg RememberModel zurück
Update ist die Hauptfunktion von Update , mit der Sie bereits vertraut sind.
SetState - im Standard-Elmish akzeptiert es nur die Modell- und Versand- und Aufrufansicht, aber ich muss msg übergeben, um die Ansicht abhängig von der Nachricht zu ersetzen. Ich werde ihre Implementierung zeigen, nachdem wir über Messaging nachgedacht haben.
Die Funktion runWith empfängt die Konfiguration, ruft dann init auf , das Modell und die erste Nachricht werden zurückgegeben. In den Zeilen 2,3 werden zwei veränderbare Objekte deklariert, das erste - in dem der Status gespeichert wird, das zweite wird von der Versandfunktion benötigt.
In der 4. Zeile wird der Puffer deklariert - Sie können ihn als Warteschlange nehmen, der erste kam herein - der erste kam heraus (tatsächlich ist die Implementierung von RingBuffer sehr interessant, ich habe ihn aus der Bibliothek genommen, ich rate Ihnen, ihn auf Github zu lesen).
Als nächstes kommt die rekursive Versandfunktion selbst, die gleiche, die in der Ansicht aufgerufen wird. Beim ersten Aufruf umgehen wir die if- Zeile in Zeile 6 und gehen sofort in die Schleife. Setzen Sie reented auf true, damit nachfolgende rekursive Aufrufe nicht in diese Schleife zurückkehren, sondern hinzufügen neue Nachricht im Puffer .
In Zeile 9 führen wir die Aktualisierungsmethode aus , von der wir das geänderte Modell und eine neue Nachricht abrufen (zum ersten Mal ist dies eine RememberModel- Nachricht).
Zeile 10 zeichnet das Modell, die SetState- Methode sieht folgendermaßen aus:

Wie Sie sehen können, verursachen unterschiedliche Beiträge unterschiedliche Ansichten
Dies ist eine notwendige Maßnahme, um den Fluss nicht zu blockieren, da der Aufruf von Console.ReadLine den Programmfluss blockiert und Ereignisse wie RememberModel, ChangeColor (die innerhalb des Programms und nicht vom Benutzer ausgelöst werden) jedes Mal warten, wenn der Benutzer auf die Schaltfläche klickt, obwohl sie nur geändert werden müssen Farbe.
Zum ersten Mal wird die OnlyShowView- Funktion aufgerufen, mit der das Modell einfach gezeichnet wird.
Wenn die WaitUserAction- Nachricht anstelle von RememberModel an die Methode gesendet wird , wird die ShowAndUserActionView- Funktion aufgerufen , die das Modell zeichnet und den Fluss blockiert. Sie wartet darauf, dass die Schaltfläche gedrückt wird. Sobald die Schaltfläche gedrückt wird, wird die Versandmethode erneut aufgerufen und die Nachricht an den Puffer gesendet (weil reenvited = false ).
Als Nächstes müssen Sie alle Nachrichten verarbeiten, die von der Aktualisierungsmethode stammen . Andernfalls gehen sie verloren. Rekursive Aufrufe werden nur dann in die Schleife aufgenommen, wenn die erneute Eingabe falsch wird. Zeile 11 sieht kompliziert aus, ist aber nur ein Push aller Nachrichten im Puffer :
let exec<'msg> (dispatch: Dispatch<'msg>) (cmd: Cmd<'msg>) = cmd |> List.map (fun sub -> sub dispatch)
Für alle Teilnehmer, die von der Aktualisierungsmethode zurückgegeben werden, wird der Versand aufgerufen, wodurch diese Nachrichten zum Puffer hinzugefügt werden.
In Zeile 12 aktualisieren wir das Modell, erhalten eine neue Nachricht und kehren zu false zurück, wenn der Puffer nicht leer ist. Dies ist nicht erforderlich. Wenn jedoch keine Elemente mehr vorhanden sind und der Versand nur aus der Ansicht aufgerufen werden kann, ist dies sinnvoll. Auch in unserem Fall ist dies nicht sinnvoll, wenn alles synchron ist, da wir einen synchronen Versandaufruf in Zeile 10 erwarten. Wenn der Code jedoch asynchrone Aufrufe enthält, kann der Versand vom Rückruf aus aufgerufen werden und Sie müssen in der Lage sein, das Programm fortzusetzen.
Nun, das ist die gesamte Beschreibung der Versandfunktion. In Zeile 15 wird sie aufgerufen und der Status wird in Zeile 16 zurückgegeben.
In einer Konsolenanwendung erfolgt das Beenden, wenn der Puffer leer wird. In der Originalversion gibt runWith nichts zurück, aber ohne dies ist ein Testen nicht möglich.
Das Testprogramm ist anders. Die Funktion createProgram akzeptiert eine Liste von Nachrichten, die der Benutzer initiieren würde, und ersetzt in SetState den üblichen Klick:

Ein weiterer Unterschied zwischen meiner geänderten und der ursprünglichen Version besteht darin, dass zuerst die Aktualisierungsfunktion aufgerufen wird und dann nur die setState-Funktion . Im Gegensatz dazu musste ich in der Originalversion beim Rendern und dann bei der Nachrichtenverarbeitung dies tun, da der Aufruf von Console.ReadKey blockiert wurde (muss geändert werden) Ansicht )
Ich hoffe, ich habe es geschafft zu erklären, wie Elmish und ähnliche Systeme funktionieren. Viele Elmish-Funktionen sind über Bord geblieben. Wenn Sie sich für dieses Thema interessieren, empfehle ich Ihnen, sich die Website anzusehen.
Vielen Dank für Ihre Aufmerksamkeit!