Home Halogen-04-执行Effects
Post
Cancel

Halogen-04-执行Effects

Performing Effects

到目前为止,我们已经涵盖了很多领域。您知道如何编写Halogen HTML。您可以定义响应用户交互的组件并按类型对组件的每个部分进行建模, 有了这个基础,我们就可以在编写应用程序时继续使用另一个重要的工具: 执行effects

在本章中,我们将通过两个示例探索如何在您的组件中执行effects: 生成随机数并发出HTTP请求。一旦您知道如何执行effects,您就可以很好地掌握Halogen基础知识.

在我们开始之前,重要的是要知道您只能在评估期间执行effects,例如像handleAction这样使用HalogenM类型的函数. 但是您无法在生成初始状态或渲染期间执行effects。由于您只能在HalogenM中执行effects,因此在深入研究示例之前,让我们简要了解一下它的更多信息。

HalogenM类型

如果您还记得上一章的内容,handleAction函数会返回一个名为HalogenM的类型。这是我们写的handleAction:

handleAction :: forall output m. Action -> HalogenM State Action () output m Unit

HalogenMHalogen的关键部分,通常称为eval单子。这个monad启用了Halogen特性,如状态、分叉线程、开始订阅等。但它非常有限,仅与Halogen特定的性质有关。事实上,Halogen组件没有内置的effects机制!

相反,Halogen允许您选择要在组件中与HalogenM一起使用的monad。您可以访问HalogenM的所有功能以及您选择的monad支持的任何功能. 这用类型参数m表示,它代表monad

仅使用Halogen特定性质的组件可以让此类型参数保持打开状态。例如,我们的计数器只更新状态。但是执行effects的组件可以使用 EffectAff monad,或者您可以提供自己的自定义monad

这个handleAction可以使用来自HalogenM的函数,比如modify_,也可以使用来自Effecteffectful函数:

handleAction :: forall output. Action -> HalogenM State Action () output Effect Unit

这个可以使用来自HalogenM的函数以及来自Affeffectful函数:

handleAction :: forall output. Action -> HalogenM State Action () output Aff Unit

Halogen中更常见的是对类型参数m使用约束来描述monad可以做什么,而不是选择特定的monad, 这允许您随着应用程序的增长将多个monad混合在一起。例如,大多数Halogen应用程序将通过以下类型签名使用来自Aff的函数:

handleAction :: forall output m. MonadAff m => Action -> HalogenM State Action () output m Unit

这让您可以完成硬编码Aff类型所做的一切,但也可以让您混合其他约束。

最后一件事: 当你为你的组件选择一个monad时,它会出现在你的HalogenM类型、你的Component类型中,如果你正在使用子组件,那么它会出现在你的ComponentHTML类型中:

component :: forall query input output m. MonadAff m => H.Component query input output m

handleAction :: forall output m. MonadAff m => Action -> HalogenM State Action () output m Unit

-- We aren't using child components, so we don't have to use the constraint here, but
-- we'll learn about when it's required in the parent & child components chapter.
render :: forall m. State -> H.ComponentHTML Action () m

一个Effect例子: 随机数

让我们创建一个新的简单组件,每次单击按钮时都会生成一个新的随机数。在您阅读示例时,请注意它如何使用与我们用于编写计数器的相同类型和函数。随着时间的推移,您将习惯于快读Halogen组件的状态、动作和其他类型,以了解其功能的要点,并熟悉标准函数,如initialStaterenderhandleAction

您可以将此示例粘贴到Try Purescript中以交互方式探索它。您还可以在此存储库的示例目录中查看并运行完整的示例代码

请注意,我们没有在我们的initialStaterender函数中执行任何effects – 例如,我们将我们的状态初始化为Nothing而不是为我们的初始状态生成一个随机数 —— 但是我们可以在我们的handleAction函数(使用HalogenM类型)中自由地执行effects

module Main where

import Prelude

import Data.Maybe (Maybe(..), maybe)
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Random (random)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = runHalogenAff do
  body <- awaitBody
  runUI component unit body

type State = Maybe Number

data Action = Regenerate

component :: forall query input output m. MonadEffect 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 _ = Nothing

render :: forall m. State -> H.ComponentHTML Action () m
render state = do
  let value = maybe "No number generated yet" show state
  HH.div_
    [ HH.h1_
        [ HH.text "Random number" ]
    , HH.p_
        [ HH.text ("Current value: " <> value) ]
    , HH.button
        [ HE.onClick \_ -> Regenerate ]
        [ HH.text "Generate new number" ]
    ]

handleAction :: forall output m. MonadEffect m => 
    Action -> 
    H.HalogenM State Action () output m Unit
handleAction = case _ of
  Regenerate -> do
    newNumber <- H.liftEffect random
    H.modify_ \_ -> Just newNumber

如您所见,执行effects的组件与不执行effects的组件没有太大区别!我们只做了两件事:

  • 我们为组件和handleAction函数的m类型参数添加了MonadEffect约束. 我们的render函数不需要约束,因为我们没有任何子组件。
  • 我们实际上第一次使用了一个effect: random函数,它来自Effect.Random

让我们再分解一下这个effect

--                          [1]
handleAction :: forall output m. MonadEffect m => 
        Action -> 
        H.HalogenM State Action () output m Unit
