iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 24
0
Modern Web

學會Elm寫前端系列 第 24

24 elm Q&A: 可以在說一次Json.Decode嗎?

為什麼是JSON?

Evan Czaplicki 寫了篇為什麼選擇json來當做是資料交換的處理,而不是其他的格式,之前有簡單地帶過關於elm在json的處理是怎麼做的,不過當時沒有說很多,只是簡單提到,可以這麼做。但其實elm在json的處理是可以出一本書的:json survival kit!簡單說就是其他的交換系統都是被特定廠商挾持(譬如GraphQL ?),只有JSON是大家都可以認可的,但下個十年會怎麼變也沒有人知道,姑且就先用JSON擋著用

Json.Decode

書有沒有買沒有關係(我也沒有),不過要好好地去想想關於json要怎麼在elm裡頭變成是你要的資料型態才是重點。

先來個 octocat的api

{
  "login": "octocat",
  "id": 583231,
  "name": "The Octocat",
}

你可以自建一個 type alias

type alias User = 
{ id: Int
, login: String
, name: String
}

認識field 和 at

這個是兩個常常你會用的的method。簡單的說,field 就是針對一個,at是針對nested json裡,用list的方式去取得json內的值。

field : String -> Decoder a -> Decoder a
at : List String -> Decoder a -> Decoder a

因為我們不知道json內是string, int或是其他的type(你也可以自建自己要的decode 後的 type。)

如果不懂的話,可以直接看doc:

decodeString (field "x" int) "{ \"x\": 3 }"            == Ok 3
decodeString (field "x" int) "{ \"x\": 3, \"y\": 4 }"  == Ok 3

json = """{ "person": { "name": "tom", "age": 42 } }"""

decodeString (at ["person", "name"] string) json  == Ok "tom"

map和他的朋友們

map : (a -> value) -> Decoder a -> Decoder value
  githubUser : Decoder User
  githubUser = 
      map3 User
          (field "id" int)
          (field "login" string)
          (field "name" string)
-- 其實這就是object -> record的方法

map 有從2 到8,如果你要decode的位置沒有很多的話,可以直接使用,但是如果你需要大於8 以上的話,elm上也是直接推薦你使用社群套件:elm-decode-pipeline

Note: If you run out of map functions, take a look at elm-decode-pipeline which makes it easier to handle large objects, but produces lower quality type errors.

object -> tuple

githubUser' : Decoder (String, String)
githubUser' =
    map2 (,)
        (field "login" string)
        (field "name" string)

nested object

這是參考 Brian Hicks的文章:

{
  "Site1": {
    "PC1": {
      "ip": "x.x.x.x",
      "version": "3"
    },
    "PC2": {
      "ip": "x.x.x.x",
      "version": "3"
    }
  },
  "Site2": {
    "PC1": {
      "ip": "x.x.x.x",
      "version": "3"
    },
    "PC2": {
      "ip": "x.x.x.x",
      "version": "3"
    }
  }
}

elm的decode裡,還有一個很好用的東西:


dict : Decoder a -> Decoder (Dict String a)

type alias Value = 
    Value
value : Decoder Value

value 有點像是個停泊點,你尚未要去取得在json的特定type,你可以用一個叫 Value
type先暫時叫這個名字,等到你很確定了,再加上你要的type。

Do not do anything with a JSON value, just bring it into Elm as a Value. This can be useful if you have particularly crazy data that you would like to deal with later. Or if you are going to send it out a port and do not care about its structure.

-- first attempt
sites : Decoder (Dict String Value)
sites =
    dict value

decodeString sites ourSitesBlob 
  == Ok (Dict.fromList 
       [ ("Site1", { PC1 = { ip = "x.x.x.x", version = "3" }, PC2 = { ip = "x.x.x.x", version = "3" } })
       , ("Site2", { PC1 = { ip = "x.x.x.x", version = "3" }, PC2 = { ip = "x.x.x.x", version = "3" } })
       ])

sites : Decoder (Dict String (Dict String Value))
sites =
    dict (dict value)

-- second attempt
decodeString sites ourSitesBlob 
  == Ok (Dict.fromList 
       [ ("Site1"
         , Dict.fromList 
           [ ( "PC1", { ip = "x.x.x.x", version = "3" } )
           , ( "PC2", { ip = "x.x.x.x", version = "3" } )
           ]
         )
       , ( "Site2"
         , Dict.fromList 
           [ ( "PC1", { ip = "x.x.x.x", version = "3" } )
           , ( "PC2", { ip = "x.x.x.x", version = "3" } ) 
           ]
         )
       ])

-- final result

type alias Machine =
    { ip : String
    , version : String
    }


machine : Decoder Machine
machine =
    map2 Machine
        (field "ip" string)
        (field "version" string)

我們可以一步步的去嘗試去取得我們想要的data.

Http.send & Http.get

但是我們不太可能常常在本機端剛好有json的data,常常要使用http的方式,我們拿這個elm-json-decoding來當例子:

getData : Http.Request (List User)
getData =
    Http.get "/src/data.json" usersDecoder


fetchUsers : Cmd Msg
fetchUsers =
Http.send NewHttpData getData

send : (Result Error a -> msg) -> Request a -> Cmd msg

先是用 Htpp.get 得到的type 因為你的decoder會是 List User 。我們等等再來看,先來看 send
send 會是一個 Cmd 會吃一個 Request 當parameter,再傳出去給 elm runtime ,
所以要send出一個你定義的 Msg type,而且這個type要是 Result

type Msg
    = NewHttpData (Result Http.Error (List User))

update msg model =
    case msg of
        NewHttpData result ->
            case result of
                Ok users ->
                    ( { model | users = Just users }, Cmd.none )

                Err e ->
                    ( { model | error = Just e }, Cmd.none )

我們自行定義的Msg裡的 NewHttpData 就是一個 Result 如果成功就會是 List User
,我們再自己定義自己要的decoder

type alias User =
    { name : String
    , age : Int
    , description : Maybe String
    , languages : List String
    , playsFootball : Bool
    }


type alias Model =
    { users : Maybe (List User)
    , error : Maybe Http.Error
}

usersDecoder : Decode.Decoder (List User)
usersDecoder =
    Decode.at [ "users" ] (Decode.list userDecoder)


userDecoder : Decode.Decoder User
userDecoder =
    Decode.map5
        User
        (Decode.at [ "name" ] Decode.string)
        (Decode.at [ "age" ] Decode.int)
        (Decode.maybe (Decode.at [ "description" ] Decode.string))
        (Decode.at [ "languages" ] (Decode.list Decode.string))
        (Decode.at [ "sports" ] sportsDecoder)

這樣就完成了從伺服器端到elm後,如何讀取json了。 希望這樣有再更清楚一點。


上一篇
23 elm Q&A: 聽說和react, redux有關係?
下一篇
25 elm Q&A: 如何submit一個form?登入登出?
系列文
學會Elm寫前端30

尚未有邦友留言

立即登入留言