Home Halogen-03-组件
Post
Cancel

Halogen-03-组件

组件介绍

Halogen HTMLHalogen应用程序的基本构建块之一。但是,生成HTML的纯函数缺少实际应用程序所需的许多基本功能: 表示随时间变化的值的状态、对网络请求等事物的effects以及响应DOM事件的能力(例如,当用户单击按钮时).

Halogen组件接受input并生成Halogen HTML, 就像我们目前看到的函数一样。然而, 与函数不同的是,组件维护内部状态,可以更新其状态或执行响应事件的effects,并且可以与其他组件通信。

Halogen使用组件架构。这意味着Halogen使用组件让您将UI拆分为独立的、可重复使用的部件并单独考虑每个部件。然后,您可以将组件组合在一起以生成复杂的应用程序.

例如,每个Halogen应用程序都由至少一个组件组成,称为root组件。Halogen组件可以包含更多组件,生成的组件树构成您的Halogen应用程序。

在本章中,我们将学习编写Halogen组件的大部分基本类型和函数。对于初学者来说,这是本指南中最难的一章,因为其中许多概念都是全新的。如果您第一次阅读它感到不知所措,请不要担心!当您编写Halogen应用程序时,您将一遍又一遍地使用这些类型和函数,它们很快就会成为第二天性。如果您在阅读本章时遇到困难,请尝试在构建一个不同于此处描述的简单组件的同时再次阅读它。

在本章中,我们还将看到更多Halogen声明式编程风格的示例。当你编写一个组件时,你负责描述对于任何给定的内部状态应该存在什么样的UIHalogen在幕后更新实际的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,我们将保持outputm类型变量打开。

这是我们计数器的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声明性的原因: 对于任何给定的状态,您描述它对应的UIHalogen处理确保状态更改始终导致您描述的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中处理事件。首先,您在属性数组中编写事件处理程序以及您可能需要的任何其他propertiesattributes和引用,然后,您将事件处理程序与组件知道如何处理的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(IncrementDecrement)。

然后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,它是一个包含initialStaterendereval函数的记录,并从中生成一个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表示可用于在组件中运行effectsmonad。我们的组件不运行任何effects,所以我们将这个变量保持打开状态。

因此,我们可以通过将所有H.Component类型变量保持打开状态来指定我们的计数器组件。

最终产品

这是很多东西!我们终于用类型完全指定了我们的计数器组件。如果您可以轻松地构建这样的组件,您基本上是全面了解构建 Halogen组件的大部分方法。本指南的其余部分将建立在您对stateactionrendering 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
This post is licensed under CC BY 4.0 by the author.

Halogen-02-渲染HalogenHTML

Halogen-04-执行Effects