在Elm体系结构中编写程序

时间:2018-07-03 14:08:04

标签: elm composition elm-architecture

假设我要创建一个包含两个组件的网页,例如NavbarBody。这两个组件不会互相影响,可以独立开发。因此,我有两个Elm文件,每个文件中都包含以下组件:

type Model = ...

type Msg = ...

init : (Model, Cmd Msg)

update : Msg -> Model -> (Model, Cmd Msg)

view : Model -> Html Msg

假设它们都可以正常工作,我们如何将它们组合成一个包含这两个组成部分的程序?

我试图写这样的东西:

type Model = {body : Body.Model , navbar : Navbar.Model}
type Msg = BodyMsg Body.Msg | NavbarMsg Navbar.Msg

view : Model -> Html Msg
view model = div [] [Body.view model.body, Navbar.view model.navbar]

update : Msg -> Model -> (Model, Cmd Msg)
update = ...

当我尝试编写此更新函数时,以上内容很快变得很难看。特别是,一旦我从Navbar.updateBody.update的Cmd更新功能中提取了Msg,如何提取它们并将它们再次反馈给这些功能?另外,上面的view函数看起来并不是特别习惯。

推荐使用elm架构解决此问题的方法是什么?这种模式在榆木建筑中是惯用的吗?

3 个答案:

答案 0 :(得分:5)

我认为@dwaynecrooks涵盖了问题的技术方面。但是我相信您的问题也暗示了设计方面。


如何增加榆木代码?

正如其他人指出的那样:在组件方面进行思考几乎可以肯定会带您走上Elm不太吸引人的道路。 (许多人从这里开始。我和我的团队从两年前开始。我花了3个应用程序/主要重新设计使我至少对基本原理感到满意。)

我建议您将Elm应用程序视为一棵树,而不是组件。树的每个节点代表一个抽象级别,并在该级别上描述应用程序的行为。当您感觉到给定级别的细节太多时,您可以开始考虑如何将新的较低级别的抽象作为子节点引入。

在实践中,每个节点都在自己的Elm模块中实现:父母导入孩子。您可能还认为您不必坚持通常的model/update/view签名,而应该关注应用程序域的特殊性。在我看来,Richard Feldman在他的Real World SPA example app中就是这么做的。 Evan's Life of a file talk也与此问题有关。


navbar + body

关于您的特殊情况-这不是罕见的情况-这是我的经验。如果我们说我们的Web应用程序有一个导航栏,然后有一些正文,则这是该应用程序的静态描述。这种描述可能适合基于组件的思维方式,但是如果您最终想要一个优雅的Elm应用程序,那么它的帮助就会减少。

相反,值得尝试在这种抽象级别上描述应用的行为,这听起来可能像这样:用户可以选择x,{导航栏中的{1}},y个项目。单击这些项目将以z的方式影响该项目,也将以qa的方式影响主体。他还可以单击导航栏中的b,这将显示一个弹出窗口,或者单击v,这会将他退出应用程序。

如果您采用此描述并应用了我上面描述的逻辑,则您可能最终应该进行某种设计,其中大多数导航栏都以最高抽象级别描述。这包括项目wxyz和行为vab。现在,行为w可能意味着必须显示一个特定的,丰富的页面,该页面具有其自己的详细行为,该行为在较低的抽象级别上进行了描述,而行为a可能意味着基于选择必须加载一些内容,然后再次在较低的抽象级别上确定此加载过程的详细信息。依此类推。

当我们开始以这种方式解决问题时,找出如何拆分逻辑以及如何处理特殊情况变得更加直接。 例如,我们意识到,当有人说某个页面要在“导航栏中”显示某些内容时,她真正的意思是导航栏应折叠(或变换)特定页面,以便该页面可以显示其自己的标题在那个地区。

关注应用的行为而不是静态内容区域有助于解决这一问题。

答案 1 :(得分:4)

是的,您在正确的道路上。

在视图中,您需要使用Html.map

view : Model -> Html Msg
view model =
  div []
    [ Html.map BodyMsg (Body.view model.body)
    , Html.map NavbarMsg (Navbar.view model.navbar)
    ]

Body.view model.body的类型为Html Body.Msg,这要求我们使用Html.map来获取正确的类型Html Msg。对于Navbar.view model.navbar同样。

而且,对于update函数,您应该这样写:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    BodyMsg bodyMsg ->
      let
        (newBody, newCmd) = Body.update bodyMsg model.body
      in
        { model | body = newBody } ! [ Cmd.map BodyMsg newCmd ]

    NavbarMsg navbarMsg ->
      let
        (newNavbar, newCmd) = Navbar.update navbarMsg model.navbar
      in
        { model | navbar = newNavbar } ! [ Cmd.map NavbarMsg newCmd ]

BodyMsg情况下,newBody的类型为Body.Model,因此我们可以为其设置body中的model字段。但是,newCmd的类型为Cmd Body.Msg,因此在返回之前,我们需要使用Cmd.map来获取正确的返回类型Cmd Msg

类似的推理可以用于NavbarMsg情况。

  

此外,上面的视图功能看起来并不是惯用语。

您对查看代码有何困扰?

NB 此答案假定您使用的是Elm 0.18

答案 2 :(得分:3)

这基本上就是要走的路,是的。 GitHub上的Elm中有一个较大的SPA的流行示例。在这里可以看到Main.elm,它负责映射每个页面的消息:https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm

您的示例缺少的一件事是绝对需要的消息类型的映射。我想您应该留出一个较小的职位,但是根据我的经验,这是样板所在的实际部分。

但是,您应该尝试不模仿像React这样的组件方法。只需使用功能。 SPA中的单独页面是一个示例,其中具有专用的消息类型和相应的功能就很有意义,就像使用program一样。

本文介绍了扩展大型Elm应用程序的一般方法,还提到了有关每个组件没有专用消息的要点。