【问题标题】:Composing Programs in the Elm Architecture在 Elm 架构中编写程序
【发布时间】:2018-07-03 14:08:04
【问题描述】:

假设我想创建一个包含两个组件的网页,例如 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 将Msg 从Cmd 的更新函数中提取出来,我该如何提取它们并将它们再次反馈给这些函数?另外,上面的视图函数看起来也不是特别地道。

elm-architecture 推荐的解决此问题的方法是什么?这种模式在 elm 架构中是惯用的吗?

【问题讨论】:

  • 只是补充一下,我不这么认为,如果这应该是您在 Elm 中创建 Web 应用程序的方法。组件式开发(如 In React、Polymer)与功能范式(如 Elm、PureScript)完全不同
  • @kishlaya 在 Elm 中创建 Web 应用程序的推荐方法是什么?
  • 您可以关注this页面获取指南。
  • 我要补充一点,如果您正在构建您的应用程序,并且最终得到了两个如上所述的独立模块,那么这是完全可以接受的,我在回答中描述的方式是您将如何组合他们。另一方面,还有其他方法可以考虑构建您的应用程序,而不涉及组件式思维。以tutorial 为例。

标签: elm composition elm-architecture


【解决方案1】:

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


如何增长 Elm 代码?

正如其他人所指出的:从组件的角度思考几乎肯定会让你走上一条不太吸引 Elm 的道路。 (很多人都是从那里开始的。我和我的团队在 2 年前就开始了,我们花了 3 次应用程序/重大重新设计才达到我认为我们至少可以对基本原理感到满意的地步。) em>

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

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


navbar+body的情况

关于您的特殊情况——这并不罕见——这是我的经验。如果我们说我们的 web 应用程序有一个导航栏,然后是一些正文,那么这是对应用程序的一个相当静态的描述。这种描述可能适合基于组件的想法,但如果您想最终获得一个优雅的 Elm 应用程序,则帮助不大。

相反,值得尝试在这个抽象级别描述您的应用的行为,这听起来可能像这样:用户可以选择x,y ,z 导航栏中的项目。单击这些项目将以q 的方式影响项目,也会以ab 的方式影响正文。他还可以点击导航栏中的v,这将显示一个弹出窗口,或者执行w,这会将他从应用程序中注销。

如果您采用此描述并应用我上面描述的逻辑,您可能最终会得到某种设计,其中大部分导航栏都以最高抽象级别进行描述。这包括项目xyzv 和行为abw。现在,行为a 可能意味着必须显示一个特定的、丰富的页面,该页面具有在较低抽象级别上描述的自己的详细行为,而行为b 可能意味着基于选择,某些内容必须是加载,并再次在较低的抽象级别上制定此加载过程的细节。以此类推。

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

关注应用的行为而不是静态内容区域对此有所帮助。

【讨论】:

    【解决方案2】:

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

    在视图中你需要使用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,因此我们可以将model 中的body 字段设置为它。但是,newCmd 的类型为 Cmd Body.Msg,因此在我们返回之前,我们需要使用 Cmd.map 来获取正确的返回类型 Cmd Msg

    类似的推理可用于NavbarMsg 案例。

    另外,上面的视图函数看起来也不是特别地道。

    查看代码有什么困扰你的地方?

    注意 此答案假设您使用的是 Elm 0.18

    【讨论】:

    • ! 表示法是什么?
    • view 函数的问题在于,它在两个东西周围都包裹了一个额外的<div></div> 标签。为什么我不能在没有额外包装的情况下将两个 Html 元素一个接一个地并置?
    • @AgnisomChattopadhyay (!) : model -> List (Cmd msg) -> (model, Cmd msg) 你可以认为是model ! [msgs] = (model, batch [msgs])
    • @AgnisomChattopadhyay (!) 是一个定义为here 的函数,我用它作为写({ model | body = newBody }, Cmd.batch [ Cmd.map BodyMsg newCmd ]) 的速记。除了在这种情况下不需要它,因为单个命令不需要Cmd.batch。所以我可以写({ model | body = newBody }, Cmd.map BodyMsg newCmd)
    • @Svilen 根据提出的问题,上面的代码很好。另一个问题是他是否应该把所有东西都分解成这样的组件,这样就需要像上面这样的代码。我必须查看所有源代码才能了解如何最好地重构它,如果有的话。在我看来,代码是以面向组件的风格开发的,因此导致了这个问题。另一种方法是从数据结构和函数的角度进行思考,例如github.com/dwayne/elm-2048。埃文对此进行了精彩的讨论,youtube.com/watch?v=XpDsk374LDE
    【解决方案3】:

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

    您的示例中缺少的一件事是绝对需要的消息类型的映射。我猜你为了写一个更小的帖子而忽略了这一点,但根据我的经验,这是样板文件的实际部分。

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

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

    【讨论】:

    • “这篇文章解释...” - 抱歉,链接在哪里?
    猜你喜欢
    • 1970-01-01
    • 2010-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多