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
Obviamente, essa abordagem pode ter problemas com o crescimento das funções do aplicativo:
- 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;
- 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:
- na pasta Tipo, os tipos personalizados são declarados;
- componentes personalizados são declarados na pasta Component;
- Ponto de entrada do projeto de arquivo Main.elm;
- 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:
- Modelo
- Mensagens (Msg)
- resultado da execução (Return);
- função de inicialização (init);
- função de mutação (atualização);
- função de visualização.
Cada componente pode conter uma assinatura, se necessário.

Fig. 1. Diagrama de atividade do componente
Inicialização
Cada componente deve ser acionado, ou seja, deve ser recebido:
- modelo
- O comando ou lista de comandos que devem inicializar o estado do componente
- 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:
- novo modelo ou novo estado (modelo);
- 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;
- 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:
- Mensagem (Msg)
- 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:
- 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;
- Para um usuário autorizado, um formulário de posicionamento de perguntas é exibido.
Para descrever o aplicativo, você precisa:
- Modelo
- Mensagens (Msg)
- ponto de início do aplicativo (principal);
- função de inicialização (init);
- função de mutação;
- função de apresentação;
- 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:
- bloquear a interação com o aplicativo durante o processo de download;
- validação de dados. Requer uma conversa em separado;
- janela realmente pop-up com a capacidade de fechar.