2016-01-27 11 views
13

Korzystanie html pakiet Elm jest to możliwe, aby żądania http:Jak wyodrębnić wyniki żądań HTTP w Elm

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

Są to wszystkie New York Times repo na Github. Zasadniczo istnieją dwie rzeczy chciałbym z odpowiedzi GitHub, nazwa id i

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

Typ Elm dla Http.get wymaga JSON Decoder obiektowi

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

ja nie wiedzieć, jak otwierać listy jeszcze. Więc umieściłem dekoder Json.Decode.string i przynajmniej te typy były dopasowane, ale nie miałem pojęcia, co zrobić z obiektem 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)) 

Chcę tylko obiektem Elm nazw repo więc mogę wyświetlać w niektórych div elementów, ale nie mogę nawet uzyskać dane na zewnątrz.


Może ktoś powoli chodzić mi przez jak napisać dekoder i jak uzyskać dane na zewnątrz z Elm?

Odpowiedz

24

Aktualizacja dla Elm 0,17:

I zostały zaktualizowane pełny sens tej odpowiedzi do pracy z Elm 0,17. Możesz see the full source code here. Będzie działał pod numerem http://elm-lang.org/try.

Wprowadzono szereg zmian w języku i interfejsie API w wersji 0.17, które powodują, że niektóre z poniższych zaleceń są przestarzałe. Możesz read about the 0.17 upgrade plan here.

Pozostawię oryginalną odpowiedź na 0,16 nietknięte poniżej, ale można compare the final gists to see a list of what has changed. Uważam, że nowsza wersja 0.17 jest czystsza i łatwiejsza do zrozumienia.

Original Odpowiedź na Elm 0,16:

Wygląda na to, że używasz Elm rEPL. As noted here, nie będziesz w stanie wykonywać zadań w REPL. Więcej informacji na temat , dlaczego w pewnym sensie jest. Zamiast tego, stwórzmy rzeczywisty projekt wiązu.

Zakładam, że pobrałeś standard Elm tools.

Najpierw należy utworzyć folder projektu i otworzyć go w terminalu.

Typowym sposobem na rozpoczęcie projektu Elm jest użycie StartApp. Wykorzystajmy to jako punkt wyjścia. Najpierw należy użyć narzędzia wiersza poleceń menedżera pakietów Elm, aby zainstalować wymagane pakiety. Uruchom następujące polecenie w terminalu w katalogu głównym projektu:

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 

Teraz utwórz plik w katalogu głównym projektu o nazwie Main.elm. Oto kilka podstawowych kodów StartApp na dobry początek.Nie będę tu tłumaczył szczegółów, ponieważ to pytanie dotyczy konkretnie zadań. Możesz dowiedzieć się więcej, przechodząc przez Elm Architecture Tutorial. Na razie skopiuj to do 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 ] 
    ] 

Możesz teraz uruchomić ten kod przy pomocy Reaktora elm. Idź do terminalu w folderze projektu i wprowadzić

elm reactor 

Spowoduje to uruchomienie serwera WWW na porcie 8000 domyślnie, można podciągnąć http://localhost:8000 w przeglądarce, a następnie przejdź do Main.elm aby zobaczyć „Hello , Wiąz "przykład.

Koniec celem jest, aby utworzyć przycisk, który po kliknięciu, ciągnie się na liście nytimes repozytoriów i znajdują się identyfikatory i nazwy każdego z nich. Najpierw utwórzmy ten przycisk. Zrobimy to za pomocą standardowych funkcji generowania html. Aktualizować funkcję view z czymś takim:

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

na własną rękę, kliknij przycisk robi nic. Musimy utworzyć działanie, które następnie będzie obsługiwane przez funkcję update. Działanie, które uruchamia przycisk, polega na pobraniu danych z punktu końcowego Github. Action staje się teraz:

type Action 
    = NoOp 
    | FetchData 

