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