ElmでWebサービス的なものを作るとなった時にAPIサーバとの通信は必須になってくる。
普段JSのaxiosを使っている自分は割と手間取ったのでメモっておく。
基本的にはElm Guideの HTTP
JSON
項目の内容をなぞっているだけ。
想定読者
- The Elm Architecture(Model,View,Update)の概要をなんとなく理解している
- Cmdの使い方・挙動をなんとなく理解している
バージョン
- Elm 0.19
ソース
yarn server
でjson-serverを動かした状態で yarn start
でフロントエンド環境を立ち上げると動作確認できるようになっている。
デモ
Http.get/postとHttp.requestの違い
HTTPパッケージのドキュメントを見ると get
post
関数と request
関数がある。
一見、 get
post
の方がシンプルに使えて良さそうに見えるが、リクエストヘッダーの設定ができないっぽい。
実際には request
を多用することになりそう。
APIリクエストの流れ
実際にAPIを叩くコードを見ていく。
リクエスト関数
json-serverで返ってくる値は次のように設定した。
{
"articles": [
{
"id": 1,
"title": "json-server",
"author": "typicode"
},
{
"id": 2,
"title": "faker.js",
"author": "Marak"
}
],
"profile": {
"name": "typicode"
}
}
まずはprofileのリソースを取得する。
次のようにリクエスト関数を定義。
getProfile : Cmd Msg
getProfile =
Http.request
{ method = "GET"
, headers =
[ Http.header "Authorization" ("Bearer " ++ "TOKEN")
, Http.header "Accept" "application/json"
, Http.header "Content-Type" "application/json"
]
, url = "http://localhost:3000/profile"
, expect = Http.expectJson GotProfile profileDecoder
, body = Http.emptyBody
, timeout = Nothing
, tracker = Nothing
}
headers
Authorizationは認証が必要という想定で適当な値を設定してある。
url
叩くエンドポイントを設定。
body
空なので Http.emptyBody
を渡している。
expect
Http.expectJson
はレスポンス結果がJSONであることを期待する。引数としてレスポンスが返ってきた後のJSONデコーダー関数とデコードされた値を受け取るUpdate関数を渡す。
timeout
タイムアウト。特に設定したくないのでNothingとしている。
tracker
については何を設定する項目なのかわかっていない。
ドキュメントによると、
The tracker lets you cancel and track requests.
トラッカーを使用すると、リクエストをキャンセルして追跡できます。
やはりちょっとわからないが、ひとまずNothingとしている。
JSONデコーダー
/profile
を叩くと
{
"name": "typicode"
}
が返ってくる。
これに対しては次のようにデコーダーを定義する。
profileDecoder : Decoder String
profileDecoder =
field "name" string
field
関数の第一引数にキー名、第二引数にDecoder関数を渡す。
今回は文字列型なので Json.Decode.string
関数を渡す。
特にこのあたりは使い方を知っているだけで理解が怪しい点。
Update関数
作成したリクエスト関数はUpdateの中で使う。
Msgの型は下のように定義。
type Msg
= GetProfile
GotProfile (Result Http.Error String)
Result型は1つ目に失敗時のエラーの型、2つ目に成功時に受け取る値の型を定義する。
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GetProfile ->
( model
, getProfile
)
GotProfile result ->
case result of
Ok name ->
( { model | name = name }, Cmd.none )
Err _ ->
( model, Cmd.none )
ViewからはUpdate関数である GetProfile
を呼ぶ。
GetProfile
はリクエスト関数の getProfile
を呼ぶ。
getProfile
でAPIへのリクエストが行われ、受け取ったレスポンスをデコードして GotProfile
へ渡す。
GotProfile
ではデコードされた値が渡ってくるので、その値をModelに格納する。
JSONが配列で返ってくる場合
/articles
を叩くと、
[
{
"id": 1,
"title": "json-server",
"author": "typicode"
},
{
"id": 2,
"title": "faker.js",
"author": "Marak"
}
]
このように配列が返ってくる。
Elm的にはListとして受け入れる必要がある。
ListのデコードはElm Guideでは記載されていないが、ググって出てきたスタックオーバーフローの回答がかなり参考になった。
考え方としてはまずListでない単一要素のデコーダーを定義する。
続いてそのデコーダーをベースにListにデコーダーを定義する。
getArticles : Cmd Msg
getArticles =
Http.request
{ method = "GET"
, headers =
[ Http.header "Authorization" ("Bearer " ++ "TOKEN")
, Http.header "Accept" "application/json"
, Http.header "Content-Type" "application/json"
]
, url = "http://localhost:3000/articles"
, expect = Http.expectJson GotArticles articlesDecoder -- ③Listのデコーダーを渡す
, body = Http.emptyBody
, timeout = Nothing
, tracker = Nothing
}
articleDecoder : Decoder Article -- ①まず単一のデコーダーを定義
articleDecoder =
Json.Decode.map3 Article
(Json.Decode.field "id" Json.Decode.int)
(Json.Decode.field "title" Json.Decode.string)
(Json.Decode.field "author" Json.Decode.string)
articlesDecoder : Decoder (List Article) -- ②上を基にListのデコーダーを定義
articlesDecoder =
Json.Decode.list articleDecoder
デコーダーが定義できてしまえば後は同じで、あまり迷わず実装できた。
type Msg
= GetProfile
GotProfile (Result Http.Error String)
GetArticles
GotArticles (Result Http.Error (List Article))
Update関数の引数もList型で定義しておく。
参考
https://guide.elm-lang.org/effects/json.html
https://package.elm-lang.org/packages/elm/http/latest/Http
https://stackoverflow.com/questions/39320948/decode-json-array-with-objects-in-elm