假设我要创建一个包含两个组件的网页,例如Navbar
和Body
。这两个组件不会互相影响,可以独立开发。因此,我有两个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.update
或Body.update
的Cmd更新功能中提取了Msg,如何提取它们并将它们再次反馈给这些功能?另外,上面的view函数看起来并不是特别习惯。
推荐使用elm架构解决此问题的方法是什么?这种模式在榆木建筑中是惯用的吗?
答案 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
的方式影响该项目,也将以q
或a
的方式影响主体。他还可以单击导航栏中的b
,这将显示一个弹出窗口,或者单击v
,这会将他退出应用程序。
如果您采用此描述并应用了我上面描述的逻辑,则您可能最终应该进行某种设计,其中大多数导航栏都以最高抽象级别描述。这包括项目w
,x
,y
,z
和行为v
,a
,b
。现在,行为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应用程序的一般方法,还提到了有关每个组件没有专用消息的要点。