handleAction = case _ of
  Regenerate -> do
    newNumber <- H.liftEffect random -- [2]
    H.modify_ \_ -> Just newNumber   -- [3]
  • 我们已经限制了我们的m类型参数说我们支持任何monad,只要那个monad支持MonadEffect. 这是”我们需要能够在我们的评估代码中使用Effect函数”的另一种说法。
  • 随机函数的类型为Effect Number。但是我们不能直接使用它:我们的组件不支持Effect而是支持任何monad m,只要该monad可以从Effect运行effects. 这是一个细微的区别,但最终我们要求random函数的类型为MonadEffect m => m Number而不是直接为 Effect. 幸运的是,我们可以使用LiftEffect函数将任何Effect类型转换为MonadEffect m => m。这是Halogen中的常见模式,因此如果您使用MonadEffect,请记住liftEffect
  • modify_函数让你更新状态,它直接来自带有其他状态更新功能的HalogenM。在这里,我们使用它来将新的随机数写入我们的状态。

这是一个很好的示例,说明您可以如何自由地将Effect中的effects与特定于Halogen的函数(如modify_)交织在一起。让我们再做一次,这次使用Aff monad来实现异步效果。

一个Aff例子: HTTP Requests

Internet上的其他地方获取信息是很常见的。例如,假设我们想使用GitHubAPI来获取用户。我们将使用affjax包来发出我们的请求, 它本身依赖于Aff monad来实现异步效果。

不过,这个例子更有趣: 我们还将使用preventDefault函数来防止表单提交刷新页面,该页面在Effect中运行。这意味着我们的示例展示了如何将不同的effects(EffectAff)与Halogen函数(HalogenM)交织在一起。

Random示例一样,您可以将此示例粘贴到Try Purescript中以交互方式探索它。您还可以在此存储库的examples目录中查看并运行完整的示例代码.

这个组件定义应该看起来很熟悉。我们定义我们的StateAction类型并实现我们的initialStaterenderhandleAction函数. 我们将它们组合到我们的组件规范中,并将它们变成一个有效的组件H.mkComponent

再次注意,我们的effects集中在handleAction函数中,并且在构造初始状态或渲染Halogen HTML时没有执行任何effects

module Main where

import Prelude

import Affjax as AX
import Affjax.ResponseFormat as AXRF
import Data.Either (hush)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.VDom.Driver (runUI)
import Web.Event.Event (Event)
import Web.Event.Event as Event

main :: Effect Unit
main = runHalogenAff do
  body <- awaitBody
  runUI component unit body

type State =
  { loading :: Boolean
  , username :: String
  , result :: Maybe String
  }

data Action
  = SetUsername String
  | MakeRequest Event

component :: forall query input output m. MonadAff 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 _ = { loading: false, username: "", result: Nothing }

render :: forall m. State -> H.ComponentHTML Action () m
render st =
  HH.form
    [ HE.onSubmit \ev -> MakeRequest ev ]
    [ HH.h1_ [ HH.text "Look up GitHub user" ]
    , HH.label_
        [ HH.div_ [ HH.text "Enter username:" ]
        , HH.input
            [ HP.value st.username
            , HE.onValueInput \str -> SetUsername str
            ]
        ]
    , HH.button
        [ HP.disabled st.loading
        , HP.type_ HP.ButtonSubmit
        ]
        [ HH.text "Fetch info" ]
    , HH.p_
        [ HH.text $ if st.loading then "Working..." else "" ]
    , HH.div_
        case st.result of
          Nothing -> []
          Just res ->
            [ HH.h2_
                [ HH.text "Response:" ]
            , HH.pre_
                [ HH.code_ [ HH.text res ] ]
            ]
    ]

handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
  SetUsername username -> do
    H.modify_ _ { username = username, result = Nothing }

  MakeRequest event -> do
    H.liftEffect $ Event.preventDefault event
    username <- H.gets _.username
    H.modify_ _ { loading = true }
    response <- H.liftAff $ AX.get AXRF.string ("https://api.github.com/users/" <> username)
    H.modify_ _ { loading = false, result = map _.body (hush response) }

这个例子特别有趣,因为:

  • 它混合了来自多个monad的函数(preventDefaultEffectAX.getAffgetsmodify_HalogenM)。我们可以使用liftEffectliftAff以及我们的约束来确保一切都很好地协同工作。
  • 我们只有一个约束,MonadAff。那是因为任何可以在Effect中运行的东西也可以在Aff中运行,所以MonadAff意味着 MonadEffect
  • 我们正在一次评估中进行多个状态更新。

最后一点特别重要:当您修改组件呈现的状态时。这意味着在本次评估期间,我们:

  • loading设置为true,这会导致组件重新渲染并显示Working..
  • loading设置为false并更新结果,这会导致组件重新渲染并显示结果(如果有的话)。

值得注意的是,因为我们使用的是MonadAff,所以我们的请求不会阻止组件做其他工作,而且我们不必处理回调来获得这种异步超能力。我们在MakeRequest中编写的计算只是暂停,直到我们得到响应,然后继续第二次更新状态。

仅在必要时修改状态并在可能的情况下一起批量更新是一个聪明的主意(就像我们调用modify_一次来更新loadingresult字段一样). 这有助于确保您只在需要时重新渲染。

重新审视事件处理

在这个例子中发生了很多事情,所以值得花点时间关注它引入的新事件处理特性。与按钮示例的简单点击处理程序不同, 此处定义的处理程序确实使用了它们所提供的事件数据:

  • 用户名输入的值由onValueInput处理程序(SetUsername操作)使用。
  • onSubmit处理程序(MakeRequest操作)中的事件上调用preventDefault

传递给处理程序的参数类型取决于用于附加它的函数。有时,对于onValueInput,处理程序只是接收从事件中提取的数据 - 在这种情况下是字符串。大多数其他on...函数设置一个处理程序来接收整个事件,或者作为Event类型的值,或者作为像MouseEvent这样的特殊类型。详细信息可以在Halogen.HTML.Events的模块文档中找到;用于事件的类型和函数可以在web-eventsweb-uievents包中找到。

This post is licensed under CC BY 4.0 by the author.

Halogen-03-组件

Halogen-05-生命周期和订阅