ソースコードとサンプル
サンプル
ソースコード
ページ遷移入ってますが、ほぼそのままMain.elmとかに使えるはず。
準備
Httpは副作用が発生する手続きである。
Elmで副作用が発生する手続きはCmd Msg
で表現されるが、
複数の副作用が連続するコードはTaskに変換したほうが楽。
ということで、まずはHttpをTaskで利用できるようにする。
HttpでのTask利用
HttpをTaskで利用するには3つの材料が必要。
- Http.task
- Resolver
- Json.Decode 1
Http.task
はHTTPリクエストをTask
型で返し、Resolver
はそのリクエストのレスポンスを受け取り、処理する関数である。
type alias ErrorMessage =
String
jsonResolver : Http.Resolver ErrorMessage String
jsonResolver =
-- いちいちHttpErrorをUpdateで処理するのは二度手間なので、Resolverで処理する
Http.stringResolver <|
\response ->
case response of
Http.BadUrl_ url ->
Err <| "Bad URL: " ++ url
Http.Timeout_ ->
Err "Request Timeout"
Http.NetworkError_ ->
Err "Network Error"
-- BadStatusでもbodyが得られるので、レスポンスによってメッセージを分けたいのであれば、BadStatus用のDecoderを用意して、デコードしてやればよい
Http.BadStatus_ metadata body ->
Err <| "BadStatus: " ++ String.fromInt metadata.statusCode ++ "\nBody: " ++ body
Http.GoodStatus_ metadata body ->
-- 大抵の場合はここで以下のような形でJSONのデコードを行う
{-
case Json.Decode.decodeString decoder body of
Ok value ->
Ok value
Err err ->
Err <| "レスポンスのデコードに失敗しました。\n" ++ Json.Decode.errorToString err
-}
-- decoderはこの関数の引数で渡すと良い
Ok body
requestGet : String -> Task ErrorMessage String
requestGet url =
Http.task
{ method = "GET"
, headers = []
, url = url
, body = Http.emptyBody
, resolver = jsonResolver
, timeout = Nothing
}
これでGETでのHTTPリクエストがTaskとして得られるようになったので、これを逐次的に実行したり、並列に実行するだけである。
おまけ(Taskの使い方)
Taskはelm-guideなどにも記載がほぼないので、馴染みがないかもしれないが、最悪これだけ覚えておけばよい。
- Taskは最終的に
Cmd Msg
として実行されるので、Cmd Msg
の中間表現である -
Cmd Msg
に変換するにはTask.perform
かTask.attempt
を使う -
Task.perform
は必ず成功するTaskに使う -
Task.attempt
は失敗するかもしれないTaskに使う(Httpを使うケースはこちら)
詳細な説明は、@ababup1192 さんのElmのTaskにこんにちはを参照してください。
Httpを逐次的(シーケンシャル)に実行
Task化したHttpリクエストを逐次的に実行するには、TaskのListを作り、Task.sequence
をしてTaskをまとめ、Task.attempt
を呼べばよい。
下記コードは4つのendpointに対してHTTP GETをシーケンシャルに呼び出すコードである。
request1〜4が順番に呼ばれるが、リクエストが失敗したタイミングで処理が止まり、次のリクエストは呼び出されない。
なお、request2とrequest3の呼び出しにTask.andThen
を使っているが、これは1つ前のリクエストの結果を使ってリクエストを行うという時に利用できる。
-- 抜粋コード
requestSequential : Task ErrorMessage (List String)
requestSequential =
let
request1 =
requestGet "https://dog.ceo/api/breeds/image/random"
request2 b =
let
_ =
Debug.log "Response1: " b
-- 前のAPIのレスポンスを使って、次のAPIを呼び出すようなAPIが簡単に見つからなかったので、
-- 以前の結果を使っているよという証明で、Debug.logを使ってconsole.logに書き出しています。
in
requestGet "https://data.ripple.com/v2/ledgers/"
request3 c =
let
_ =
Debug.log "Response2: " c
in
requestGet "http://openlibrary.org/people/george08/lists.json"
request4 =
requestGet "https://binaryjazz.us/wp-json/genrenator/v1/genre/"
in
-- 以前のリクエストの結果を使って、次のリクエストを行うにはTask.andThenを使う
-- Task.sequenceはList (Task a)をTask (List a)にするもの。
-- よって、このTaskは最終的に「request3の結果とrequest4の結果」を取得することになる
-- どこかのTaskが失敗した時点で以降の処理が中断し、エラーが返される
Task.sequence
[ request1
|> Task.andThen (\r -> request2 r)
|> Task.andThen (\r -> request3 r)
, request4
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RequestSequential ->
-- Task.attemptでTask.sequenceしたTaskを呼び出す
( { results = [] }, Task.attempt GotSequential requestSequential )
GotSequential result ->
-- GotSequentialは「最終的に成功したか失敗したか」の1回しか呼び出されない
case result of
Ok response ->
( { model | results = response }, Cmd.none )
Err e ->
-- とりあえず文字列にして結果の代わりに表示でもしておく
( { model | results = [ e ] }, Cmd.none )
Httpを並列に実行
こちらは超シンプルで、Task.attempt
で得られた結果のCmd Msg
をCmd.batch
で呼び出せば良い。
Cmd.batch
は複数のCmd Msg
を同時(並列)に実行する際に使われる。
-- 抜粋コード
requestParallel : Cmd Msg
requestParallel =
let
request1 =
requestGet "https://dog.ceo/api/breeds/image/random"
request2 =
requestGet "https://data.ripple.com/v2/ledgers/"
request3 =
requestGet "http://openlibrary.org/people/george08/lists.json"
request4 =
requestGet "https://binaryjazz.us/wp-json/genrenator/v1/genre/"
in
-- 並列に実行する場合はCmd.batchを使う
-- 並列に実行するので、以前のリクエストを使って次のリクエストを行うことはできない
-- 必ず全てのリクエストの結果が必要な場合などに使うと良い
Cmd.batch
[ Task.attempt GotParallel <| request1
, Task.attempt GotParallel <| request2
, Task.attempt GotParallel <| request3
, Task.attempt GotParallel <| request4
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RequestParallel ->
( { results = [] }, requestParallel )
GotParallel result ->
-- GotParallelはリクエストが発生した回数分呼ばれる。なので、今回は4回呼ばれる。
-- 複数の結果を受け取ることになるので、結果が得られるたびにmodelへ追加している
case result of
Ok response ->
( { model | results = response :: model.results }, Cmd.none )
Err e ->
( { model | results = e :: model.results }, Cmd.none )
あとがき
超平たく説明しているので詳細は、@ababup1192 さんのElmでHttpをわかってしまおうも合わせてご確認ください。
-
今回のサンプル上では使っていないが、JSONを利用するのであれば必要。そして、通常HTTPの利用シーンとしてJSONレスポンスを得ることがほとんどなので、ほぼ必須となる。 ↩