在 Elm 架构中编写程序

Composing Programs in the 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,我该如何提取它们并将它们再次反馈给这些函数?另外,上面的视图函数看起来并不特别地道。

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

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

您的示例中缺少的一件事是绝对需要的消息类型的映射。我猜你忽略了它以获得更小的 post,但根据我的经验,这是样板文件的实际部分。

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

一样

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

是的,你走对了。

在您需要使用的视图中 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,因此在我们可以 return 之前,我们需要使用 Cmd.map 来获得 Cmd Msg 的正确 return 类型。

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

Also, the view function above does not particularly look idiomatic.

您对视图代码有什么困扰?

N.B. 此答案假定您使用的是 Elm 0.18.

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


如何增长 Elm 代码?

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

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

实际上每个节点都在自己的 Elm 模块中实现:parents 导入它们的 children。您可能还认为您不必坚持通常的 model/update/view 签名,而是应该关注应用域的特殊性。这就是 - 在我的阅读中 - 理查德费尔德曼在他的 Real World SPA example app. And Evan's Life of a file talk 中所做的也与这个问题有关。


navbar+body的情况

关于您的特殊情况——这并非罕见——这是我的经验。如果我们说我们的 webapp 有一个导航栏,然后是一些 body,这是对应用程序的相当静态的描述。这种描述可能符合基于组件的思维,但如果你想最终得到一个优雅的 Elm 应用程序,它就没那么有用了。

相反,值得尝试在此抽象级别上描述应用程序的行为,这听起来可能是这样的:用户可以select x,y,z 导航栏中的项目。单击这些项目将以 q 方式影响项目,并且还会以 ab 方式影响 body。他还可以单击导航栏中的 v,这将显示一个弹出窗口或执行 w,这会使他退出应用程序。

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

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

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