Elm. Confortável e estranho. Composição:

Continuamos a falar sobre o olmo 0,18 .


Elm. Confortável e desajeitado
Elm. Confortável e estranho. Json.Encoder e Json.Decoder
Elm. Confortável e estranho. Http, Tarefa


Neste artigo, consideraremos os problemas da arquitetura de aplicativos Elm e as possíveis opções para implementar a abordagem de desenvolvimento de componentes.


Como tarefa, considere a implementação de uma janela suspensa que permita que um usuário registrado adicione uma pergunta. No caso de um usuário anônimo, ele sugere primeiro fazer login ou registrar-se.


Também assumimos que, posteriormente, pode ser necessário implementar a recepção de outros tipos de conteúdo do usuário, mas a lógica de trabalhar com usuários autorizados e anônimos permanecerá a mesma.


Composição embaraçosa


O código fonte de uma implementação ingênua . Como parte dessa implementação, armazenaremos tudo em um modelo.


Todos os dados necessários para a autorização e a pesquisa do usuário estão no mesmo nível no modelo. A mesma situação com mensagens (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 

O tipo de interface é descrito como o tipo de união Ui, que é usado com o tipo Maybe.


 type Ui = LoginUi -- Popup shown with authentication form | QuestionUi -- Popup shown with textarea to leave user question 

Portanto, ui = Nothing descreve a ausência de uma janela pop-up, e o pop-up Just é aberto com uma interface específica.

Na função de atualização, o par é correspondido, a mensagem e os dados do usuário. Ações diferentes são executadas dependendo desse par.


 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of 

Por exemplo, quando você clica no botão "Abrir pop-up", uma mensagem do OpenPopup é gerada. A mensagem do OpenPopup na função de atualização é tratada de várias maneiras. Para um usuário anônimo, um formulário de autorização é gerado e, para um usuário autorizado, um formulário no qual você pode deixar uma pergunta.


 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.user) of -- Anonymous user message handling section (OpenPopup, Anonymous) -> ( { model | ui = Just LoginUi, message = "" }, Cmd.none) -- Authenticated user message handling section (OpenPopup, User userName) -> ( { model | ui = Just QuestionUi, message = "" }, Cmd.none) 

Obviamente, essa abordagem pode ter problemas com o crescimento das funções do aplicativo:


  1. sem agrupamento de dados no modelo e nas mensagens. Tudo está em um plano. Portanto, não há limites de componentes: uma mudança na lógica de uma parte provavelmente afetará o restante;
  2. a reutilização de código é possível com base no princípio de copiar e colar com todas as conseqüências resultantes.

Composição conveniente


O código fonte é uma implementação conveniente . No quadro desta implementação, tentaremos dividir o projeto em componentes independentes. Dependências permitidas entre componentes.


Estrutura do projeto:


  1. na pasta Tipo, os tipos personalizados são declarados;
  2. componentes personalizados são declarados na pasta Component;
  3. Ponto de entrada do projeto de arquivo Main.elm;
  4. Os arquivos login.json e questions.json são usados ​​como dados de teste para a resposta do servidor à autorização e para salvar informações sobre a pergunta, respectivamente.

Componentes personalizados


Cada componente, com base na arquitetura da linguagem, deve conter:


  1. Modelo
  2. Mensagens (Msg)
  3. resultado da execução (Return);
  4. função de inicialização (init);
  5. função de mutação (atualização);
  6. função de visualização.

Cada componente pode conter uma assinatura, se necessário.


imagem
Fig. 1. Diagrama de atividade do componente


Inicialização


Cada componente deve ser acionado, ou seja, deve ser recebido:


  1. modelo
  2. O comando ou lista de comandos que devem inicializar o estado do componente
  3. resultado de execução. O resultado da execução no momento da inicialização pode ser necessário para validar a autorização do usuário, como nos exemplos deste artigo.

A lista de argumentos da função de inicialização (init) depende da lógica do componente e pode ser arbitrária. Pode haver várias funções de inicialização. Suponha que, para um componente de autorização, duas opções de inicialização possam ser fornecidas: com um token de sessão e com dados do usuário.


O código que usa o componente, após a inicialização, deve passar comandos para elm runtime usando a função Cmd.map .


Mutação


A função de componente de atualização deve ser chamada para cada mensagem de componente. Como resultado da execução, a função retorna um triplo:


  1. novo modelo ou novo estado (modelo);
  2. comando ou lista de comandos para Elm runtime (Cmd Msg). Os comandos podem ser comandos para executar solicitações HTTP, interagir com portas e muito mais;
  3. resultado da execução (talvez retorno). Talvez o tipo tenha dois estados Nothing e Just a. No nosso caso, Nada - não há resultado, Apenas a - há um resultado. Por exemplo, para autorização, o resultado pode ser Just (UserData autenticado) - o usuário está logado com dados do UserData.

Após a mutação, o código usando o componente deve atualizar o modelo do componente e passar comandos para o tempo de execução Elm usando a função Cmd.map .


