Continuamos a falar sobre o olmo 0,18 .
Elm. Confortável e desajeitado
Elm. Confortável e estranho. Composição:
Elm. Confortável e estranho. Json.Encoder e Json.Decoder
Neste artigo, consideramos problemas de interação com o lado do servidor.
Execução de consulta
Exemplos de consultas simples podem ser encontrados na descrição do pacote Http .
O tipo de solicitação é Http.Request a .
O tipo de resultado da consulta é Result Http.Error a.
Ambos os tipos são parametrizados pelo tipo de usuário, cujo decodificador deve ser especificado ao gerar a solicitação.
Você pode executar a solicitação usando as funções:
- Http.send;
- Http.toTask.
Http.send permite executar a solicitação e, após a conclusão, passa a mensagem para a função de atualização especificada no primeiro argumento. A mensagem carrega dados sobre o resultado da solicitação.
Http.toTask permite criar uma tarefa a partir de uma solicitação que você pode executar. Usar a função Http.toTask, na minha opinião, é o mais conveniente, pois as instâncias de tarefas podem ser combinadas usando várias funções , por exemplo, Task.map2 .
Vejamos um exemplo. Por exemplo, para salvar dados do usuário, duas consultas dependentes consecutivas devem ser executadas. Deixe que ele crie uma postagem do usuário e salve as fotos para ele (é usada alguma CDN).
Primeiro, considere a implementação do caso Http.Send. Para isso, precisamos de duas funções:
save : UserData -> Request Http.Error UserData save userData = Http.post “/some/url” (Http.jsonBody (encodeUserData userData)) decodeUserData saveImages : Int -> Images -> Request Http.Error CDNData saveImages id images = Http.post (“/some/cdn/for/” ++ (toString id)) (imagesBody images) decodedCDNData
Os tipos UserData e CDNData não serão descritos; por exemplo, eles não são importantes. A função encodeUserData é um codificador. O saveImages aceita um identificador de dados do usuário, usado para formar o endereço, e uma lista de fotos. A função imagesBody forma um corpo de solicitação do tipo multipart / data-form . As funções decodeUserData e decodedCDNData decodificam a resposta do servidor para dados do usuário e o resultado da solicitação CDN, respectivamente.
Em seguida, precisamos de duas mensagens, os resultados da consulta:
type Msg = DataSaved (Result Http.Error UserData) | ImagesSaved (Result Http.Error CDNData)
Suponha que, em algum ponto da implementação da função de atualização, haja uma seção de código que armazene dados. Por exemplo, pode ser assim:
update : Msg -> Model -> (Model, Cmd Msg) update msg model case Msg of ClickedSomething -> (model, Http.send DataSaved (save model.userData))
Nesse caso, uma consulta é criada e marcada com uma mensagem DataSaved. Além disso, esta mensagem é recebida:
update : Msg -> Model -> (Model, Cmd Msg) update msg model case Msg of DataSaved (Ok userData) -> ( {model | userData = userData}, Http.send ImagesSaved (saveImages userData.id model.images)) DataSaved (Err reason) -> (model, Cmd.None)
No caso de uma economia bem-sucedida, atualizamos os dados no modelo e chamamos a solicitação para salvar as fotos para onde transferimos o identificador de dados do usuário recebido. O processamento da mensagem ImagesSaved será semelhante ao DataSaved; será necessário lidar com casos bem-sucedidos e com falha.
Agora considere a opção de usar a função Http.toTask. Usando as funções descritas, definimos uma nova função:
saveAll : UserData -> Images -> Task Http.Error (UserData, CDNData) saveAll : userData images = save model.userData |> Http.toTask |> Task.andThen (\newUserData -> saveImages usersData.id images |> Http.toTask |> Task.map (\newImages -> (userData, newImages) } )
Agora, usando a capacidade de combinar tarefas, podemos obter todos os dados em uma mensagem:
type Msg = Saved (Result Http.Error (UserData, CDNData)) update : Msg -> Model -> (Model, Cmd Msg) update msg model case Msg of ClickedSomething -> (model, Task.attempt Saved (saveAll model.userData model.images)) DataSaved (Ok (userData, images)) -> ( {model | userData = userData, images = images}, Cmd.none) DataSaved (Err reason) -> (model, Cmd.None)
Para executar solicitações, usamos a função Task.attempt , que permite concluir a tarefa. Não deve ser confundido com a função Task.perform . Task.perform - permite concluir tarefas que não podem falhar . Task.attempt - Executa tarefas que podem falhar .
Essa abordagem é mais compacta em termos de número de mensagens, complexidade da função de atualização e permite manter a lógica mais local.
Nos meus projetos, aplicativos e componentes, costumo criar o módulo Commands.elm, no qual descrevo as funções de interação com a parte do servidor com o tipo ... -> Task Http.Error a.
Status de execução da solicitação
No processo de execução de consultas, a interface geralmente precisa ser bloqueada no todo ou em parte, além de relatar erros na execução de consultas, se houver. Em geral, o status de uma solicitação pode ser descrito como:
- a solicitação não foi concluída;
- a solicitação está em andamento;
- A solicitação foi concluída com sucesso.
- solicitação falhou.
Para essa descrição, há um pacote RemoteData . No começo, eu o usei ativamente, mas com o tempo, a presença de um tipo adicional de WebData se tornou redundante e o trabalho com ele foi entediante. Em vez deste pacote, as seguintes regras apareceram:
- declare todos os dados do servidor como Talvez. Nesse caso, Nothing, indica a ausência de dados;
- declare um atributo de carregamento do tipo Int no modelo de aplicativo ou componente. O parâmetro armazena o número de solicitações executadas. O único inconveniente dessa abordagem é a necessidade de aumentar e diminuir o atributo no início da solicitação e na conclusão, respectivamente;
- declare um atributo de erro do tipo List String no modelo de aplicativo ou componente. Este atributo é usado para armazenar dados de erro.
O esquema descrito não é muito melhor do que a opção com o pacote RemoteData, como mostra a prática. Se alguém tiver outras opções, compartilhe nos comentários.
O status de execução da solicitação deve incluir o progresso do download do pacote Http.Progress .
Sequência de tarefas
Considere as opções para sequências de tarefas que geralmente são encontradas no desenvolvimento:
- tarefas dependentes sequenciais;
- tarefas independentes consistentes;
- tarefas independentes paralelas.
Tarefas dependentes sucessivas já foram consideradas acima; darei uma descrição geral e abordagens para implementação nesta seção.
A sequência de tarefas é interrompida na primeira falha e um erro é retornado. Se for bem-sucedido, alguma combinação de resultados será retornada:
someTaskA |> Task.andThen (\resultA -> someTaskB |> Task.map (\resultB -> (resultA, resultB) ) )
Este código cria uma tarefa do tipo Erro de tarefa (a, b), que pode ser executado posteriormente.
A função Task.andThen permite transferir uma nova tarefa a ser executada em caso de conclusão bem-sucedida da anterior. A função Task.map permite converter resultados de execução de melão em caso de sucesso.
Existem opções em que a conclusão bem-sucedida da tarefa não será suficiente e é necessário verificar a consistência dos dados. Suponha que os IDs de usuário correspondam:
someTaskA |> Task.andThen (\resultA -> someTaskB |> Task.andThen (\resultB -> case resultA.userId == resultB.userId of True -> Task.succeed (resultA, resultB) False -> Task.fail “User is not the same” ) )
Vale ressaltar que, em vez da função Task.map, a função Task.andThen é usada e o sucesso da segunda tarefa é determinado independentemente, usando as funções Task.succeed e Task.fail .
Se uma das tarefas puder falhar e isso for aceitável, você precisará usar a função Task.onError para indicar o valor em caso de erro:
someTaskA |> Task.onError (\msg -> Task,succeed defaultValue) |> Task.andThen (\resultA -> someTaskB |> Task.map (\resultB -> (resultA, resultB) ) )
A chamada para a função Task.onError deve ser declarada imediatamente após a tarefa ser declarada.
Consultas independentes sucessivas podem ser executadas usando as funções Task.mapN. O que permite combinar vários resultados de tarefas em um. A primeira tarefa interrompida interrompe a execução de toda a cadeia, portanto, use a função Task.onError para os valores padrão. Verifique também a função Task.sequence , que permite executar uma série de tarefas semelhantes.
Tarefas paralelas na implementação do idioma atual não são descritas. Sua implementação é possível no nível do aplicativo ou componente através do processamento de eventos na função de atualização. Toda a lógica permanece nos ombros do desenvolvedor.