¿A alguien no le gustó Redux en React debido a su implementación en JS?
No me gustó la torpe caja de interruptores en los reductores, hay idiomas con una coincidencia de patrones más conveniente y escribe mejores eventos y modelos de modelos. Por ejemplo, F #.
Este artículo es una explicación del dispositivo de mensajería en Elmish .
Daré un ejemplo de una aplicación de consola escrita en esta arquitectura, quedará claro en su ejemplo cómo usar este enfoque, y luego entenderemos la arquitectura de Elmish.
Escribí una aplicación de consola simple para leer poemas, en la semilla hay varios poemas, uno para cada autor, que se muestran en la consola.
La ventana contiene solo 4 líneas de texto, puede desplazarse por el poema presionando los botones "Arriba" y "Abajo", los botones numéricos cambian el color del texto y los botones izquierdo y derecho le permiten navegar por el historial de acciones, por ejemplo, el usuario lee el poema de Pushkin, cambia al poema de Yesenin, cambia el color del texto, y luego pensó que el color no era muy bueno y a Yesenin no le gustó, hizo doble clic en la flecha izquierda y regresó al lugar donde había terminado de leer Pushkin.
Este milagro se ve así:

Considere la implementación.
Si piensa en todas las opciones, está claro que todo lo que el usuario puede hacer es presionar el botón; al presionarlo, puede determinar qué quiere el usuario y puede desear:
- Cambiar autor
- Cambiar de color
- Desplazarse (arriba / abajo)
- Ir a la versión anterior / siguiente
Dado que el usuario debe poder volver a la versión anterior, debe corregir sus acciones y recordar el modelo , como resultado, todos los mensajes posibles se describen de la siguiente manera:
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
En el modelo, debe almacenar información sobre el texto que está ahora en la consola, el historial de acciones del usuario y la cantidad de acciones que el usuario revertirá para saber qué modelo debe mostrarse.
type Model = { viewTextInfo: ViewTextInfo countVersionBack: int history: ViewTextInfo list } type ViewTextInfo = { text: string; formatText: string; countLines: int; positionY: int; color: ConsoleColor }
Arquitectura de Elmish - model-view-update, el modelo ya ha sido considerado, pasemos a ver:
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)
Esta es una de las vistas, se basa en viewTextInfo , espera la reacción del usuario y envía este mensaje a la función de actualización .
Más adelante examinaremos en detalle qué sucede exactamente cuando se llama el despacho y qué tipo de función es.
Actualización :
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, []
Dependiendo del tipo de mensaje , selecciona qué función procesará el mensaje.
Esta es una actualización de la acción del usuario, asignando el botón al mensaje, el último caso, devuelve el evento WaitUserAction , ignora el clic y espera otras acciones del usuario.
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
Cambiamos el autor, tenga en cuenta que countVersionBack se restablece inmediatamente a 0, lo que significa que si el usuario retrocedió en su historia y luego quiso cambiar el color, esta acción se tratará como nueva y se agregará al historial .
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
También enviamos un mensaje RememberModel , cuyo controlador actualiza el historial y agrega el modelo actual.
let updateModelHistory model = { model with history = model.history @ [ model.viewTextInfo ] }, Cmd.ofMsg WaitUserAction
El resto de las actualizaciones se pueden ver aquí , son similares a las consideradas.
Para probar el rendimiento del programa, realizaré pruebas para varios escenarios:
PruebasEl método de ejecución toma la estructura en la que se almacena la lista de Mensajes y devuelve el modelo después de que se procesan.
[<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
Para esto, se utiliza la biblioteca FsCheck, que proporciona la capacidad de generar datos.
Ahora considere el núcleo del programa, el código en Elmish fue escrito para todas las ocasiones, lo simplifiqué (el código original) :
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
El tipo Dispath <'msg> es exactamente el envío que se usa en la vista , toma Mensaje y devuelve la unidad
Sub <'msg> : la función de suscriptor, acepta el envío y devuelve la unidad , engendramos la lista Sub cuando usamos ofMsg :
let ofMsg<'msg> (msg: 'msg): Cmd<'msg> = [ fun (dispatch: Dispatch<'msg>) -> dispatch msg ]
Después de llamar a ofMsg , como Cmd.ofMsg RememberModel al final del método updateChangeAuthor , después de un tiempo se llama al suscriptor y el mensaje entra en el método de actualización
Cmd <'msg> - Hoja Sub <' msg>
Pasemos al tipo de Programa , este es un tipo genérico, toma el tipo de modelo, mensaje y vista , en la aplicación de consola no hay necesidad de devolver algo de la vista , pero en la vista Elmish.React devuelve la estructura F # del árbol DOM.
Campo de inicio : llamado al comienzo de elmish, esta función devuelve el modelo inicial y el primer mensaje, en mi caso devuelvo Cmd.ofMsg RememberModel
La actualización es la función principal de la actualización , ya está familiarizado con ella.
SetState : en el Elmish estándar, solo acepta el modelo y la vista de despacho y llamadas, pero necesito pasar un mensaje para reemplazar la vista según el mensaje, mostraré su implementación después de considerar la mensajería.
La función runWith recibe la configuración, luego llama a init , se devuelve el modelo y el primer mensaje, en las líneas 2,3 se declaran dos objetos mutables, el primero: en qué estado se almacenará, el segundo es necesario para la función de despacho .
En la cuarta línea, se declara el búfer : puede tomarlo como una cola, el primero entró, salió el primero (en realidad, la implementación de RingBuffer es muy interesante, lo saqué de la biblioteca, le aconsejo que lo lea en github )
Luego viene la función de despacho recursivo en sí, la misma que se llama a la vista , cuando llamamos por primera vez, omitimos si está en la línea 6 e inmediatamente pasamos al bucle, establecemos reentrado en verdadero para que las llamadas recursivas posteriores no vuelvan a este bucle, pero agreguen Nuevo mensaje en el búfer .
En la línea 9, ejecutamos el método de actualización , del cual recogemos el modelo modificado y un nuevo mensaje (por primera vez, este es el mensaje RememberModel )
La línea 10 dibuja el modelo, el método SetState se ve así:

