如何在Elm中提取Http请求的结果

joh*_*ual 13 io json http elm

使用Elm的html包可以发出http请求:

https://api.github.com/users/nytimes/repos
Run Code Online (Sandbox Code Playgroud)

这些都是纽约时报在Github上的回购.基本上我从Github响应中有两个项目,id名称

[ { "id": 5803599,  "name": "backbone.stickit"  , ... }, 
  { "id": 21172032, "name": "collectd-rabbitmq" , ... }, 
  { "id": 698445,   "name": "document-viewer"   , ... }, ... ]
Run Code Online (Sandbox Code Playgroud)

Elm类型Http.get需要一个Json Decoder对象

> Http.get
<function> : Json.Decode.Decoder a -> String -> Task.Task Http.Error a
Run Code Online (Sandbox Code Playgroud)

我不知道如何打开列表.所以我把解码器Json.Decode.string和至少匹配的类型,但我不知道如何处理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))
Run Code Online (Sandbox Code Playgroud)

我只想要一个repo名称的Elm对象,所以我可以在一些div元素中显示,但我甚至无法获取数据.


有人可以慢慢地告诉我如何编写解码器以及如何使用Elm获取数据?

Cha*_*ert 25

榆树0.17更新:

我已经更新了这个答案的完整要点,与Elm 0.17一起工作.您可以在此处查看完整的源代码.它将在http://elm-lang.org/try上运行.

在0.17中进行了许多语言和API更改,使得以下某些建议过时.您可以在此处阅读有关0.17升级计划的信息.

我将在下面保留0.16未触及的原始答案,但您可以比较最终的要点以查看已更改的列表.我相信更新的0.17版本更干净,更容易理解.

榆树0.16的原始答案:

看起来你正在使用Elm REPL.如上所述,您无法在REPL中执行任务.我们稍后会详细介绍原因.相反,让我们创建一个实际的Elm项目.

我假设你已经下载了标准的Elm工具.

您首先需要创建一个项目文件夹并在终端中打开它.

开始使用Elm项目的常用方法是使用StartApp.让我们以此为出发点.首先需要使用Elm包管理器命令行工具来安装所需的包.在项目根目录的终端中运行以下命令:

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
Run Code Online (Sandbox Code Playgroud)

现在,在名为Main.elm的项目根目录下创建一个文件.这里有一些样板的StartApp代码可以帮助您入门.我不会在这里解释详细信息,因为这个问题具体是关于任务.您可以通过阅读Elm Architecture Tutorial了解更多信息.现在,将其复制到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 ]
    ]
Run Code Online (Sandbox Code Playgroud)

您现在可以使用elm-reactor运行此代码.转到项目文件夹中的终端并输入

elm reactor
Run Code Online (Sandbox Code Playgroud)

这将默认在端口8000上运行Web服务器,您可以在浏览器中提取http:// localhost:8000,然后导航到Main.elm以查看"Hello,Elm"示例.

这里的最终目标是创建一个按钮,当单击该按钮时,会拉入nytimes存储库列表并列出每个存储库的ID和名称.我们先创建该按钮.我们将通过使用标准的html生成函数来实现.使用以下内容更新view函数:

view address model =
  div []
    [ div [] [ text model.message ]
    , button [] [ text "Click to load nytimes repositories" ]
    ]
Run Code Online (Sandbox Code Playgroud)

按钮单击本身没有任何作用.我们需要创建一个Action,然后由该update函数处理.按钮启动的操作是从Github端点获取数据.Action现在变成:

type Action
  = NoOp
  | FetchData
Run Code Online (Sandbox Code Playgroud)

现在我们可以在update函数中对这个动作进行处理.现在,让我们更改消息以显示按钮单击已处理:

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

最后,我们必须使按钮单击触发该新操作.这是使用该onClick函数完成的,该函数为该按钮生成单击事件处理程序.按钮html生成行现在看起来像这样:

button [ onClick address FetchData ] [ text "Click to load nytimes repositories" ]
Run Code Online (Sandbox Code Playgroud)

大!现在,单击它时应该更新该消息.让我们转到任务.

正如我之前提到的,REPL(尚未)支持调用任务.如果你来自像Javascript这样的命令式语言,这可能看起来有悖常理,当你编写代码"从这个URL获取数据"时,它会立即创建一个HTTP请求.在像Elm这样纯粹的功能性语言中,你做的事情有点不同.当您在Elm中创建一个Task时,您实际上只是在表示您的意图,创建一种"包",您可以将其移交给运行时以执行导致副作用的操作; 在这种情况下,请联系外部世界并从URL中提取数据.

让我们继续创建一个从URL获取数据的任务.首先,我们需要在Elm中使用一种类型来表示我们关心的数据的形状.你表示你只想要idname田地.

type alias RepoInfo =
  { id : Int
  , name : String
  }
Run Code Online (Sandbox Code Playgroud)

