2016-01-27 16 views
13

utilizzando il pacchetto di Elm html è possibile effettuare richieste HTTP:Come estrarre i risultati di richieste HTTP in Elm

https://api.github.com/users/nytimes/repos 

Queste sono tutte le New York Times pronti contro termine su GitHub. Fondamentalmente ci sono due elementi che avrei voluto dalla risposta Github, il nome id e

[ { "id": 5803599, "name": "backbone.stickit" , ... }, 
    { "id": 21172032, "name": "collectd-rabbitmq" , ... }, 
    { "id": 698445, "name": "document-viewer" , ... }, ... ] 

Il tipo di Elm per Http.get richiede un JSON Decoder oggetto

> Http.get 
<function> : Json.Decode.Decoder a -> String -> Task.Task Http.Error a 

non lo faccio sapere come aprire ancora le liste. Così ho inserito il decodificatore Json.Decode.string e almeno i tipi corrispondenti, ma non avevo idea di cosa fare con l'oggetto task.

> tsk = Http.get (Json.Decode.list Json.Decode.string) url 
{ tag = "AndThen", task = { tag = "Catch", task = { tag = "Async", asyncFunction = <function> }, callback = <function> }, callback = <function> } 
    : Task.Task Http.Error (List String) 

> Task.toResult tsk 
{ tag = "Catch", task = { tag = "AndThen", task = { tag = "AndThen", task = { tag = "Catch", task = { tag = "Async", asyncFunction = <function> }, callback = <function> }, callback = <function> }, callback = <function> }, callback = <function> } 
    : Task.Task a (Result.Result Http.Error (List String)) 

Voglio solo un oggetto Elm dei nomi di pronti contro termine così posso visualizzare in alcuni div elementi, ma non posso anche ottenere i dati fuori.


Qualcuno può lentamente camminare me con come scrivere il decoder e come ottenere i dati fuori con Elm?

risposta

24

Aggiornamento per Elm 0,17:

Ho aggiornato il senso completo di questa risposta per lavorare con Elm 0,17. È possibile see the full source code here. Funzionerà su http://elm-lang.org/try.

Un numero di modifiche di lingua e API sono state apportate in 0.17 che rendono obsolete alcune delle seguenti raccomandazioni. È possibile read about the 0.17 upgrade plan here.

Lascerò la risposta originale per 0.16 invariato sotto, ma è possibile compare the final gists to see a list of what has changed. Credo che la nuova versione 0.17 sia più pulita e più facile da capire.

risposta originale per Elm 0,16:

Sembra che si sta utilizzando l'Elm REPL. As noted here, non sarai in grado di eseguire attività in REPL. Otterremo di più su perché in un po '. Invece, creiamo un vero progetto Elm.

Suppongo che tu abbia scaricato lo standard Elm tools.

Per prima cosa è necessario creare una cartella di progetto e aprirla in un terminale.

Un modo comune per iniziare su un progetto Elm è utilizzare lo StartApp. Usiamolo come punto di partenza. Per prima cosa è necessario utilizzare lo strumento da riga di comando di Elm Package Manager per installare i pacchetti richiesti. Eseguire quanto segue in un terminale nel principale del progetto:

elm package install -y evancz/elm-html 
elm package install -y evancz/elm-effects 
elm package install -y evancz/elm-http 
elm package install -y evancz/start-app 

Ora, creare un file nella directory principale progetto denominato Main.elm. Ecco un codice StartApp di tipo boilerplate per iniziare.Non entrerò nello spiegare i dettagli qui poiché questa domanda riguarda specificamente Task. Puoi saperne di più passando attraverso lo Elm Architecture Tutorial. Per ora, copia questo in Main.elm.

import Html exposing (..) 
import Html.Events exposing (..) 
import Html.Attributes exposing (..) 
import Html.Attributes exposing (..) 
import Http 
import StartApp 
import Task exposing (Task) 
import Effects exposing (Effects, Never) 
import Json.Decode as Json exposing ((:=)) 

