Alguém não gostou do Redux no React por causa de sua implementação no JS?
Não gostei da caixa de interrupção desajeitada nos redutores, existem idiomas com correspondência de padrão mais conveniente e digita melhores eventos e modelos. Por exemplo, F #.
Este artigo é uma explicação do dispositivo de mensagens no Elmish .
Vou dar um exemplo de um aplicativo de console escrito nessa arquitetura; usando seu exemplo, ficará claro como usar essa abordagem e, em seguida, entenderemos a arquitetura Elmish.
Eu escrevi um aplicativo de console simples para ler poemas, na semente existem vários poemas, um para cada autor, que são exibidos no console.
A janela contém apenas 4 linhas de texto, você pode rolar pelo poema pressionando os botões "Para cima" e "Para baixo", os botões numéricos mudam a cor do texto e os botões esquerdo e direito permitem navegar pelo histórico de ações, por exemplo, o usuário leu o poema de Pushkin, mudou para o poema de Yesenin, mudou a cor do texto e, depois, achou que a cor não era muito boa e Yesenin não gostou, deu um clique duplo na seta esquerda e voltou ao local onde havia terminado de ler Pushkin.
Esse milagre é assim:

Considere a implementação.
Se você pensar em todas as opções, fica claro que tudo o que o usuário pode fazer é pressionar o botão; pressionando-o, você pode determinar o que o usuário deseja e ele pode desejar:
- Alterar autor
- Mude a cor
- Rolar (para cima / para baixo)
- Ir para a versão anterior / próxima
Como o usuário deve poder retornar à versão de volta, é necessário corrigir as ações dele e lembrar do modelo ; como resultado, todas as mensagens possíveis são descritas a seguir:
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
No modelo, você precisa armazenar informações sobre o texto que está agora no console, o histórico de ações do usuário e o número de ações que o usuário reverterá para saber qual modelo precisa ser mostrado.
type Model = { viewTextInfo: ViewTextInfo countVersionBack: int history: ViewTextInfo list } type ViewTextInfo = { text: string; formatText: string; countLines: int; positionY: int; color: ConsoleColor }
Arquitetura Elmish - model-view-update, o modelo já foi considerado, vamos seguir para 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)
Essa é uma das visualizações, é desenhada com base em viewTextInfo , aguarda a reação do usuário e envia esta mensagem para a função de atualização .
Posteriormente, examinaremos em detalhes o que exatamente acontece quando o despacho é chamado e que tipo de função é.
Atualização :
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, []
Dependendo do tipo de mensagem , ele seleciona qual função processará a mensagem.
Esta é uma atualização da ação do usuário, mapeando o botão para a mensagem, o último caso - retorna o evento WaitUserAction - ignore o clique e aguarde novas ações do usuário.
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
Mudamos o autor, observe que countVersionBack é redefinido imediatamente para 0, o que significa que, se o usuário retroceder em sua história e depois desejar alterar a cor, essa ação será tratada como nova e adicionada ao histórico .
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
Também enviamos uma mensagem RememberModel , cujo manipulador atualiza o histórico , adicionando o modelo atual.
let updateModelHistory model = { model with history = model.history @ [ model.viewTextInfo ] }, Cmd.ofMsg WaitUserAction
O restante das atualizações pode ser visto aqui , elas são semelhantes às consideradas.
Para testar o desempenho do programa, darei testes para vários cenários:
TestesO método run utiliza a estrutura na qual a lista de Mensagens está armazenada e retorna o modelo após o processamento.
[<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 isso, é usada a biblioteca FsCheck, que fornece a capacidade de gerar dados.
Agora considere o núcleo do programa, o código em Elmish foi escrito para todas as ocasiões, simplifiquei (o 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
O tipo Dispath <'msg> é exatamente o despacho usado na visualização , ele recebe a mensagem e retorna a unidade
Sub <'msg> - a função de assinante, aceita a unidade de envio e devolução, criamos a lista Sub quando usamos ofMsg :
let ofMsg<'msg> (msg: 'msg): Cmd<'msg> = [ fun (dispatch: Dispatch<'msg>) -> dispatch msg ]
Após chamar ofMsg , como Cmd.ofMsg RememberModel no final do método updateChangeAuthor , depois de um tempo o assinante é chamado e a mensagem entra no método de atualização
Cmd <'msg> - Sub da folha <' msg>
Vamos passar para o tipo de programa , é um tipo genérico, leva o tipo de modelo, mensagem e exibição , no aplicativo de console não é necessário retornar algo da exibição , mas no Elmish.React view ele retorna a estrutura F # da árvore DOM.
Campo Init - chamado no início do elmish, essa função retorna o modelo inicial e a primeira mensagem; no meu caso, retorno Cmd.ofMsg RememberModel
Atualização é a principal função da atualização , você já está familiarizado com ela.
SetState - no Elmish padrão, ele aceita apenas o modelo, o despacho e a visualização de chamadas, mas preciso passar msg para substituir a visualização, dependendo da mensagem, mostrarei sua implementação depois de considerarmos as mensagens.
A função runWith recebe a configuração, depois chama init , o modelo e a primeira mensagem são retornados, nas linhas 2,3 são declarados dois objetos mutáveis, o primeiro - no qual o estado será armazenado, o segundo é necessário pela função de despacho .
Na quarta linha, o buffer é declarado - você pode pegá-lo como uma fila, o primeiro entrou - o primeiro saiu (na verdade, a implementação do RingBuffer é muito interessante, peguei na biblioteca, recomendo que você o leia no github )
A seguir, vem a própria função de despacho recursivo, a mesma chamada em vista , na primeira chamada, ignoramos a linha if na linha 6 e entramos imediatamente no loop, configurados como true para que as chamadas recursivas subsequentes não retornem a esse loop, mas adicione nova mensagem no buffer .
Na linha 9, executamos o método de atualização , do qual escolhemos o modelo alterado e uma nova mensagem (pela primeira vez, essa é a mensagem RememberModel )
A linha 10 desenha o modelo, o método SetState se parece com o seguinte:

Como você pode ver, postagens diferentes causam visualizações diferentes
Essa é uma medida necessária para não bloquear o fluxo, porque chamar Console.ReadLine bloqueia o fluxo do programa e eventos como RememberModel, ChangeColor (que são acionados dentro do programa, não pelo usuário) aguardam cada vez que o usuário clica no botão, embora eles precisem mudar. cor.
Pela primeira vez, a função OnlyShowView será chamada, o que simplesmente desenhará o modelo.
Se a mensagem WaitUserAction chegasse ao método em vez de RememberModel, a função ShowAndUserActionView seria chamada , que desenharia o modelo e bloquearia o fluxo, esperando que o botão fosse pressionado, assim que o botão fosse pressionado, o método de despacho seria chamado novamente e a mensagem seria enviada para o buffer (porque reenvited = false )
Em seguida, você precisa processar todas as mensagens que vieram do método de atualização ; caso contrário, as perderemos; as chamadas recursivas só entrarão em loop se o novo se tornar falso. A linha 11 parece complicada, mas na verdade é apenas um empurrão de todas as mensagens no buffer :
let exec<'msg> (dispatch: Dispatch<'msg>) (cmd: Cmd<'msg>) = cmd |> List.map (fun sub -> sub dispatch)
Para todos os assinantes retornados pelo método de atualização , o envio será chamado, adicionando essas mensagens ao buffer .
Na linha 12, atualizamos o modelo, obtemos uma nova mensagem e retornamos falsos quando o buffer não está vazio, não é necessário, mas se não houver elementos restantes e o envio puder ser chamado apenas de vista , isso faz sentido. Novamente, no nosso caso, quando tudo é síncrono, isso não faz sentido, pois esperamos uma chamada de despacho síncrona na linha 10, mas se houver chamadas assíncronas no código, o despacho pode ser chamado a partir do retorno de chamada e você precisa continuar o programa.
Bem, essa é toda a descrição da função de despacho , na linha 15 é chamada e o estado é retornado na linha 16.
Em um aplicativo de console, a saída ocorre quando o buffer fica vazio. Na versão original, runWith não retorna nada, mas o teste é impossível sem isso.
O programa para teste é diferente, a função createProgram aceita uma lista de mensagens que o usuário iniciaria e no SetState elas substituem o clique usual:

Outra diferença entre minha versão alterada e a original é que a função de atualização é chamada primeiro e, em seguida, apenas a função setState , pelo contrário, na versão original, processando primeiro e depois o processamento de mensagens, eu tive que fazer isso devido a uma chamada de bloqueio para o Console.ReadKey (preciso alterar ver )
Espero ter conseguido explicar como o Elmish e sistemas similares funcionam, muitas funcionalidades do Elmish permaneceram no mar; se você estiver interessado neste tópico, aconselho que você consulte o site deles.
Obrigado pela atenção!