作为关于Elm内部类型构造的注释,让我们停下来看一下我们如何创建RepoInfo实例.由于有两个字段,您可以使用RepoInfo两种方法之一构建一个字段.以下两个陈述是等效的:

-- 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"
Run Code Online (Sandbox Code Playgroud)

当我们谈论Json解码时,第二个构建实例将变得更加重要.

我们还将这些列表添加到模型中.我们还必须改变这个init功能,从一个空列表开始.

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

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

由于来自URL的数据以JSON格式返回,我们需要一个Json解码器将原始JSON转换为我们的类型安全的Elm类.创建以下解码器.

repoInfoDecoder : Json.Decoder RepoInfo
repoInfoDecoder =
  Json.object2
    RepoInfo
    ("id" := Json.int) 
    ("name" := Json.string) 
Run Code Online (Sandbox Code Playgroud)

让我们分开吧.解码器将原始JSON映射到我们映射的类型的形状.在这种情况下,我们的类型是一个带有两个字段的简单记录别名.请记住,我前面提到我们可以通过使用RepoInfo带有两个参数的函数来创建RepoInfo实例吗?这就是我们Json.object2用来创建解码器的原因.第一个arg object是一个本身带有两个参数的函数,这就是我们传入的原因RepoInfo.它相当于具有arity 2的函数.

剩下的参数说明了类型的形状.由于我们的RepoInfo模型列出了id第一个和name第二个,这就是解码器期望参数的顺序.

我们需要另一个解码器来解码RepoInfo实例列表.

repoInfoListDecoder : Json.Decoder (List RepoInfo)
repoInfoListDecoder =
  Json.list repoInfoDecoder
Run Code Online (Sandbox Code Playgroud)

现在我们有了模型和解码器,我们可以创建一个函数来返回获取数据的任务.请记住,这实际上并不是获取任何数据,它只是创建了一个我们可以在以后传递给运行时的函数.

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

有许多方法可以处理可能发生的各种错误.让我们选择Task.toResult,它将请求的结果映射到Result类型.它会使我们的事情变得更容易,对于这个例子就足够了.让我们改变这个fetchData签名:

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

请注意,我x在我的类型注释中使用了Task的错误值.那只是因为,通过映射到a Result,我将永远不必关心任务中的错误.

现在,我们需要一些操作来处理两个可能的结果:HTTP错误或成功的结果.Action用这个更新:

type Action
  = NoOp
  | FetchData
  | ErrorOccurred String
  | DataFetched (List RepoInfo)
Run Code Online (Sandbox Code Playgroud)

您的更新功能现在应该在模型上设置这些值.

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)
Run Code Online (Sandbox Code Playgroud)

现在,我们需要一种方法将Result任务映射到这些新操作之一.由于我不想陷入错误处理,我只是想toString用来将错误对象更改为字符串以进行调试

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

这为我们提供了一种将永不失败的任务映射到Action的方法.但是,StartApp处理的是Effects,它是一个关于Tasks(以及其他一些东西)的薄层.在我们将它们组合在一起之前,我们还需要一个部分,这是将永不失败的HTTP任务映射到我们的类型Action的效果的一种方法.

fetchDataAsEffects : Effects Action
fetchDataAsEffects =
  fetchData
    |> Task.map httpResultToAction
    |> Effects.task
Run Code Online (Sandbox Code Playgroud)

你可能已经注意到我称之为"从不失败".起初这让我很困惑,所以让我试着解释一下.当我们创建一个任务时,我们会保证一个结果,但它是成功还是失败.为了使Elm应用程序尽可能健壮,我们实质上通过显式处理每个案例来消除失败的可能性(我主要是指一个未处理的Javascript异常).这就是为什么我们经历了首先映射到a Result然后再映射到我们的问题Action,它显式处理错误消息.说它永远不会失败并不是说HTTP问题不会发生,而是说我们正在处理所有可能的结果,并且通过将错误映射到有效的操作将错误映射到"成功".

在我们最后一步之前,让我们确保我们view可以显示存储库列表.

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)
      ]
Run Code Online (Sandbox Code Playgroud)

最后,将这一切联系在一起的部分是让FetchData我们的update函数的情况返回启动我们任务的效果.像这样更新case语句:

FetchData ->
  ({ model | message = "Initiating data fetch!" }, fetchDataAsEffects)
Run Code Online (Sandbox Code Playgroud)

而已!您现在可以运行elm reactor并单击按钮以获取存储库列表.如果要测试错误处理,可以直接修改Http.get请求的URL 以查看发生的情况.

我已经将这个完整的工作示例作为要点发布了.如果您不想在本地运行它,可以通过将该代码粘贴到http://elm-lang.org/try来查看最终结果.

我试图在整个过程中的每一步都非常明确和简洁.在典型的Elm应用程序中,很多这些步骤将简化为几行,并且将使用更多惯用的简写.我试图通过尽可能小而明确的方式来避免这些障碍.我希望这有帮助!