Como puede ver, diferentes publicaciones causan diferentes vistas
Esta es una medida necesaria para no bloquear el flujo, ya que llamar a Console.ReadLine bloquea el flujo del programa, y los eventos como RememberModel, ChangeColor (que se activan dentro del programa, no por el usuario) esperarán cada vez hasta que el usuario haga clic en el botón, aunque solo necesitan cambiar color
Por primera vez, se llamará a la función OnlyShowView , que simplemente dibujará el modelo.
Si el mensaje WaitUserAction llegara al método en lugar de RememberModel, se llamaría a la función ShowAndUserActionView , que dibujaría el modelo y bloquearía la secuencia, esperando que se presione el botón, tan pronto como se presionó el botón, el método de envío se volvería a llamar y el mensaje se enviaría al búfer (porque reenvited = false )
A continuación, debe procesar todos los mensajes que provienen del método de actualización , de lo contrario los perderemos, las llamadas recursivas solo entrarán en el ciclo si reentrado se vuelve falso. La línea 11 parece complicada, pero de hecho es solo una inserción de todos los mensajes en el búfer :
let exec<'msg> (dispatch: Dispatch<'msg>) (cmd: Cmd<'msg>) = cmd |> List.map (fun sub -> sub dispatch)
Para todos los suscriptores devueltos por el método de actualización , se llamará despacho , agregando así estos mensajes al búfer .
En la línea 12 actualizamos el modelo, recibimos un nuevo mensaje y volvemos a reentrar a falso cuando el búfer no está vacío, no es necesario, pero si no quedan elementos y el despacho solo se puede llamar desde la vista , esto tiene sentido. Nuevamente, en nuestro caso, cuando todo es síncrono, esto no tiene sentido, ya que esperamos una llamada de despacho síncrono en la línea 10, pero si hay llamadas asíncronas en el código, se puede llamar al despacho desde la devolución de llamada y debe poder continuar el programa.
Bueno, esa es la descripción completa de la función de envío , en la línea 15 se llama y el estado se devuelve en la línea 16.
En una aplicación de consola, la salida se produce cuando el búfer se vacía. En la versión original, runWith no devuelve nada, pero las pruebas son imposibles sin esto.
El programa para probar es diferente, la función createProgram acepta una lista de mensajes que el usuario iniciaría y en SetState reemplazan el clic habitual:

Otra diferencia entre mi versión modificada y la original es que primero se llama a la función de actualización, y luego solo la función setState , por el contrario, en la versión original, renderizando primero y luego procesando el mensaje, tuve que hacer esto debido a una llamada de bloqueo a la consola . ver )
Espero haber logrado explicar cómo funcionan Elmish y sistemas similares, se dejó atrás una gran cantidad de funcionalidades de Elmish. Si está interesado en este tema, le aconsejo que visite su sitio web .
Gracias por su atencion!