组件介绍
Halogen HTML
是Halogen
应用程序的基本构建块之一。但是,生成HTML
的纯函数缺少实际应用程序所需的许多基本功能: 表示随时间变化的值的状态、对网络请求等事物的effects
以及响应DOM
事件的能力(例如,当用户单击按钮时).
Halogen
组件接受input
并生成Halogen HTML
, 就像我们目前看到的函数一样。然而, 与函数不同的是,组件维护内部状态,可以更新其状态或执行响应事件的effects
,并且可以与其他组件通信。
Halogen
使用组件架构。这意味着Halogen
使用组件让您将UI
拆分为独立的、可重复使用的部件并单独考虑每个部件。然后,您可以将组件组合在一起以生成复杂的应用程序.
例如,每个Halogen
应用程序都由至少一个组件组成,称为root
组件。Halogen
组件可以包含更多组件,生成的组件树构成您的Halogen
应用程序。
在本章中,我们将学习编写Halogen
组件的大部分基本类型和函数。对于初学者来说,这是本指南中最难的一章,因为其中许多概念都是全新的。如果您第一次阅读它感到不知所措,请不要担心!当您编写Halogen
应用程序时,您将一遍又一遍地使用这些类型和函数,它们很快就会成为第二天性。如果您在阅读本章时遇到困难,请尝试在构建一个不同于此处描述的简单组件的同时再次阅读它。
在本章中,我们还将看到更多Halogen
声明式编程风格的示例。当你编写一个组件时,你负责描述对于任何给定的内部状态应该存在什么样的UI
。Halogen
在幕后更新实际的DOM
元素以匹配您想要的UI
。
一个小例子
我们已经看到了一个简单的组件示例:一个可以递增或递减的计数器。
module Main where
import Prelude
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
data Action = Increment | Decrement
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval { handleAction = handleAction }
}
where
initialState _ = 0
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
]
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1
该组件维护一个整数作为其内部状态,并更新该状态以响应两个按钮上的点击事件。
这个组件可以工作,但在现实世界的应用程序中,我们不会不指定所有类型。让我们用它使用的所有类型从头开始重建这个组件。
构建基本组件(带类型)
典型的Halogen
组件接受input
,维持内部状态, 从该状态生成Halogen HTML
, 并根据事件更新其状态或执行effects
, 在这个实例中,我们不需要执行任何effects
,但我们很快就会介绍它们。
让我们分解组件的每个部分,并在此过程中分配类型。
处理输入
Halogen
组件可以接受来自父组件或应用程序根的input
。如果您将组件视为一个函数,那么input
就是该函数的参数。
如果你的组件接受input
,那么你应该用类型来描述它。例如,接受整数作为input
的组件将使用以下类型:
type Input = Int
我们的计数器不需要任何input
,所以我们有两个选择。 首先,我们可以说我们的输入类型是Unit
,这意味着我们只需要一个虚拟值并将其丢弃:
type Input = Unit
其次,更常见的是,我们的input
类型出现在组件中的任何地方,我们都可以简单地将其保留为类型变量forall i. ...
, 使用任何一种方法都很好,但从这里开始,我们将使用类型变量来表示我们的组件不使用的类型。
状态
Halogen
组件会随着时间的推移保持内部状态,用于驱动组件的行为并生成HTML
。我们的counter
组件维护当前计数,一个整数,所以我们将使用它作为我们的状态类型:
type State = Int
我们的组件还需要产生一个初始状态值。所有Halogen
组件都需要一个initialState
函数,该函数从input
值生成初始状态:
initialState :: Input -> State
我们的counter
组件不使用它的input
,所以我们的initialState
函数不会使用input
类型,而只会让该类型变量保持打开状态。当组件运行时,我们的计数器应该从0
开始。
initialState :: forall input. input -> State
initialState _ = 0
处理动作
Halogen
组件可以更新状态、执行effects
并与其他组件通信以响应内部出现的事件, 组件使用action
类型来描述组件可以做哪些事情来响应内部事件。
我们的计数器有两个内部事件:
-
按钮上的单击事件以减少计数+
按钮上的单击事件以增加计数
我们可以使用我们称为Action
的数据类型来描述我们的组件应该做什么来响应这些事件:
data Action = Increment | Decrement
这种类型表示我们的组件能够递增和递减。稍后,我们将看到在我们的HTML
中使用这种类型——Halogen
声明特性的另一个例子。
就像我们的state
类型必须与一个描述如何生成State
值的initialState
函数配对一样,我们的Action
类型应该与一个名为handleAction
的函数配对,该函数描述当这些actions
之一发生时要做什么。
handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit
与我们的input
类型一样,我们可以为我们不使用的类型保留类型变量。
- 类型
()
表示我们的组件没有子组件。我们也可以将它作为类型变量打开,因为我们没有使用它 ——slots
,按照惯例 —— 但是()
太短了,您会看到这种类型被普遍使用。 output
类型参数仅在您的组件与父组件通信时使用。m
类型参数仅在您的组件执行effects
时相关。
由于我们的计数器没有子组件,我们将使用()
来描述它们,并且因为它不与父组件通信或执行effects
,我们将保持output
和m
类型变量打开。
这是我们计数器的handleAction
函数:
handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1
我们的handleAction
函数通过将我们的状态变量减少1
来响应Decrement
,并通过将我们的状态变量增加1
来响应 Increment
。
modify
允许您更新状态,给定先前的状态,返回新状态modify_
与modify
相同,但它不返回新状态(因此您不必像使用modify
那样显式丢弃结果)get
允许您检索当前状态gets
允许您检索当前状态并对其应用函数(最常见的是_.fieldName
从记录中检索特定字段)
当我们谈论执行effects
时,我们会更多地谈论HalogenM
。我们的计数器不执行effects
,所以我们需要的只是状态更新函数。
渲染函数
Halogen
组件使用称为render
的函数从它们的状态生成HTML
。每次状态改变时渲染函数都会运行。这就是Halogen
声明性的原因: 对于任何给定的状态,您描述它对应的UI
。Halogen
处理确保状态更改始终导致您描述的UI
的工作量。
Halogen
中的渲染函数是纯的,这意味着您无法在渲染过程中执行诸如获取当前时间、发出网络请求或其他类似操作。您所能做的就是为您的状态值生成HTML
。
当我们查看渲染函数的类型时,我们可以看到我们在上一章中提到的ComponentHTML
类型。此类型是HTML
类型的更专业版本, 专门用于组件中生成的HTML
。再次, 我们将使用()
并让m
保持打开状态,因为它们仅在使用子组件时相关,我们将在后面的章节中介绍。
render :: forall m. State -> H.ComponentHTML Action () m
现在我们正在使用我们的渲染函数,我们回到上一章应该熟悉的Halogen HTML
!你可以像上一章一样在ComponentHTML
中编写常规HTML
:
import Halogen.HTML.Events
render :: forall m. State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
]
处理事件
我们现在可以看到如何在Halogen
中处理事件。首先,您在属性数组中编写事件处理程序以及您可能需要的任何其他properties
、attributes
和引用,然后,您将事件处理程序与组件知道如何处理的Action
相关联。最后,当事件发生时,你的handleAction
函数被调用来处理事件。
您可能很好奇我们为什么要向onClick
提供匿名函数。要了解原因,我们可以查看onClick
的实际类型:
onClick
:: forall row action
. (MouseEvent -> action)
-> IProp (onClick :: MouseEvent | row) action
-- Specialized to our component
onClick
:: forall row
. (MouseEvent -> Action)
-> IProp (onClick :: MouseEvent | row) Action
在Halogen
中,事件处理程序将回调作为它们的第一个参数。此回调接收发生的DOM
事件(在单击事件的情况下,这是一个 MouseEvent
), 它包含一些您可能想要使用的元数据,然后负责返回Halogen
应运行以响应事件的action
。在我们的例子中,我们不会检查事件本身,所以我们扔掉参数并返回我们想要运行的action
(Increment
或Decrement
)。
然后onClick
函数返回IProp
类型的值。你应该记得上一章的IProp
。作为复习,Halogen HTML
元素指定了它们支持的属性和事件的列表。属性和事件依次指定它们的类型。Halogen
然后能够确保您永远不会在不支持它的元素上使用属性或事件。在这种情况下,按钮确实支持onClick
事件,所以我们很高兴!
在这个简单的例子中,MouseEvent
参数被传递给onClick
的处理函数忽略,因为action
完全由哪个按钮接收click
来决定。在查看本指南第3
部分中的effects
后,我们将讨论访问事件本身。
把这一切结合在一起
让我们将每个类型和函数重新组合起来以生成我们的计数器组件——这次是指定类型。让我们重新审视我们编写的类型和函数:
-- This can be specified if your component takes input, or you can leave
-- the type variable open if your component doesn't.
type Input = Unit
type State = Int
initialState :: forall input. input -> State
initialState = ...
data Action = Increment | Decrement
handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit
handleAction = ...
render :: forall m. State -> H.ComponentHTML Action () m
render = ...
这些类型和函数是典型Halogen
组件的核心构建块。但是像这样仅靠它们是不够的 —— 我们需要将它们集中到一个地方。
我们将使用H.mkComponent
函数来做到这一点。该函数接受一个ComponentSpec
,它是一个包含initialState
、render
和eval
函数的记录,并从中生成一个Component
:
component =
H.mkComponent
{ -- First, we provide our function that describes how to produce the first state
initialState
-- Then, we provide our function that describes how to produce HTML from the state
, render
-- Finally, we provide our function that describes how to handle actions that
-- occur while the component is running, which updates the state.
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
我们将在以后的章节中更多地讨论eval
函数。暂时您可以将eval
函数视为定义组件如何响应事件; 目前,我们唯一关心的事件类型是actions
,因此我们将使用的唯一函数是handleAction
。
我们的组件现在已经完成,但我们缺少最后一个类型定义:我们的组件类型。
H.Component类型
mkComponent
函数从一个ComponentSpec
生成一个组件,它是Halogen
运行一个组件所需的函数的记录。我们将在后续章节中更详细地介绍这种类型。
mkComponent :: H.ComponentSpec ... -> H.Component query input output m
生成的组件的类型为H.Component
,它本身采用四个类型参数来描述组件的公共接口, 我们的组件不与父组件或子组件通信,因此它不使用任何这些类型变量。不过,我们现在将简要介绍它们,以便您了解后续章节中的内容。
- 第一个参数
query
表示父组件可以与此组件通信的方式。说到父组件和子组件时,我们会更多地讨论它。 - 第二个参数
input
代表我们的组件接受的input
。在我们的例子中,组件不接受任何输入,所以我们将保持这个变量打开。 - 第三个参数
output
表示该组件与其父组件进行通信的一种方式。当我们谈论父组件和子组件时,我们会更多地谈论它。 - 最后一个参数
m
表示可用于在组件中运行effects
的monad
。我们的组件不运行任何effects
,所以我们将这个变量保持打开状态。
因此,我们可以通过将所有H.Component
类型变量保持打开状态来指定我们的计数器组件。
最终产品
这是很多东西!我们终于用类型完全指定了我们的计数器组件。如果您可以轻松地构建这样的组件,您基本上是全面了解构建 Halogen
组件的大部分方法。本指南的其余部分将建立在您对state
、action
和rendering HTML
的理解之上。
我们添加了一个主函数来运行我们的Halogen
应用程序,以便您可以通过将其粘贴到Try PureScript中来试用此示例, 我们将在后面的章节中介绍如何运行Halogen
应用程序 —— 现在您可以忽略main
函数并专注于我们定义的组件。
module Main where
import Prelude
import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type State = Int
data Action = Increment | Decrement
component :: forall query input output m. H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = 0
render :: forall m. State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
]
handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1