Argumentos necessários para a função de atualização, de acordo com a arquitetura do aplicativo Elm:


  1. Mensagem (Msg)
  2. modelo (modelo).

Se necessário, a lista de argumentos pode ser complementada.


Submissão


A função de visualização é chamada quando é necessário inserir a visualização do componente na visualização geral do aplicativo.


O argumento necessário para a função view deve ser o modelo do componente. Se necessário, a lista de argumentos pode ser complementada.


O resultado da execução da função de exibição deve ser passado para a função Html.map .


Integração de aplicativos


O exemplo descreve dois componentes: Auth e Question . Componentes dos princípios descritos acima. Considere como eles podem ser integrados ao aplicativo .


Primeiro, vamos determinar como nosso aplicativo deve funcionar. Há um botão na tela, quando você clica nele:


  1. para um usuário não autorizado, um formulário de autorização é exibido, após a autorização - um formulário de postagem de pergunta;
  2. Para um usuário autorizado, um formulário de posicionamento de perguntas é exibido.

Para descrever o aplicativo, você precisa:


  1. Modelo
  2. Mensagens (Msg)
  3. ponto de início do aplicativo (principal);
  4. função de inicialização (init);
  5. função de mutação;
  6. função de apresentação;
  7. função de assinatura.

Modelo


 type alias Model = { user: User , ui: Maybe Ui } type Ui = AuthUi Component.Auth.Model | QuestionUi Component.Question.Model 

O modelo contém informações sobre o usuário (usuário) e o tipo da interface atual (ui). A interface pode estar no estado padrão (Nothing) ou em um dos componentes Just a.


Para descrever os componentes, usamos o tipo Ui, que associa (tags) cada modelo de componente a uma variante específica de um conjunto de tipos. Por exemplo, uma marca AuthUi associa um modelo de autorização (Component.Auth.Model) a um modelo de aplicativo.


Mensagens


 type Msg = OpenPopup | AuthMsg Component.Auth.Msg | QuestionMsg Component.Question.Msg 

Nas mensagens, você deve marcar todas as mensagens do componente e incluí-las nas mensagens do aplicativo. As tags AuthMsg e QuestionMsg vinculam as mensagens do componente de autorização e a pergunta do usuário, respectivamente.


Uma mensagem do OpenPopup é necessária para processar uma solicitação para abrir uma interface.


Função principal


 main : Program Never Model Msg main = Html.program { init = init , update = update , subscriptions = subscriptions , view = view } 

O ponto de entrada do aplicativo é descrito normalmente para um aplicativo Elm.


Função de inicialização


 init : ( Model, Cmd Msg ) init = ( initModel, Cmd.none ) initModel : Model initModel = { user = Anonymous , ui = Nothing } 

A função de inicialização cria um modelo inicial e não requer a execução de comandos.


Função de mutação


Código fonte da função
 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 ) 

Porque Se o modelo e as mensagens do aplicativo estiverem conectados, processaremos algumas mensagens (Msg) e o tipo de interface (model.ui: Ui).


 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case (msg, model.ui) of 

Lógica de trabalho


Se uma mensagem do OpenPopup for recebida e a interface padrão for especificada no modelo (model.ui = Nothing), inicializaremos o componente Auth. Se o componente Auth informar que o usuário está autorizado, inicializamos o componente Question e o salvamos no modelo de aplicativo. Caso contrário, salvamos o modelo de componente no modelo de aplicativo.


 (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 ) 

Se uma mensagem com a tag AuthMsg for recebida e a interface de autorização for especificada no modelo (model.ui = Just (AuthUi authModel)), passamos a mensagem do componente e o modelo do componente para a função Auth.update. Como resultado, obtemos um novo modelo de componente, comandos e resultado.


Se o usuário estiver autorizado, inicializamos o componente Pergunta, caso contrário, atualizamos os dados da interface no modelo de aplicativo.


 (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 ) 

De maneira semelhante ao componente Auth, as mensagens para o componente Question são processadas. Se a pergunta for publicada com êxito, a interface mudará para o padrão (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 ) 

Todos os outros casos são ignorados.


  _ -> ( model, Cmd.none ) 

Função de visualização


 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 

Dependendo do tipo de interface (model.ui), a função de apresentação gera uma interface padrão ou chama a função de apresentação do componente e mapeia o tipo de mensagem do componente para o tipo de mensagem do aplicativo (Html.map).


Função de assinatura


 subscriptions : Model -> Sub Msg subscriptions model = Sub.none 

Sem assinatura.


Seguinte


Este exemplo, embora um pouco mais conveniente, mas bastante ingênuo. O que está faltando:


  1. bloquear a interação com o aplicativo durante o processo de download;
  2. validação de dados. Requer uma conversa em separado;
  3. janela realmente pop-up com a capacidade de fechar.

Source: https://habr.com/ru/post/pt424341/


All Articles