type Action 
    = NoOp 

type alias Model = 
    { message : String } 

app = StartApp.start 
    { init = init 
    , update = update 
    , view = view 
    , inputs = [ ] 
    } 

main = app.html 

port tasks : Signal (Task.Task Effects.Never()) 
port tasks = app.tasks 

init = 
    ({ message = "Hello, Elm!" }, Effects.none) 

update action model = 
    case action of 
    NoOp -> 
     (model, Effects.none) 

view : Signal.Address Action -> Model -> Html 
view address model = 
    div [] 
    [ div [] [ text model.message ] 
    ] 

Ora è possibile eseguire questo codice utilizzando elm-reactor. Vai al terminale nella cartella del progetto e immettere

elm reactor 

Questo farà eseguire un server web sulla porta 8000 per impostazione predefinita e si può tirare su http://localhost:8000 nel browser, quindi accedere alla Main.elm di vedere il "Ciao , Elm "esempio.

L'obiettivo finale qui è quello di creare un pulsante che, quando si fa clic, richiama l'elenco di repository di nytimes ed elenca gli ID ei nomi di ciascuno. Iniziamo a creare quel pulsante. Lo faremo utilizzando le funzioni di generazione html standard. Aggiornare la funzione view con qualcosa di simile:

view address model = 
    div [] 
    [ div [] [ text model.message ] 
    , button [] [ text "Click to load nytimes repositories" ] 
    ] 

Di per sé, il pulsante di scatto non fa nulla. Dobbiamo creare un'azione che venga poi gestita dalla funzione update. L'azione avviata dal pulsante consiste nel recuperare i dati dall'endpoint Github. Action diventa ora:

type Action 
    = NoOp 
    | FetchData 

e ora possiamo spegnere la gestione di questa azione nella funzione update in questo modo. Per ora, cambiamo il messaggio per indicare che il pulsante di scatto è stata gestita:

update action model = 
    case action of 
    NoOp -> 
     (model, Effects.none) 
    FetchData -> 
     ({ model | message = "Initiating data fetch!" }, Effects.none) 

Infine, dobbiamo causare tasto clic per attivare quella nuova azione. Questo viene fatto usando la funzione onClick, che genera un gestore di eventi click per quel pulsante. La linea di generazione del pulsante html ora appare così:

button [ onClick address FetchData ] [ text "Click to load nytimes repositories" ] 

Grande! Ora il messaggio dovrebbe essere aggiornato quando si fa clic su di esso. Passiamo alle attività.

Come già detto in precedenza, il REPL non supporta (ancora) l'invocazione di attività. Questo può sembrare controintuitivo se provieni da un linguaggio imperativo come Javascript, dove quando scrivi un codice che dice "vai a prendere i dati da questo url", crea immediatamente una richiesta HTTP. In un linguaggio puramente funzionale come Elm, fai le cose in modo un po 'diverso. Quando crei un'attività in Elm, stai solo indicando le tue intenzioni, creando una sorta di "pacchetto" che puoi trasferire al runtime per fare qualcosa che causa effetti collaterali; in questo caso, contatta il mondo esterno e estrai i dati da un URL.

Andiamo avanti e creare un'attività che recupera i dati dall'URL. Innanzitutto, avremo bisogno di un tipo all'interno di Elm per rappresentare la forma dei dati che ci interessano. Hai indicato di volere solo i campi id e name.

type alias RepoInfo = 
    { id : Int 
    , name : String 
    } 

Come nota sul tipo di costruzione all'interno Elm, fermiamoci per un minuto e parlare di come creiamo RepoInfo istanze. Poiché ci sono due campi, puoi costruire uno RepoInfo in due modi. Le due istruzioni seguenti sono equivalenti:

-- This creates a record using record syntax construction 
{ id = 123, name = "example" } 

-- This creates an equivalent record using RepoInfo as a constructor with two args 
RepoInfo 123 "example" 