I możemy teraz skrótową się obchodzenia tego działania w funkcji update jak tak. Teraz zmieńmy wiadomość, aby pokazać, że przycisk został kliknięcie obsługiwane:

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

Wreszcie, musimy spowodować kliknie przycisk, aby wywołać tę nową akcję. Odbywa się to za pomocą funkcji onClick, która generuje program obsługi zdarzeń kliknięcia dla tego przycisku. Linia generowania html przycisku wygląda następująco:

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

Świetnie! Teraz wiadomość powinna zostać zaktualizowana po jej kliknięciu. Przejdźmy do zadań.

Jak już wspomniano wcześniej, REPL ma (jeszcze) nie obsługuje wywoływanie zadań. Może to wydawać się sprzeczne z intuicją, jeśli używasz imperatywnego języka, takiego jak Javascript, gdzie podczas pisania kodu z napisem "pobierz dane z tego adresu URL" natychmiast tworzy się żądanie HTTP. W czysto funkcjonalnym języku, takim jak Elm, robisz rzeczy trochę inaczej. Kiedy tworzysz Zadanie w Wiązzie, tak naprawdę właśnie wskazujesz swoje intencje, tworząc rodzaj "pakietu", który możesz przekazać do środowiska wykonawczego, aby zrobić coś, co powoduje efekty uboczne; w takim przypadku skontaktuj się ze światem zewnętrznym i pobierz dane z adresu URL.

Chodźmy do przodu i utworzyć zadanie, które pobiera dane z url. Po pierwsze, będziemy potrzebować typu wewnątrz wiązu, aby przedstawić kształt danych, na których nam zależy. Wskazałeś, że chcesz tylko pola id i name.

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

Jako uwagę na temat budowy typu wewnątrz Elma, zatrzymajmy się na chwilę i porozmawiajmy o tym, jak tworzymy instancje RepoInfo. Ponieważ istnieją dwa pola, możesz zbudować RepoInfo na jeden z dwóch sposobów. Poniższe dwa stwierdzenia są równoważne:

-- 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" 

Ten drugi etap budowy instancji stanie się ważniejszy, gdy będziemy rozmawiać o dekodowaniu Jsona.

Dodajmy również listę tych do modelu. Będziemy musieli również zmienić funkcję init, aby rozpocząć od pustej listy.

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

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

Ponieważ dane z adresu URL wraca w formacie JSON, musimy się tłumaczyć JSON dekoder do surowego JSON do naszego typu bezpieczny klasy Elm. Utwórz następujący dekoder.

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

Wybierzmy to osobno. Dekoder jest tym, co odwzorowuje surowy JSON na kształt typu, który mapujemy. W tym przypadku nasz typ to prosty alias rekordu z dwoma polami. Pamiętaj, że kilka dni temu wspomniałem, że możemy utworzyć instancję RepoInfo przy użyciu RepoInfo jako funkcji, która pobiera dwa parametry? Właśnie dlatego używamy Json.object2 do stworzenia dekodera. Pierwszy argument do object to funkcja, która sama bierze dwa argumenty i dlatego przechodzimy w RepoInfo. Jest to odpowiednik funkcji z arnością drugą.

Pozostałe argumenty określają kształt typu. Ponieważ nasz model RepoInfo wymienia najpierw id pierwszy i name sekund, jest to kolejność, w której dekoder oczekuje, że argumenty będą.

Potrzebujemy innego dekodera do odkodowania listy instancji RepoInfo.

Teraz, gdy mamy model i dekoder, możemy utworzyć funkcję, która zwraca zadanie do pobrania danych. Pamiętaj, że to nie jest pobieranie żadnych danych, to tylko tworzenie funkcji, którą możemy później przekazać do środowiska wykonawczego.

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

Istnieje wiele sposobów radzenia sobie z różnymi błędami, które mogą wystąpić. Wybierzmy opcję Task.toResult, która odwzorowuje wynik żądania na typ Result. Ułatwi nam to nieco i wystarczy na ten przykład. Zmieńmy to fetchData podpis do tego:

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