Questo secondo era di costruire l'istanza diventerà più importante quando si parla di decodifica JSON.

Aggiungiamo anche un elenco di questi al modello. Dovremo cambiare anche la funzione init per iniziare con una lista vuota.

type alias Model = 
    { message : String 
    , repos : List RepoInfo 
    } 

init = 
    let 
    model = 
     { message = "Hello, Elm!" 
     , repos = [] 
     } 
    in 
    (model, Effects.none) 

Poiché i dati dall'URL torna in formato JSON, avremo bisogno di un decoder JSON per tradurre il grezzo JSON nel nostro type-safe class Elm. Crea il seguente decodificatore.

repoInfoDecoder : Json.Decoder RepoInfo 
repoInfoDecoder = 
    Json.object2 
    RepoInfo 
    ("id" := Json.int) 
    ("name" := Json.string) 

Scegliamolo. Un decodificatore è ciò che associa il JSON grezzo alla forma del tipo a cui stiamo mappando. In questo caso, il nostro tipo è un alias di record semplice con due campi. Ricorda che ho menzionato alcuni passaggi fa che possiamo creare un'istanza di RepoInfo usando RepoInfo come una funzione che richiede due parametri? Ecco perché stiamo usando Json.object2 per creare il decoder. Il primo argomento su object è una funzione che accetta due argomenti, ed è per questo che stiamo passando in RepoInfo. È equivalente a una funzione con arity due.

Gli argomenti rimanenti spiegano la forma del tipo. Poiché il nostro modello RepoInfo elenca id per primo e name in secondo luogo, questo è l'ordine in cui il decodificatore si aspetta che gli argomenti siano.

Avremo bisogno di un altro decodificatore per decodificare un elenco di istanze RepoInfo.

repoInfoListDecoder : Json.Decoder (List RepoInfo) 
repoInfoListDecoder = 
    Json.list repoInfoDecoder 

Ora che abbiamo un modello e decoder, possiamo creare una funzione che restituisce il compito per il recupero dei dati. Ricorda, questo non è in realtà il recupero di tutti i dati, è semplicemente la creazione di una funzione che possiamo trasferire al runtime più tardi.

fetchData : Task Http.Error (List RepoInfo) 
fetchData = 
    Http.get repoInfoListDecoder "https://api.github.com/users/nytimes/repos" 

Esistono diversi modi per gestire la varietà di errori che possono verificarsi. Scegliamo Task.toResult, che mappa il risultato della richiesta in un tipo Result. Ci renderà le cose più facili in un po ', ed è sufficiente per questo esempio. Cambiamo che fetchData firma a questo:

fetchData : Task x (Result Http.Error (List RepoInfo)) 
fetchData = 
    Http.get repoInfoListDecoder "https://api.github.com/users/nytimes/repos" 
    |> Task.toResult 

Si noti che sto usando x nel mio tipo di annotazione per il valore di errore di task. Questo è solo perché, mappando a un Result, non dovrò mai preoccuparsi di un errore dall'attività.

Ora, avremo bisogno di alcune azioni per gestire i due possibili risultati: un errore HTTP o un risultato positivo. Aggiornare Action con questo:

type Action 
    = NoOp 
    | FetchData 
    | ErrorOccurred String 
    | DataFetched (List RepoInfo) 

tua funzione di aggiornamento dovrebbe ora impostare i valori del modello.

update action model = 
    case action of 
    NoOp -> 
     (model, Effects.none) 
    FetchData -> 
     ({ model | message = "Initiating data fetch!" }, Effects.none) 
    ErrorOccurred errorMessage -> 
     ({ model | message = "Oops! An error occurred: " ++ errorMessage }, Effects.none) 
    DataFetched repos -> 
     ({ model | repos = repos, message = "The data has been fetched!" }, Effects.none) 