Zauważ, że używam x w moim typie adnotacji o wartości błędu zadania. Dzieje się tak tylko dlatego, że mapując do Result, nigdy nie będę musiał przejmować się błędem z zadania.

Teraz będziemy potrzebować pewnych działań, aby obsłużyć dwa możliwe wyniki: Błąd HTTP lub pomyślny wynik. Aktualizować Action z tym:

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

Twoja funkcja aktualizacji powinien teraz ustawić te wartości od modelu.

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) 

Teraz musimy znaleźć sposób, aby mapować zadanie Result do jednego z tych nowych działań. Ponieważ nie chcę ugrzęznąć w obsługę błędów, jestem po prostu zamiar użyć toString zmienić obiekt błędu na sznurku do celów debugowania

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

To daje nam sposób mapować Niemniej niepowodzenie zadania w działaniu. Jednak StartApp zajmuje się efektami, które są cienką warstwą w stosunku do zadań (a także kilku innych rzeczy).Będziemy potrzebować jeszcze jednego kawałka, zanim będziemy mogli powiązać go ze sobą, a to jest sposób na odwzorowanie nigdy nieudanego zadania HTTP na Efekty naszego działania typu.

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

Być może zauważyłeś, że nazwałem to, "nigdy nie zawodzę". Początkowo było to dla mnie mylące, więc pozwól mi wyjaśnić. Kiedy tworzymy zadanie, mamy gwarancję wyniku, ale jest to sukces lub porażka. Aby aplikacje Elma były jak najbardziej odporne, w zasadzie usuwamy możliwość niepowodzenia (przez co rozumiem głównie nieobsługiwany wyjątek JavaScript), poprzez jawne obsługiwanie każdego przypadku. Właśnie dlatego przebrnęliśmy przez problem polegający na tym, że najpierw mapowaliśmy na Result, a następnie na naszą Action, która jawnie obsługuje komunikaty o błędach. Powiedzenie, że nigdy się nie zawiedzie, nie oznacza, że ​​problemy z HTTP nie mogą się zdarzyć, oznacza to, że mamy do czynienia z każdym możliwym wynikiem, a błędy są mapowane na "sukcesy" przez odwzorowanie ich na prawidłową akcję.

Przed naszym ostatnim krokiem, upewnijmy się, że nasz view może pokazać listę repozytoriów.

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

Wreszcie kawałek, który łączy to wszystko razem jest, aby FetchData przypadku naszego update funkcja zwróci efekt, który inicjuje nasze zadanie. Zaktualizuj instrukcję sprawy tak:

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

To wszystko! Możesz teraz uruchomić elm reactor i kliknąć przycisk, aby pobrać listę repozytoriów. Jeśli chcesz przetestować obsługę błędów, możesz po prostu zmienić URL dla żądania Http.get, aby zobaczyć, co się stanie.

Opublikowałem cały działający przykład tego as a gist. Jeśli nie chcesz uruchomić go lokalnie, możesz zobaczyć końcowy wynik, wklejając ten kod do http://elm-lang.org/try.

Starałem się być bardzo jednoznaczny i zwięzły na temat każdego kroku po drodze. W typowej aplikacji Elma wiele z tych kroków zostanie skondensowanych do kilku wierszy i użyty zostanie bardziej idiomatyczny skrót. Próbowałem oszczędzić ci tych przeszkód, robiąc rzeczy tak małe i wyraźne, jak to tylko możliwe. Mam nadzieję, że to pomoże!

+1

Jest to odpowiedź, która sprawia, że ​​dążę do nauki Wiązu. Ok, początkowo trudno jest to uchwycić i możesz przekręcić swój mózg, aktualizacja 0.16 -> 0.17 jest bolesna ... ale wygląda na to, że społeczność jest naprawdę dobra i otwarta na nowych klientów. Dziękuję bardzo za czas poświęcony na pisanie tej odpowiedzi, bardzo mi pomógł. – gbarillot

Powiązane problemy