Ora, abbiamo bisogno di un modo per mappare il compito Result a una di queste nuove azioni. Dal momento che non voglio impantanarsi nella gestione degli errori, sto solo andando a utilizzare toString per modificare l'oggetto di errore in una stringa a scopo di debug

httpResultToAction : Result Http.Error (List RepoInfo) -> Action 
httpResultToAction result = 
    case result of 
    Ok repos -> 
     DataFetched repos 
    Err err -> 
     ErrorOccurred (toString err) 

Questo ci dà un modo per mappare un never- attività non riuscite a un'azione. Tuttavia, StartApp si occupa di Effetti, che è un sottile strato su Attività (oltre ad alcune altre cose).Avremo bisogno di un altro pezzo prima che possiamo legarlo insieme, e questo è un modo per mappare l'attività HTTP che non ha mai fallito ad un Effetto del nostro tipo Azione.

fetchDataAsEffects : Effects Action 
fetchDataAsEffects = 
    fetchData 
    |> Task.map httpResultToAction 
    |> Effects.task 

Forse avete notato che ho chiamato questa cosa, "mai mancare". All'inizio questo mi ha confuso, quindi lasciami provare a spiegare. Quando creiamo un'attività, ci viene garantito un risultato, ma è un successo o un fallimento. Al fine di rendere le app di Elm il più robuste possibile, eliminiamo in sostanza la possibilità di errore (con cui intendo principalmente, un'eccezione Javascript non gestita), gestendo esplicitamente ogni caso. Ecco perché abbiamo affrontato il problema della mappatura prima a Result e poi al nostro Action, che gestisce in modo esplicito i messaggi di errore. Dire che non fallisce mai non significa dire che i problemi HTTP non possono accadere, vuol dire che stiamo gestendo ogni possibile risultato, e che gli errori sono mappati ai "successi" mappandoli a un'azione valida.

Prima del nostro passaggio finale, assicuriamoci che il nostro view possa mostrare l'elenco dei repository.

view : Signal.Address Action -> Model -> Html 
view address model = 
    let 
    showRepo repo = 
     li [] 
     [ text ("Repository ID: " ++ (toString repo.id) ++ "; ") 
     , text ("Repository Name: " ++ repo.name) 
     ] 
    in 
    div [] 
     [ div [] [ text model.message ] 
     , button [ onClick address FetchData ] [ text "Click to load nytimes repositories" ] 
     , ul [] (List.map showRepo model.repos) 
     ] 

Infine, il pezzo che lega insieme tutto questo è quello di rendere il FetchData caso della nostra funzione update restituire l'effetto che inizia il nostro compito. Aggiorna la dichiarazione del caso in questo modo:

FetchData -> 
    ({ model | message = "Initiating data fetch!" }, fetchDataAsEffects) 

Questo è tutto! È ora possibile eseguire elm reactor e fare clic sul pulsante per recuperare l'elenco dei repository. Se vuoi testare la gestione degli errori, puoi semplicemente manipolare l'URL per la richiesta Http.get per vedere cosa succede.

Ho pubblicato l'intero esempio di lavoro di questo as a gist. Se non vuoi eseguirlo localmente, puoi vedere il risultato finale incollando quel codice in http://elm-lang.org/try.

Ho cercato di essere molto esplicito e conciso su ogni passaggio lungo il percorso. In una tipica app Elm, molti di questi passaggi verranno ridotti a poche righe e verrà utilizzata una maggiore stenografia idiomatica. Ho cercato di risparmiarti quegli ostacoli rendendo le cose più piccole ed esplicite possibili. Spero che aiuti!

+1

Questo è il tipo di risposta che mi spinge ad imparare Elm. Ok, è difficile da afferrare all'inizio e può torcere il cervello, l'aggiornamento 0.16 -> 0.17 è doloroso ... ma sembra che la comunità sia davvero, davvero gentile e aperta ai nuovi arrivati. Grazie mille per il tempo che hai dedicato a scrivere questa risposta, mi ha aiutato molto. – gbarillot